クラス
D のオブジェクト指向的な機能は、 全てクラスで実現されています。 クラス階層のルートにはObjectクラスがあります。Objectは、 それぞれの派生クラスが最低限持つ機能と、そのデフォルト実装を定義しています。
クラスは、プログラマ定義の型です。 クラスのサポートが、 カプセル化、継承、多態など、D をオブジェクト指向言語たらしめています。 D のクラスは、単一継承と、それに加えてインターフェイス継承が可能です。 クラスオブジェクトは参照によってのみインスタンス化されます。
クラスはexportできます。 つまり、DLLやEXEから外部に、 クラス名やその非privateメンバを公開することができます。
クラス宣言の定義:
ClassDeclaration:
class Identifier BaseClassListopt ClassBody
ClassTemplateDeclaration
BaseClassList:
: SuperClass
: SuperClass , Interfaces
: Interfaces
SuperClass:
Identifier
Interfaces:
Interface
Interface , Interfaces
Interface:
Identifier
ClassBody:
{ }
{ ClassBodyDeclarations }
ClassBodyDeclarations:
ClassBodyDeclaration
ClassBodyDeclaration ClassBodyDeclarations
ClassBodyDeclaration:
DeclDef
Invariant
ClassAllocator
ClassDeallocator
クラスは、以下の構成要素からなります:
- 基底クラス
- インターフェイス
- 動的フィールド
- 静的フィールド
- 型
- メンバ関数
- static member functions
- 仮想関数
- 同期関数
- コンストラクタ
- デストラクタ
- 静的コンストラクタ
- 静的デストラクタ
- shared静的コンストラクタ
- shared静的デストラクタ
- 不変条件
- 単体テスト
- クラスアロケータ
- クラスデアロケータ
- alias this
class Foo {
... メンバ ...
}
クラス定義の閉じ括弧 } の後ろに ;
が無いことにご注意下さい。
また、変数を次のように宣言することもできません:
class Foo { } var;
代わりにこう書きます:
class Foo { }
Foo var;
アクセス制御
クラスメンバへのアクセスは ProtectionAttribute によって制御します。 デフォルトの保護属性は public です。 アクセス制御は可視性には影響しません。
フィールド
クラスのメンバには常に . 演算子でアクセスします。
基底クラスのメンバは、 メンバ名の前に基底クラス名とドットを書くと参照できます:
class A { int a; }
class B : A { int a; }
void foo(B b) {
b.a = 3; // B.a にアクセス
b.A.a = 4; // A.a にアクセス
D コンパイラは、 クラスのフィールドの順序を自由に並び替えて、 実装定義の方法で最適なメモリ配置とすることができます。 フィールドは、 関数におけるローカル変数のようなものと考えて下さい。 ローカル変数は、 最適なフレームレイアウトのために、 コンパイラが自由に並び替えてスタックやレジスタを割り当てます。 これによってコードの設計者は、 マシンに最適な順に従うのではなく、 読みやすさを重視してフィールドを並べることができます。 明示的なメモリレイアウトの制御は、 クラスではなく構造体/共用体で行います。
フィールドのプロパティ
.offsetofプロパティは、フィールドの、 クラスの実体の先頭から数えたバイト数を返します。 .offsetof はフィールドそのものを表す式に対してのみ適用できます。 クラスの型で修飾されたフィールドには使えません:
class Foo {
int x;
}
...
void test(Foo foo) {
size_t o;
o = Foo.x.offsetof; // エラー。Foo.x には 'this' が必要
o = foo.x.offsetof; // ok
}
クラスのプロパティ
.tupleof プロパティは、 隠しフィールドおよび基底クラスのフィールドを除く、 全てのフィールドからなる ExpressionTuple を返します。
class Foo { int x; long y; }
void test(Foo foo) {
foo.tupleof[0] = 1; // foo.x を 1 に
foo.tupleof[1] = 2; // foo.y を 1 に
foreach (x; foo.tupleof)
writef(x); // 12 と表示
}
プロパティ .__vptr と .__monitor は、それぞれ、 クラスオブジェクトの vtbl[] とモニタを指しています。 ただし、これらはユーザーコードで使用すべきではありません。
基底クラス
全てのクラスは、1つの基底クラスを継承します。特に指定がなければ Object クラスからの継承になります。Object は、 Dのクラス継承階層のルートです。メンバ関数
staticでないメンバ関数は、 隠し引数 this をもち、 この引数を通して他のクラスメンバにアクセスできます。
staticでないメンバ関数には、通常の FunctionAttribute に加えて、 const, immutable, shared, inout という属性を付与できます。 これらは隠し引数 this に対して適用されます。
class C {
int a;
const void foo() {
a = 3; // エラー。 'this' は const
}
void foo() immutable {
a = 3; // エラー。 'this' は immutable
}
同期関数
同期クラスメンバ関数は、記憶域クラス synchronized で宣言されます。 静的メンバ関数の場合、そのクラスの classinfo オブジェクトによって同期が行われます。つまり、 そのクラスの全ての静的メンバ関数はすべておなじ一つのモニタを使用します。 非静的メンバ関数の場合、 使用されるモニタはオブジェクトごとに用意されます。例えば:
class Foo {
synchronized void bar() { ...statements... }
}
は、(モニタに関しては)以下と同等です:
class Foo {
void bar() {
synchronized (this) { ...statements... }
}
}
構造体には同期メンバ関数はありません。
コンストラクタ
Constructor:
this Parameters FunctionBody
TemplatedConstructor
メンバは、 常にその型の デフォルト初期化子 で初期化されます。 整数型なら 0、浮動小数点数型なら NaN、 といった具合です。 これによって、 あるコンストラクタでメンバの初期化を忘れたために 隠れた問題が生じる、という事態を避けることができます。 クラスの定義では、 デフォルトを置き換える静的初期化子を指定できます:
class Abc {
int a; // デフォルトの初期化子は 0
long b = 7; // デフォルトの初期化子は 7
float f; // デフォルトの初期化子は NaN
}
静的な初期化は、
コンストラクタ呼び出しより前に実行されます。
コンストラクタは返値を持たない、this という名前の関数です:
class Foo {
this(int x) // Foo のコンストラクタを宣言
{ ...
}
this()
{ ...
}
}
基底クラスの構築は、基底のコンストラクタを
super という名前で呼び出して行います:
class A { this(int y) { } }
class B : A {
int j;
this() {
...
super(3); // 基底クラスのコンストラクタ A.this(3) を呼び出し
...
}
}
コンストラクタは、同じクラスの他のコンストラクタを呼ぶことで 初期化処理を共有できます。 (移譲コンストラクタ (delegating constructor) と呼びます):
class C {
int j;
this() {
...
}
this(int i) {
this();
j = i;
}
}
もし this や superによる他のコンストラクタの呼び出しが
コンストラクタ中に存在せず、基底クラスがコンストラクタを持っていれば、
コンストラクタの先頭に super() の呼び出しが自動で挿入されます。
クラスにコンストラクタが定義されていないが、基底クラスにはある、 という場合は、次の形式のデフォルトコンストラクタ:
this() { }
が暗黙のうちに生成されます。
クラスオブジェクトの構築は非常に柔軟性がありますが、 いくつかの制限も加わります:
- お互いを呼び出し合うコンストラクタは不正です。
ただし、コンパイラがこれを検出することは要求されません。
相互呼び出しは未定義動作となります。
this() { this(1); } this(int i) { this(); } // 不正, コンストラクタの循環呼び出し
- コンストラクタ内に別のコンストラクタ呼び出しがあるときは、
どの実行パスを通ってもちょうど1つだけコンストラクタが呼ばれるように
なっていなければなりません:
this() { a || super(); } // 不正 this() { (a) ? this(1) : super(); } // ok this() { for (...) { super(); // 不正, ループの内側 } }
- コンストラクタ呼び出しより前に、暗黙にも明示的にも this を参照することはできません。
- コンストラクタ呼び出しはラベルの後に置くことはできません。 (gotoが存在するときに、上に書いた条件のチェックを簡単にするためです。)
クラスオブジェクトのインスタンスは、NewExpression で生成します。
A a = new A(3);
以下のステップが実行されます:
- オブジェクト用のメモリ領域を割り当てます。 失敗すると、null を返すのではなく OutOfMemoryError 例外が投げられます。 従って、面倒なnullチェックは不要です。
- クラス定義で与えられた値で、 データを静的に初期化します。 ここでvtbl[] (仮想関数へのポインタのテーブル)へのポインタが設定されます。 これによって、 コンストラクタの時点で、 完全に構築されて仮想関数も呼び出せるオブジェクトが渡されることを保証します。 これは静的な初期オブジェクトを新しいメモリ領域へ memcpy する操作と同等ですので、 高性能なコンパイラはそのような最適化を行うかもしれません。
- もしクラスにコンストラクタが定義されていれば、 引数リストに合うコンストラクタが呼び出されます。
- クラス不変条件チェックがONなら、 コンストラクタの後に invariant() が呼び出されます。
デストラクタ
Destructor:
~ this ( ) FunctionBody
オブジェクトが削除される際に、
ガベージコレクタがデストラクタ関数を呼び出します。
構文は:
class Foo {
~this() // Foo のデストラクタ
{
}
}
一つのクラスにつき高々一つのデストラクタしか存在できません。 デストラクタは引数を取らず、属性を何も持ちません。 常に仮想関数です。
デストラクタは、 オブジェクトの保持するリソースを解放する役割を担います。
プログラムは、オブジェクトがもはや参照されないことを(delete式によって) ガベージコレクタへ明示的に伝えることもできます。 この場合はGCはすぐに デストラクタを呼び出し、オブジェクトのメモリを自由領域へ戻します。 デストラクタが2回以上呼ばれないことは保証されています。
デストラクタの終了後には、 自動的に基底クラスのデストラクタが呼び出されます。 基底クラスのデストラクタを明示的に呼ぶ方法は存在しません。
GC が参照されなくなったオブジェクトを必ず解放するという保証はありません。 さらに、 GC がデストラクタを呼び出す順番も保証されません。 これはつまり、 GC管理下にあるオブジェクトへの参照をメンバとして持つオブジェクトのデストラクタを ガベージコレクタが呼び出すときには、それらの参照は既に無効となっている可能性があるということです。 従って、 デストラクタからはメンバオブジェクトを参照できません。 この規則は、 auto オブジェクトや DeleteExpression で削除されるオブジェクト (GCからのデストラクタ呼び出しではないので、参照は必ず有効)には当てはまりません。
データセグメントから参照されているオブジェクトは、 決してGCに回収されることはありません。
静的コンストラクタ
StaticConstructor:
static this ( ) FunctionBody
静的コンストラクタは、 メインスレッドで main() 関数に制御が移る前と、各スレッドの起動時に、 スレッドローカル記憶域の初期化作業を行うための関数です。 静的コンストラクタは、 クラスの静的メンバを、 コンパイル時には計算できない値で初期化する際に使用します。
他の言語では、 コンパイル時に計算できない値をメンバの初期化子として使うことで、 暗黙の内に組み込まれます。この方法に関する問題は、 いつコードが実行されるのか正確な制御ができないことです。 例えば:
class Foo {
static int a = b + 1;
static int b = a * 2;
}
aとbは結局どの値になるのか?
初期化が実行される順序は?
初期化が始まる前のaとbの値は何なのか、
これはコンパイルエラーになるのか、
それとも
実行時エラーになるのか?
様々な混乱は、初期化子が静的なのか動的なのか明確でないことが原因となっています。
DD はこれを単純にします。 全てのメンバ初期化は、 コンパイラによってコンパイル時に決定されなくてはなりません。 従って、 メンバの初期化には評価順序の依存関係はなく、 初期化されていない値を読みとることはできなくなっています。 動的な初期化は、特別な構文 static this() で定義される 静的コンストラクタによって行われます。
class Foo {
static int a; // デフォルトの 0 に初期化される
static int b = 1;
static int c = b + a; // エラー, 初期化子が定数でない
static this() // 静的コンストラクタ
{
a = b + 1; // a を 2 に設定
b = a * 2; // b を 4 に設定
}
}
If main() やスレッドが例外を投げず正常終了した場合、 静的デストラクタが、 スレッド終了時に実行される関数のリストに追加されます。 静的コンストラクタは空の引数リストを持ちます。
同一モジュール内の複数の静的コンストラクタは、 ソース中に登場する順に上から実行されます。 他のモジュールからimportされているモジュールの静的コンストラクタは、 全て、 importする側の静的コンストラクタよりも前に実行されます。
静的コンストラクタ宣言の static は、属性ではありません。 必ず this の直前に書く必要があります:
class Foo {
static this() { ... } // 静的コンストラクタ
static private this() { ... } // 静的コンストラクタではない
static {
this() { ... } // 静的コンストラクタではない
}
static:
this() { ... } // 静的コンストラクタではない
}
静的デストラクタ
StaticDestructor:
static ~ this ( ) FunctionBody
静的デストラクタは、static ~this()
という構文で定義される特別な関数です。
class Foo {
static ~this() // 静的デストラクタ
{
}
}
静的デストラクタは、 対応する静的コンストラクタが正常に実行完了しているときのみ、 スレッドの終了時に呼び出されます。 静的デストラクタは空の引数リストを持ちます。 静的デストラクタは、 静的コンストラクタの呼び出しと逆順で実行されます。
静的デストラクタの宣言の static は、属性ではありません。 必ず ~this の直前に書く必要があります:
class Foo {
static ~this() { ... } // 静的デストラクタ
static private ~this() { ... } // 静的デストラクタではない
static
{
~this() { ... } // 静的デストラクタではない
}
static:
~this() { ... } // 静的デストラクタではない
}
shared 静的コンストラクタ
SharedStaticConstructor:
shared static this ( ) FunctionBody
shared 静的コンストラクタは、全ての StaticConstructor の前に実行されます。sharedなグローバルデータの初期化のための使用が意図されています。
shared 静的デストラクタ
SharedStaticDestructor:
shared static ~ this ( ) FunctionBody
shared 静的デストラクタはプログラムの終了時に、 SharedStaticConstructor の逆の順番で実行されます。
クラス不変条件
Invariant:
invariant ( ) BlockStatement
クラス不変条件は、
(メンバ関数の実行途中以外は)常に真でなければならない
クラスの性質を記述するために使われます。例えば、
日時を表現するクラスは、"日"が1..31で"時"が0..23である、という不変条件を持ちます:
class Date {
int day;
int hour;
invariant() {
assert(1 <= day && day <= 31);
assert(0 <= hour && hour < 24);
}
}
クラス不変条件は、 assertがtrueでなければならない、という契約です。 不変条件は、コンストラクタ完了直後、 デストラクタ実行直前に実行されます。 そして、public/export メンバ関数の実行前後の順番は以下の通りです。
- 事前条件
- 不変条件
- 本体
- 不変条件
- 事後条件
invariant内では、直接間接を問わず、 そのクラスのstaticでないpublicメンバを 呼び出すことができません。 そのような呼び出し方をすると 再帰的に invariant のチェックが入ってしまい、スタックオーバーフローが発生します。
invariant は暗黙に const 扱いになります。
invariant は public や export メンバの開始時に呼び出されるため、 そのようなメンバ関数は コンストラクタからは呼び出すべきではありません。
class Foo {
public void f() { }
private void g() { }
invariant() {
f(); // エラー。invariantの中でpublicメンバ関数は呼べない
g(); // OK。 g() はpublicではない。
}
}
assert()
式の引数にクラスオブジェクトを渡したときも、
不変条件がチェックできます:
Date mydate;
...
assert(mydate); // Dateクラスの不変条件が成立していることのチェック
不変条件が破れていた場合、
AssertError 例外が投げられます。
不変条件は継承されます。
つまり、全てのクラスの不変条件には、
暗黙の内に基底クラスの不変条件が追加されます。
クラス1つにつき、invariant は最大一つしか定義できません。
リリース用にコンパイルする際には不変条件のコードは生成されず、 プログラムは最高速で動作します。
クラス・アロケータ
注: クラスアロケータは D2 では非推奨です。ClassAllocator:
new Parameters FunctionBody
次の形をしたメンバ関数:
new(uint size) {
...
}
は、クラス・アロケータと呼ばれます。
クラスアロケータは、第一引数の型がuintでさえあれば、
任意個数の引数を取ることができます。
1つのクラスに幾つでも定義することができ、
通常の関数オーバーロードの規則に従って正しいものが呼び出されます。
new式:
new Foo;
が実行されて、
Fooがアロケータを持つクラスであれば、
インスタンスがメモリ上で占めるサイズを第一引数として、
アロケータが呼び出されます。
アロケータはメモリを割り当て、
void*.
で返さなくてはなりません。
もしアロケータが失敗したら、 null を返すのではなく例外を投げるようします。
アロケータが2個以上の引数を取るのであれば、
NewExpression の
new の後に、括弧で追加の引数を指定できます:
class Foo {
this(char[] a) { ... }
new(uint size, int x, int y) {
...
}
}
...
new(1,2) Foo(a); // new(Foo.sizeof,1,2)の呼び出し
派生クラスは、 特に指定が無ければ親クラスからアロケータを継承します。
インスタンスがスタック上に割り当てられるときには、 アロケータは呼ばれません。
クラス・デアロケータ
注: クラス・デアロケータと delete 演算子はD2では非推奨です。 デストラクタを呼び出してオブジェクトを終了させるには clear 関数を使います。 そのオブジェクトのメモリはすぐには解放されません。代わりに、GC がその後の任意の時点でメモリを回収します:class Foo { int x; this() { x = 1; } }
Foo foo = new Foo;
clear(foo);
assert(foo.x == int.init); // まだアクセス可能
ClassDeallocator:
delete Parameters FunctionBody
次の形をしたメンバ関数:
delete(void *p) {
...
}
は、クラス・デアロケータと呼ばれます。
デアロケータは void* 型の引数を唯一つ取ります。
1クラスにつき1つだけ指定できます。
delete式:
delete f;
実行されて、 fがデアロケータを持つクラスのインスタンスへの参照であれば、 (存在すれば)デストラクタが呼ばれた後、 インスタンスへのポインタを引数に、 デアロケータが呼ばれます。メモリを解放するのは、デアロケータの責任です。
派生クラスは、 特に指定が無ければ親クラスからデアロケータを継承します。
インスタンスがスタック上に割り当てられているときには、 アロケータは呼ばれません。
alias this
AliasThis:
alias Identifier this;
AliasThis 宣言は、 未定義のメンバ探索を転送する先のメンバを宣言します。Identifier で指定されたメンバが転送先になります。
クラスや構造体はAliasThis されたメンバに暗黙変換されます。
struct S {
int x;
alias x this;
}
int foo(int i) { return i * 2; }
void test() {
S s;
s.x = 7;
int i = -s; // i == -7
i = s + 8; // i == 15
i = s + s; // i == 14
i = 9 + s; // i == 16
i = foo(s); // int への暗黙変換
}
転送先がクラスや構造体であった場合、 見つからなかったメンバアクセスは AliasThis に転送されます
struct Foo
{
int baz = 4;
int get() { return 7; }
}
class Bar
{
Foo foo;
alias foo this;
}
void test() {
auto bar = new Bar;
int i = bar.baz; // i == 4
i = bar.get(); // i == 7
}
Identifier が引数なしのプロパティメンバ関数を指していた場合、 転送はその関数の返値に向けて行われます。
struct S
{
int x;
@property int get() {
return x * 2;
}
alias get this;
}
void test() {
S s;
s.x = 2;
int i = s; // i == 4
}
複数の AliasThis を書くことが出来ます。 暗黙変換と転送先を探す際には、すべての AliasThis を試します。複数の AliasThis 候補がマッチした場合は、 あいまいなためエラーとなります。 注: 複数の AliasThis は現在のところ未実装です。
scope クラス
次のように scope 属性の付いたクラスが、scopeクラスです:scope class Foo { ... }
scope性は継承され、
scopeクラスから派生したクラスは皆scopeになります。
scopeクラスの参照は、関数のローカル変数としてのみ存在できます。 その変数は scope と宣言されていなくてはいけません:
scope class Foo { ... }
void func() {
Foo f; // エラー、scopeクラスの参照はscopeでなくてはダメ
scope Foo g = new Foo(); // 正しい
}
scopeクラスの参照がスコープから抜けると、(もし存在すれば)
デストラクタが自動的に呼ばれます。これは、
例外によってスコープを抜けた時であっても成り立ちます。
final クラス
final と宣言されたクラスは継承できません:
final class A { }
class B : A { } // エラー。クラス A は final
ネストクラス
A ネストクラス とは、 関数や他のクラスのスコープ内で定義されるクラスのことをいいます。 ネストクラスからは、すぐ外側のクラスと関数内で定義された変数や その他のシンボルにアクセスすることができます:class Outer {
int m;
class Inner {
int foo() {
return m; // Outerのメンバへアクセスできる
}
}
}
void func() {
int m;
class Inner {
int foo() {
return m; // func() のローカル変数にアクセスできる
}
}
}
static 属性付きで宣言されていた場合、
スタックローカルの変数や this:
の必要なシンボルなどの周囲のスコープにはアクセスできなくなります:
class Outer {
int m;
static int n;
static class Inner {
int foo() {
return m; // エラー。Innerはstaticでmには this
return n; // OK。n は static
}
}
}
void func() {
int m;
static int n;
static class Inner {
int foo() {
return m; // エラー。Innerはstaticでmはスタックローカル
return n; // OK。n は static
}
}
}
非staticネストクラスは、隠しメンバ(コンテクスト・ポインタと呼びます)
を一つ加えることで実現されています。
関数内にあるときは周囲の関数のフレームポインタが、
クラス内にあるときは周囲のクラスの this
ポインタが、隠しメンバに格納されています。
非staticネストクラスがインスタンス化される際には、 コンストラクタの呼び出し前にコンテキスト・ポインタが設定されるので、 コンストラクタからは周辺の変数へと完全にアクセス可能です。 非staticネストクラスは、 必要なコンテキスト・ポインタ情報が入手可能な時のみインスタンス化できます:
class Outer {
class Inner { }
static class SInner { }
}
void func() {
class Nested { }
Outer o = new Outer; // Ok
Outer.Inner oi = new Outer.Inner; // エラー。Outerに'this'がない
Outer.SInner os = new Outer.SInner; // Ok
Nested n = new Nested; // Ok
}
NewExpression の頭にくっつけることで、 内部クラスの生成時に this を指定することができます:
class Outer {
int a;
class Inner {
int foo() {
return a;
}
}
}
int bar() {
Outer o = new Outer;
o.a = 3;
Outer.Inner oi = o.new Inner;
return oi.foo(); // 3を返す
}
この例では o が外部クラス Outer のインスタンスを指す this として機能しています。
内部クラスのインスタンスのプロパティ .outer は、 外部クラスの this ポインタを返します。外部コンテキストがクラスでない場合 .outer は void* 型でコンテキストへのポインタを返します。
class Outer {
class Inner {
Outer foo() {
return this.outer;
}
}
void bar() {
Inner i = new Inner;
assert(this == i.foo());
}
}
void test() {
Outer o = new Outer;
o.bar();
}
無名ネストクラス
無名ネストクラスは、 NewAnonClassExpression で定義とインスタンス化が同時に行われます:
NewAnonClassExpression:
new AllocatorArgumentsopt class ClassArgumentsopt SuperClassopt Interfacesopt ClassBody
ClassArguments:
( ArgumentListopt )
これは次と同等です:
class Identifier : SuperClass Interfaces
ClassBody
new (ArgumentList) Identifier (ArgumentList);
Identifier は無名ネストクラス用に生成されたクラス名とします。
const/immutable/shared クラス
ClassDeclaration が記憶域クラス const か immutable か shared で修飾されていた場合、 そのクラスのすべてのメンバが同じ記憶域クラスで修飾されます。 基底クラスがconst/immutable/sharedなら、 その派生クラスも全てconst/immutable/sharedになります。