クラス

構造体と同様に、classは新しい型を定義するための機能だ。この定義により、クラスはユーザー定義型となる。構造体とは異なり、クラスはD言語のオブジェクト指向プログラミング(OOP)パラダイムを提供する。OOPの主な特徴は次の通りだ。

カプセル化は、後の章で説明する保護属性によって実現される。継承は、他の型の実装を取得するためのものだ。多態性は、プログラムの各部分を相互に抽象化するためのもので、クラスインターフェースによって実現される。

この章では、クラスを高いレベルで紹介し、クラスが参照型であることを強調する。クラスについては、後の章で詳しく説明する。

構造体との比較

一般的に、クラスは構造体とよく似ている。この章で構造体について説明した機能のほとんどは、クラスにも適用される。

ただし、クラスと構造体には重要な違いがある。

クラスは参照型

構造体との最大の違いは、構造体は値型であり、クラスは参照型であることだ。以下で説明するその他の違いは、ほとんどこの事実によるものだ。

クラス変数はnull

null値とis演算子の章で簡単に述べたように、クラス変数はnullになる。つまり、クラス変数はどのオブジェクトにもアクセスできない。クラス変数自体には値はない。実際のクラスオブジェクトは、newキーワードによって構築する必要がある。

また、nullへの参照を==または!=演算子で比較することはエラーになることも覚えておいてほしい。代わりに、比較はisまたは!is演算子を使用して行う必要がある:

MyClass referencesAnObject = new MyClass;
assert(referencesAnObject !is null);

MyClass variable;   // オブジェクトを参照していない
assert(variable is null);
D

その理由は、==演算子はオブジェクトのメンバーの値を参照する必要がある場合があり、null変数を通じてメンバーにアクセスしようとすると、メモリアクセスエラーが発生するためだ。そのため、クラス変数は常にisおよび!is演算子で比較する必要がある。

クラス変数とクラスオブジェクト

クラス変数とクラスオブジェクトは異なる概念だ。

クラスオブジェクトは、newキーワードによって構築され、名前はない。プログラム内でクラスタイプが実際に表す概念は、クラスオブジェクトによって提供される。例えば、Studentクラスが学生の名前と成績を表すと仮定すると、そのような情報はStudentオブジェクトのメンバーによって格納される。クラスオブジェクトは匿名であることもあって、直接アクセスすることはできない。

一方、クラス変数は、クラスオブジェクトにアクセスするための言語機能だ。文法的にはクラス変数に対して操作が行われているように見えるが、実際には操作はクラスオブジェクトにディスパッチされる。

値型と参照型の章で見た次のコードを考えてみよう。

auto variable1 = new MyClass;
auto variable2 = variable1;
D

newキーワードは匿名クラスオブジェクトを構築する。variable1variable2は、単にその匿名オブジェクトへのアクセスを提供するだけだ:

 (anonymous MyClass object)    variable1    variable2
 ───┬───────────────────┬───  ───┬───┬───  ───┬───┬───
    │        ...        │        │ o │        │ o │
 ───┴───────────────────┴───  ───┴─│─┴───  ───┴─│─┴───
              ▲                    │            │
              │                    │            │
              └────────────────────┴────────────┘
コピー

コピーは変数にのみ影響し、オブジェクトには影響しない。

クラスは参照型であるため、新しいクラス変数を別のクラスのコピーとして定義すると、同じオブジェクトへのアクセスを提供する2つの変数が作成される。実際のオブジェクトはコピーされない。

postblit関数this(this)は、クラスでは使用できない。

auto variable2 = variable1;
D

上記のコードでは、variable2variable1によって初期化されている。2つの変数は同じオブジェクトへのアクセスを開始する。

実際のオブジェクトをコピーする必要がある場合は、その目的のためのメンバー関数をクラスに用意する必要がある。配列との互換性を保つため、この関数の名前はdup()とする必要がある。この関数は、新しいクラスオブジェクトを作成して返す必要がある。さまざまな型のメンバーを持つクラスでこれを確認しよう。

class Foo {
    S      o;  // Sが構造体型であると仮定する
    char[] s;
    int    i;

// ...

    this(S o, const char[] s, int i) {
        this.o = o;
        this.s = s.dup;
        this.i = i;
    }

    Foo dup() const {
        return new Foo(o, s, i);
    }
}
D

dup()メンバー関数は、Fooのコンストラクタを利用して新しいオブジェクトを作成し、その新しいオブジェクトを返す。コンストラクタは、配列の.dupプロパティによって、sメンバーを明示的にコピーすることに注意。値型であるoおよびiは自動的にコピーされる。

次のコードは、dup()を使用して新しいオブジェクトを作成する。

auto var1 = new Foo(S(1.5), "hello", 42);
auto var2 = var1.dup();
D

その結果、var1var2に関連付けられたオブジェクトは異なるものになる。

同様に、immutableのコピーは、idup()という適切な名前のメンバー関数によって提供することができる。この場合、コンストラクタもpureとして定義する必要がある。pureキーワードについては、後の章で説明する。

class Foo {
// ...
    this(S o, const char[] s, int i) pure {
        // ...

    }
    immutable(Foo) idup() const {
        return new immutable(Foo)(o, s, i);
    }
}

// ...

    immutable(Foo) imm = var1.idup();
D
代入

コピーと同様に、代入は変数にのみ影響する。

クラス変数に代入すると、その変数は現在のオブジェクトから切り離され、新しいオブジェクトと関連付けられる。

