クラス
構造体と同様に、classは新しい型を定義するための機能だ。この定義により、クラスはユーザー定義型となる。構造体とは異なり、クラスはD言語のオブジェクト指向プログラミング(OOP)パラダイムを提供する。OOPの主な特徴は次の通りだ。
- カプセル化:メンバーへのアクセス制御 (カプセル化は構造体でも利用可能だが、この章まで説明しなかった)
- 継承:別の型のメンバーを取得する
- 多態性:より一般的な型の代わりに、より特殊な型を使用できる
カプセル化は、後の章で説明する保護属性によって実現される。継承は、他の型の実装を取得するためのものだ。多態性は、プログラムの各部分を相互に抽象化するためのもので、クラスインターフェースによって実現される。
この章では、クラスを高いレベルで紹介し、クラスが参照型であることを強調する。クラスについては、後の章で詳しく説明する。
構造体との比較
一般的に、クラスは構造体とよく似ている。この章で構造体について説明した機能のほとんどは、クラスにも適用される。
ただし、クラスと構造体には重要な違いがある。
クラスは参照型
構造体との最大の違いは、構造体は値型であり、クラスは参照型であることだ。以下で説明するその他の違いは、ほとんどこの事実によるものだ。
クラス変数はnull
null値とis演算子の章で簡単に述べたように、クラス変数はnullになる。つまり、クラス変数はどのオブジェクトにもアクセスできない。クラス変数自体には値はない。実際のクラスオブジェクトは、newキーワードによって構築する必要がある。
また、nullへの参照を==または!=演算子で比較することはエラーになることも覚えておいてほしい。代わりに、比較はisまたは!is演算子を使用して行う必要がある:
その理由は、==演算子はオブジェクトのメンバーの値を参照する必要がある場合があり、null変数を通じてメンバーにアクセスしようとすると、メモリアクセスエラーが発生するためだ。そのため、クラス変数は常にisおよび!is演算子で比較する必要がある。
クラス変数とクラスオブジェクト
クラス変数とクラスオブジェクトは異なる概念だ。
クラスオブジェクトは、newキーワードによって構築され、名前はない。プログラム内でクラスタイプが実際に表す概念は、クラスオブジェクトによって提供される。例えば、Studentクラスが学生の名前と成績を表すと仮定すると、そのような情報はStudentオブジェクトのメンバーによって格納される。クラスオブジェクトは匿名であることもあって、直接アクセスすることはできない。
一方、クラス変数は、クラスオブジェクトにアクセスするための言語機能だ。文法的にはクラス変数に対して操作が行われているように見えるが、実際には操作はクラスオブジェクトにディスパッチされる。
値型と参照型の章で見た次のコードを考えてみよう。
newキーワードは匿名クラスオブジェクトを構築する。variable1とvariable2は、単にその匿名オブジェクトへのアクセスを提供するだけだ:
(anonymous MyClass object) variable1 variable2
───┬───────────────────┬─── ───┬───┬─── ───┬───┬───
│ ... │ │ o │ │ o │
───┴───────────────────┴─── ───┴─│─┴─── ───┴─│─┴───
▲ │ │
│ │ │
└────────────────────┴────────────┘
コピー
コピーは変数にのみ影響し、オブジェクトには影響しない。
クラスは参照型であるため、新しいクラス変数を別のクラスのコピーとして定義すると、同じオブジェクトへのアクセスを提供する2つの変数が作成される。実際のオブジェクトはコピーされない。
postblit関数this(this)は、クラスでは使用できない。
上記のコードでは、variable2はvariable1によって初期化されている。2つの変数は同じオブジェクトへのアクセスを開始する。
実際のオブジェクトをコピーする必要がある場合は、その目的のためのメンバー関数をクラスに用意する必要がある。配列との互換性を保つため、この関数の名前はdup()とする必要がある。この関数は、新しいクラスオブジェクトを作成して返す必要がある。さまざまな型のメンバーを持つクラスでこれを確認しよう。
dup()メンバー関数は、Fooのコンストラクタを利用して新しいオブジェクトを作成し、その新しいオブジェクトを返す。コンストラクタは、配列の.dupプロパティによって、sメンバーを明示的にコピーすることに注意。値型であるoおよびiは自動的にコピーされる。
次のコードは、dup()を使用して新しいオブジェクトを作成する。
その結果、var1とvar2に関連付けられたオブジェクトは異なるものになる。
同様に、immutableのコピーは、idup()という適切な名前のメンバー関数によって提供することができる。この場合、コンストラクタもpureとして定義する必要がある。pureキーワードについては、後の章で説明する。
代入
コピーと同様に、代入は変数にのみ影響する。
クラス変数に代入すると、その変数は現在のオブジェクトから切り離され、新しいオブジェクトと関連付けられる。
切り離されたオブジェクトへのアクセスを提供する他のクラス変数が存在しない場合、そのオブジェクトは将来、ガベージコレクタによって破棄される。
上記の代入により、variable1は元のオブジェクトから離れ、variable2のオブジェクトへのアクセスを提供するようになる。variable1の元のオブジェクトを指す他の変数がないため、そのオブジェクトはガベージコレクタによって破棄される。
クラスの代入の動作は変更できない。つまり、クラスに対してopAssignをオーバーロードすることはできない。
定義
クラスは、structキーワードではなく、classキーワードで定義される:
コンストラクタ
構造体と同様に、コンストラクタの名前はthisである。構造体とは異なり、クラスオブジェクトは{ }構文で構築することはできない。
構造体とは異なり、コンストラクタのパラメータがメンバーに順番に割り当てられるような自動オブジェクト構築はない。
エラー: ChessPieceのコンストラクタがない
その構文を機能させるには、プログラマーが明示的にコンストラクタを定義する必要がある。
破壊
構造体と同様に、デストラクタの名前は~thisである。
ただし、構造体とは異なり、クラスのデストラクタは、クラスオブジェクトのライフタイムが終了した時点では実行されない。前述のように、デストラクタは、ガベージコレクションサイクル中に、将来のある時点で実行される。(この違いから、クラスのデストラクタは、より正確にはファイナライザと呼ぶべきだろう)。
メモリ管理の章で後で見るように、クラスのデストラクタは次のルールを遵守しなければならない:
- クラスデストラクタは、ガベージコレクタによって管理されているメンバーにアクセスしてはならない。これは、ガベージコレクタがオブジェクトとそのメンバーが特定の順序でファイナル化されることを保証する必要がないためだ。デストラクタが実行される時点で、すべてのメンバーが既にファイナル化されている可能性がある。
- クラスデストラクタは、ガベージコレクタによって管理される新しいメモリを割り当ててはならない。これは、ガベージコレクタは、ガベージコレクションサイクル中に新しいオブジェクトを割り当てられることを保証する必要がないためだ。
これらの規則に違反すると、未定義の動作になる。このような問題の例は、クラスデストラクタでオブジェクトを割り当てようとした場合に見ることができる。
プログラムは例外で終了する:
core.exception.InvalidMemoryOperationError@(0)
デストラクタで、ガベージコレクタから間接的に新しいメモリを割り当てることも同様に間違っている。例えば、動的配列の要素に使用されるメモリも、ガベージコレクタによって割り当てられる。要素のために新しいメモリブロックを割り当てる必要があるような方法で配列を使用することも、未定義の動作だ。
core.exception.InvalidMemoryOperationError@(0)
メンバーアクセス
構造体と同様に、メンバーにはドット演算子でアクセスする。
構文上は変数のメンバーにアクセスしているように見えるが、実際にはオブジェクトのメンバーにアクセスしている。クラス変数にはメンバーはなく、クラスオブジェクトにメンバーがある。king変数にはshapeメンバーはなく、匿名オブジェクトに存在する。
注釈:通常、上記のコードのようにメンバーに直接アクセスすることは適切ではない。まったく同じ構文が必要な場合は、後の章で説明するプロパティを使用することをお勧めする。
演算子オーバーロード
opAssignはクラスに対してオーバーロードできないことを除けば、演算子のオーバーロードは構造体と同じだ。クラスでは、opAssignは常に、クラス変数をクラスオブジェクトに関連付けることを意味する。
メンバー関数
メンバー関数は構造体と同じ方法で定義および使用されるが、重要な違いが1つある。クラスメンバー関数は、オーバーライド可能であり、デフォルトではオーバーライド可能になっている。この概念については、後で継承の章で説明する。
オーバーライド可能なメンバー関数は実行時のパフォーマンスコストがかかるため、詳細には立ち入らないが、オーバーライドする必要のないすべてのclass関数は、finalキーワードで定義することをお勧めする。コンパイルエラーが発生しない限り、このガイドラインを無条件に適用することができる。
構造体とのもう1つの違いは、一部のメンバー関数がObjectクラスから自動的に継承されることだ。次の章では、 overrideキーワードによってtoStringの定義を変更する方法について説明する。
isと演算子!is
これらの演算子はクラス変数に対して動作する。
is 2つのクラス変数が同じクラスオブジェクトへのアクセスを提供するかどうかを指定する。オブジェクトが同じ場合はtrueを返し、そうでない場合はfalseを返す。!isはisの逆である。
myKingとyourKingの変数のオブジェクトは異なるため、!is演算子はtrueを返す。2つのオブジェクトは同じ文字'♔'で構築されているが、依然として2つの別個のオブジェクトである。
変数が同じオブジェクトへのアクセスを提供する場合は、isはtrueを返す:
上記の2つの変数は、同じオブジェクトへのアクセスを提供する。
要約
- クラスと構造体は共通の特徴も持っているが、大きな違いもある。
- クラスは参照型だ。
newキーワードは、匿名クラスオブジェクトを構築し、クラス変数を返す。 - オブジェクトと関連付けられていないクラス変数は、
nullとなる。nullとの比較は、==や!=ではなく、isや!isで行う必要がある。 - コピーすると、オブジェクトに追加の変数が関連付けられる。クラスオブジェクトをコピーするには、その型に
dup()という名前の特別な関数がある必要がある。 - 代入は、変数とオブジェクトを関連付ける。この動作は変更できない。