ライフタイムと基本操作
まもなく、プログラマがアプリケーション固有の型を定義できる基本的な機能である構造体について説明する。構造体は、基本型や他の構造体を組み合わせて、プログラムの特別なニーズに応じて動作する高レベルの型を定義するためのものだ。構造体の後には、Dのオブジェクト指向プログラミング機能の基礎となるクラスについて学ぶ。
構造体とクラスについて学ぶ前に、まずいくつかの重要な概念について説明しておこう。これらの概念は、構造体とクラス、およびそれらの違いを理解するのに役立つ。
これまで、プログラム内の概念を表すデータの断片を"変数"と呼んできた。いくつかの箇所では、構造体およびクラスの変数を"オブジェクト"と具体的に呼んだ。この章では、これらの概念を両方とも"変数"と呼び続ける。
この章では、基本的な型、スライス、および連想配列についてのみ説明するが、これらの概念はユーザー定義型にも適用される。
変数のライフタイム
変数が定義されてから最終化されるまでの時間は、その変数の寿命である。多くの型では、使用できなくなることと 最終化されることは同時に起こる必要はない。
変数が利用できなくなる仕組みは、名前スコープの章で学んだ通りだ。単純なケースでは、変数が定義されたスコープから抜けると、その変数は利用できなくなる。
次の例で思い出そう。
このコードでは、speed
変数の寿命は、speedTest()
関数から抜け出すと終了する。上記のコードには1つの変数があり、100から109までの10個の異なる値を取る。
変数のライフタイムに関しては、次のコードは前のコードと大きく異なる:
このコードには10個の個別の変数があり、それぞれが1つの値を取る。ループが1回繰り返されるたびに、新しい変数がその寿命を開始し、各反復の終了時にその寿命は終了する。
パラメーターの寿命
パラメーターのライフタイムは、その修飾子によって決まる:
ref
: パラメータは、関数を呼び出すときに指定される実際の変数の単なる別名である。ref
パラメータは、実際の変数の有効期間に影響を与えない。
in
:値型の場合、パラメータの寿命は関数に入った時点で始まり、関数から出た時点で終わる。参照型の場合、パラメータの寿命はref
と同じである。
out
: ref
と同様、パラメータは関数を呼び出すときに指定される実際の変数の別名にすぎない。唯一の違いは、関数に入ったときに、変数が自動的にその.init
値に設定されることだ。
lazy
: パラメーターのライフタイムは、パラメーターが実際に使用された時点で開始され、その時点で終了する。
次の例では、これら4種類のパラメータを使用し、プログラムのコメントでそれらの有効期間を説明している。
基本的な操作
型に関係なく、変数のライフタイムを通じて3つの基本的な操作がある。
- 初期化: ライフの開始。
- 終結:その変数のライフサイクルの終了。
- 代入:値全体を変更する。
オブジェクトとみなされるためには、まず初期化される必要がある。一部の型には、最終操作がある場合もある。変数の値は、その存続期間中に変更される場合がある。
初期化
すべての変数は使用前に初期化する必要がある。初期化には2つのステップが含まれる:
- 変数用のスペースの予約:このスペースは、変数の値がメモリに格納される場所である。
- 構築:そのスペースに変数の最初の値(または構造体およびクラスのメンバーの最初の値)を設定する。
すべての変数は、メモリ内のその変数用に予約された場所に存在する。コンパイラが生成するコードの一部は、各変数のスペースの予約に関するものである。
次の変数を考えてみよう:
値型と参照型の章で見たように、この変数はメモリのどこかに存在すると考えることができる。
──┬─────┬─────┬─────┬── │ │ 123 │ │ ──┴─────┴─────┴─────┴──
変数が置かれるメモリの場所は、その変数のアドレスと呼ばれる。ある意味で、変数はそのアドレスに存在している。変数の値が変更されると、新しい値は同じ場所に格納される。
新しい値は、古い値があった場所と同じ場所になる。
──┬─────┬─────┬─────┬── │ │ 124 │ │ ──┴─────┴─────┴─────┴──
変数を使用可能にするためには、構築が必要だ。変数は構築される前に信頼性を持って使用できないため、コンパイラが自動的に行う。
変数は3つの方法で構築される:
- デフォルト値による構築:プログラマが値を明示的に指定しない場合
- コピーによって:変数が同じ型の別の変数のコピーとして構築される場合
- 特定の値で:プログラマが値を明示的に指定する場合
値が指定されていない場合、変数の値はその型のデフォルト値、つまり.init
値になる。
上記のspeed
の値はint.init
であり、これはたまたま0である。当然のことながら、デフォルト値によって構築された変数は、その有効期間中に他の値を持つことがある(immutable
である場合を除く)。
上記の定義では、変数file
は、ファイルシステム上の実際のファイルにはまだ関連付けられていないFile
オブジェクトである。ファイルに関連付けられるまで使用することはできない。
変数は、他の変数のコピーとして作成されることがある:
speed
は、otherSpeed
の値によって構築されている。
後述するように、この操作はクラス変数に対しては異なる意味を持つ:
classVariable
は、otherClassVariable
のコピーとして誕生するが、クラスには根本的な違いがある。speed
とotherSpeed
は別々の値だが、classVariable
とotherClassVariable
はどちらも同じ値にアクセスする。これが、値型と参照型の根本的な違いだ。
最後に、変数は、互換性のある型の式の値によって構築することができる。
speed
上記は、someCalculation()
の戻り値によって構築される。
最終化
ファイナル化とは、変数に対して実行される最終的な操作であり、そのメモリを解放する。
- 破壊: 変数に対して実行しなければならない最終的な操作。
- 変数のメモリの解放:変数が使用していたメモリの解放。
単純な基本型の場合、実行すべき最終操作はない。例えば、int
型の変数の値は0にリセットされない。このような変数については、そのメモリを解放して、後で他の変数に使用できるようにするだけだ。
一方、一部の型の変数では、最終化時に特別な操作が必要になる。例えば、File
オブジェクトでは、出力バッファに残っている文字をディスクに書き込んで、そのファイルを使用しなくなったことをファイルシステムに通知する必要がある。これらの操作は、File
オブジェクトの破棄にあたる。
配列の最終操作は、もう少し高いレベルで行われる。配列を最終化する前に、まずその要素が破棄される。要素がint
のような単純な基本型の場合、特別な最終操作は必要ない。要素が、最終化が必要な構造体またはクラス型の場合、その操作は各要素に対して実行される。
連想配列は配列と似ている。さらに、キーも、破棄が必要な型の場合は、最終化されることがある。
ガベージコレクタ:Dは ガベージコレクション言語だ。このような言語では、オブジェクトの最終化はプログラマが明示的に開始する必要はない。変数の寿命が終了すると、その最終化はガベージコレクタによって自動的に処理される。ガベージコレクタと特別なメモリ管理については、後の章で説明する。
変数は2つの方法で最終化される:
- ライフタイムが終了したとき:変数のライフタイムの終了時に最終化が行われる。
- 将来の任意の時点:ガベージコレクタによって、将来の任意の時点で最終化が行われる。
変数がどちらの方法で最終化されるかは、主にその型によって決まる。配列、連想配列、クラスなどの一部の型は、通常、将来のある時点でガベージコレクタによって破棄される。
代入
変数がそのlifetimeにおいて経験するもう1つの基本的な操作は、代入だ。
単純な基本型の場合、代入は単に変数の値を変更するだけだ。上記のメモリ表現で見たように、int
変数の値は123から124に変わる。しかし、より一般的には、代入は2つのステップで構成されており、必ずしも以下の順序で実行されるとは限らない。
- 古い値の破壊
- 新しい値の構築
破壊を必要としない単純な基本型では、この2つのステップは重要ではない。破壊を必要とする型では、代入は上記の2つのステップの組み合わせであることを覚えておくことが重要だ。