切り離されたオブジェクトへのアクセスを提供する他のクラス変数が存在しない場合、そのオブジェクトは将来、ガベージコレクタによって破棄される。

auto variable1 = new MyClass();
auto variable2 = new MyClass();
variable1 = variable2;
D

上記の代入により、variable1は元のオブジェクトから離れ、variable2のオブジェクトへのアクセスを提供するようになる。variable1の元のオブジェクトを指す他の変数がないため、そのオブジェクトはガベージコレクタによって破棄される。

クラスの代入の動作は変更できない。つまり、クラスに対してopAssignをオーバーロードすることはできない。

定義

クラスは、structキーワードではなく、classキーワードで定義される:

class ChessPiece {
    // ...
}
D
コンストラクタ

構造体と同様に、コンストラクタの名前はthisである。構造体とは異なり、クラスオブジェクトは{ }構文で構築することはできない。

class ChessPiece {
    dchar shape;

    this(dchar shape) {
        this.shape = shape;
    }
}
D

構造体とは異なり、コンストラクタのパラメータがメンバーに順番に割り当てられるような自動オブジェクト構築はない。

class ChessPiece {
    dchar shape;
    size_t value;
}

void main() {
    auto king = new ChessPiece('♔', 100);  // ← コンパイルエラー
}
エラー: ChessPieceのコンストラクタがない

その構文を機能させるには、プログラマーが明示的にコンストラクタを定義する必要がある。

破壊

構造体と同様に、デストラクタの名前は~thisである。

~this() {
    // ...
}
D

ただし、構造体とは異なり、クラスのデストラクタは、クラスオブジェクトのライフタイムが終了した時点では実行されない。前述のように、デストラクタは、ガベージコレクションサイクル中に、将来のある時点で実行される。(この違いから、クラスのデストラクタは、より正確にはファイナライザと呼ぶべきだろう)。

メモリ管理の章で後で見るように、クラスのデストラクタは次のルールを遵守しなければならない:

これらの規則に違反すると、未定義の動作になる。このような問題の例は、クラスデストラクタでオブジェクトを割り当てようとした場合に見ることができる。

class C {
    ~this() {
        auto c = new C();    // ← 間違い: クラスデストラクタで
                             //          明示的に割り当てている
    }
}

void main() {
    auto c = new C();
}

プログラムは例外で終了する:

core.exception.InvalidMemoryOperationError@(0)

デストラクタで、ガベージコレクタから間接的に新しいメモリを割り当てることも同様に間違っている。例えば、動的配列の要素に使用されるメモリも、ガベージコレクタによって割り当てられる。要素のために新しいメモリブロックを割り当てる必要があるような方法で配列を使用することも、未定義の動作だ。

~this() {
    auto arr = [ 1 ];    // ← 間違い: クラスデストラクタで間接的に
                         //          割り当てている
}
D
core.exception.InvalidMemoryOperationError@(0)
メンバーアクセス

構造体と同様に、メンバーにはドット演算子でアクセスする。

auto king = new ChessPiece('♔');
writeln(king.shape);
D

構文上は変数のメンバーにアクセスしているように見えるが、実際にはオブジェクトのメンバーにアクセスしている。クラス変数にはメンバーはなく、クラスオブジェクトにメンバーがある。king変数にはshapeメンバーはなく、匿名オブジェクトに存在する。

注釈:通常、上記のコードのようにメンバーに直接アクセスすることは適切ではない。まったく同じ構文が必要な場合は、後の章で説明するプロパティを使用することをお勧めする。

演算子オーバーロード

opAssignはクラスに対してオーバーロードできないことを除けば、演算子のオーバーロードは構造体と同じだ。クラスでは、opAssignは常に、クラス変数をクラスオブジェクトに関連付けることを意味する。

メンバー関数

メンバー関数は構造体と同じ方法で定義および使用されるが、重要な違いが1つある。クラスメンバー関数は、オーバーライド可能であり、デフォルトではオーバーライド可能になっている。この概念については、後で継承の章で説明する。

オーバーライド可能なメンバー関数は実行時のパフォーマンスコストがかかるため、詳細には立ち入らないが、オーバーライドする必要のないすべてのclass関数は、finalキーワードで定義することをお勧めする。コンパイルエラーが発生しない限り、このガイドラインを無条件に適用することができる。

class C {
    final int func() {    // ← 推奨
        // ...
    }
}
D

構造体とのもう1つの違いは、一部のメンバー関数がObjectクラスから自動的に継承されることだ。次の章では、 overrideキーワードによってtoStringの定義を変更する方法について説明する。

isと演算子!is

これらの演算子はクラス変数に対して動作する。

is 2つのクラス変数が同じクラスオブジェクトへのアクセスを提供するかどうかを指定する。オブジェクトが同じ場合はtrueを返し、そうでない場合はfalseを返す。!isisの逆である。

auto myKing = new ChessPiece('♔');
auto yourKing = new ChessPiece('♔');
assert(myKing !is yourKing);
D

myKingyourKingの変数のオブジェクトは異なるため、!is演算子はtrueを返す。2つのオブジェクトは同じ文字'♔'で構築されているが、依然として2つの別個のオブジェクトである。

変数が同じオブジェクトへのアクセスを提供する場合は、istrueを返す:

auto myKing2 = myKing;
assert(myKing2 is myKing);
D

上記の2つの変数は、同じオブジェクトへのアクセスを提供する。

要約