ライフタイムと基本操作

まもなく、プログラマがアプリケーション固有の型を定義できる基本的な機能である構造体について説明する。構造体は、基本型や他の構造体を組み合わせて、プログラムの特別なニーズに応じて動作する高レベルの型を定義するためのものだ。構造体の後には、Dのオブジェクト指向プログラミング機能の基礎となるクラスについて学ぶ。

構造体とクラスについて学ぶ前に、まずいくつかの重要な概念について説明しておこう。これらの概念は、構造体とクラス、およびそれらの違いを理解するのに役立つ。

これまで、プログラム内の概念を表すデータの断片を"変数"と呼んできた。いくつかの箇所では、構造体およびクラスの変数を"オブジェクト"と具体的に呼んだ。この章では、これらの概念を両方とも"変数"と呼び続ける。

この章では、基本的な型、スライス、および連想配列についてのみ説明するが、これらの概念はユーザー定義型にも適用される。

変数のライフタイム

変数が定義されてから最終化されるまでの時間は、その変数の寿命である。多くの型では、使用できなくなることと 最終化されることは同時に起こる必要はない。

変数が利用できなくなる仕組みは、名前スコープの章で学んだ通りだ。単純なケースでは、変数が定義されたスコープから抜けると、その変数は利用できなくなる。

次の例で思い出そう。

void speedTest() {
    int speed;               // 単一の変数 ...

    foreach (i; 0 .. 10) {
        speed = 100 + i;     // ... は10種類の値を取る。
        // ...
    }
} // ← この時点以降、'speed'は使用できなくなる。
D

このコードでは、speed変数の寿命は、speedTest()関数から抜け出すと終了する。上記のコードには1つの変数があり、100から109までの10個の異なる値を取る。

変数のライフタイムに関しては、次のコードは前のコードと大きく異なる:

void speedTest() {
    foreach (i; 0 .. 10) {
        int speed = 100 + i; // 10個の別々の変数。
        // ...
    } // ← 各変数の寿命はここで終了する。
}
D

このコードには10個の個別の変数があり、それぞれが1つの値を取る。ループが1回繰り返されるたびに、新しい変数がその寿命を開始し、各反復の終了時にその寿命は終了する。

パラメーターの寿命

パラメーターのライフタイムは、その修飾子によって決まる:

ref: パラメータは、関数を呼び出すときに指定される実際の変数の単なる別名である。refパラメータは、実際の変数の有効期間に影響を与えない。

in:値型の場合、パラメータの寿命は関数に入った時点で始まり、関数から出た時点で終わる。参照型の場合、パラメータの寿命はrefと同じである。

out: refと同様、パラメータは関数を呼び出すときに指定される実際の変数の別名にすぎない。唯一の違いは、関数に入ったときに、変数が自動的にその.init値に設定されることだ。

lazy: パラメーターのライフタイムは、パラメーターが実際に使用された時点で開始され、その時点で終了する。

次の例では、これら4種類のパラメータを使用し、プログラムのコメントでそれらの有効期間を説明している。

void main() {
    int main_in;      /* main_inの値は、
                       * パラメータにコピーされる。 */

    int main_ref;     /* main_refは、
                       * それ自体として関数に渡される。 */

    int main_out;     /* main_outは、
                       * それ自体として関数に渡される。その値は
                       * 関数に入った時点でint.initに設定される。 */

    foo(main_in, main_ref, main_out, aCalculation());
}

void foo(
    in int p_in,       /* p_inの寿命は、
                        * 関数に入った時点で始まり、
                        * 関数から出た時点で終わる。 */

    ref int p_ref,     /* p_refはmain_refの別名だ。 */

    out int p_out,     /* p_outはmain_outの別名だ。
                        * その値は、
                        * 関数に入った時点でint.initに設定される。 */

    lazy int p_lazy) { /* p_lazyの寿命は、
                        * 使用された時点で始まり、その使用が
                        * 終了すると終了する。その値は、
                        * p_lazyが関数内で使用されるたびに
                        * aCalculation()を呼び出すことで計算される。 */
    // ...
}

int aCalculation() {
    int result;
    // ...
    return result;
}
D
lifetimes.1
基本的な操作

型に関係なく、変数のライフタイムを通じて3つの基本的な操作がある。

オブジェクトとみなされるためには、まず初期化される必要がある。一部の型には、最終操作がある場合もある。変数の値は、その存続期間中に変更される場合がある。

初期化

すべての変数は使用前に初期化する必要がある。初期化には2つのステップが含まれる:

  1. 変数用のスペースの予約:このスペースは、変数の値がメモリに格納される場所である。
  2. 構築:そのスペースに変数の最初の値(または構造体およびクラスのメンバーの最初の値)を設定する。

すべての変数は、メモリ内のその変数用に予約された場所に存在する。コンパイラが生成するコードの一部は、各変数のスペースの予約に関するものである。

次の変数を考えてみよう:

int speed = 123;
D

値型と参照型の章で見たように、この変数はメモリのどこかに存在すると考えることができる。

   ──┬─────┬─────┬─────┬──
     │     │ 123 │     │
   ──┴─────┴─────┴─────┴──

変数が置かれるメモリの場所は、その変数のアドレスと呼ばれる。ある意味で、変数はそのアドレスに存在している。変数の値が変更されると、新しい値は同じ場所に格納される。

++speed;
D

新しい値は、古い値があった場所と同じ場所になる。

   ──┬─────┬─────┬─────┬──
     │     │ 124 │     │
   ──┴─────┴─────┴─────┴──

変数を使用可能にするためには、構築が必要だ。変数は構築される前に信頼性を持って使用できないため、コンパイラが自動的に行う。

変数は3つの方法で構築される:

値が指定されていない場合、変数の値はその型のデフォルト値、つまり.init値になる。

int speed;
D

上記のspeedの値はint.initであり、これはたまたま0である。当然のことながら、デフォルト値によって構築された変数は、その有効期間中に他の値を持つことがある(immutableである場合を除く)。

File file;
D

上記の定義では、変数fileは、ファイルシステム上の実際のファイルにはまだ関連付けられていないFileオブジェクトである。ファイルに関連付けられるまで使用することはできない。

変数は、他の変数のコピーとして作成されることがある:

int speed = otherSpeed;
D

speedは、otherSpeedの値によって構築されている。

後述するように、この操作はクラス変数に対しては異なる意味を持つ:

auto classVariable = otherClassVariable;
D

classVariableは、otherClassVariableのコピーとして誕生するが、クラスには根本的な違いがある。speedotherSpeedは別々の値だが、classVariableotherClassVariableはどちらも同じ値にアクセスする。これが、値型と参照型の根本的な違いだ。

最後に、変数は、互換性のある型の式の値によって構築することができる。

int speed = someCalculation();
D

speed 上記は、someCalculation()の戻り値によって構築される。

最終化

ファイナル化とは、変数に対して実行される最終的な操作であり、そのメモリを解放する。

  1. 破壊: 変数に対して実行しなければならない最終的な操作。
  2. 変数のメモリの解放:変数が使用していたメモリの解放。

単純な基本型の場合、実行すべき最終操作はない。例えば、int型の変数の値は0にリセットされない。このような変数については、そのメモリを解放して、後で他の変数に使用できるようにするだけだ。

一方、一部の型の変数では、最終化時に特別な操作が必要になる。例えば、Fileオブジェクトでは、出力バッファに残っている文字をディスクに書き込んで、そのファイルを使用しなくなったことをファイルシステムに通知する必要がある。これらの操作は、Fileオブジェクトの破棄にあたる。

配列の最終操作は、もう少し高いレベルで行われる。配列を最終化する前に、まずその要素が破棄される。要素がintのような単純な基本型の場合、特別な最終操作は必要ない。要素が、最終化が必要な構造体またはクラス型の場合、その操作は各要素に対して実行される。

連想配列は配列と似ている。さらに、キーも、破棄が必要な型の場合は、最終化されることがある。

ガベージコレクタ:D ガベージコレクション言語だ。このような言語では、オブジェクトの最終化はプログラマが明示的に開始する必要はない。変数の寿命が終了すると、その最終化はガベージコレクタによって自動的に処理される。ガベージコレクタと特別なメモリ管理については、後の章で説明する。

変数は2つの方法で最終化される:

変数がどちらの方法で最終化されるかは、主にその型によって決まる。配列、連想配列、クラスなどの一部の型は、通常、将来のある時点でガベージコレクタによって破棄される。

代入

変数がそのlifetimeにおいて経験するもう1つの基本的な操作は、代入だ。

単純な基本型の場合、代入は単に変数の値を変更するだけだ。上記のメモリ表現で見たように、int変数の値は123から124に変わる。しかし、より一般的には、代入は2つのステップで構成されており、必ずしも以下の順序で実行されるとは限らない。

破壊を必要としない単純な基本型では、この2つのステップは重要ではない。破壊を必要とする型では、代入は上記の2つのステップの組み合わせであることを覚えておくことが重要だ。