構造体およびクラスの契約プログラミング
契約プログラミングは、コーディングのエラーを減らすのに非常に効果的だ。契約プログラミングの章で、契約プログラミングの2つの機能について紹介した。in
ブロックとout
ブロックは、関数の入力および出力の契約を保証する。
注釈:この章の"in
ブロックとenforce
チェック"のセクションにあるガイドラインを必ず確認。この章の例は、オブジェクトとパラメータの一貫性の問題はプログラマのエラーによるものという前提に基づいている。そうでない場合は、関数本体内でenforce
チェックを使用する必要がある。
念のため、ヘロンの公式を使って三角形の面積を計算する関数を書いてみよう。この関数のin
とout
ブロックは、まもなく構造体のコンストラクタに移動する。
この計算が正しく機能するためには、三角形の各辺の長さがゼロより大きい必要がある。さらに、三角形において、1つの辺が他の2つの辺の和より大きいことは不可能であるため、その条件もチェックする必要がある。
これらの入力条件が満たされると、三角形の面積は0より大きくなる。次の関数は、これらの要件がすべて満たされていることを確認する。
メンバー関数の前提条件と後条件
in
およびout
ブロックは、メンバー関数でも使用できる。
上記の関数を、Triangle
構造体のメンバー関数に変換しよう。
三角形の辺はメンバー変数になったため、この関数はパラメータを受け取らなった。そのため、この関数にはin
ブロックはない。その代わりに、メンバーはすでに一貫した値を持っていると仮定している。
オブジェクトの一貫性は、以下の機能によって保証される。
オブジェクトの一貫性に関する事前条件と事後条件
上記のメンバー関数は、オブジェクトのメンバーがすでに一貫した値を持っていることを前提として記述されている。この前提を保証する1つの方法は、オブジェクトが必ず一貫した状態で起動するように、コンストラクタにin
ブロックを定義することだ。
これにより、実行時に無効なTriangle
オブジェクトが作成されるのを防ぐことができる:
コンストラクタのin
ブロックは、このような無効なオブジェクトの生成を防止する:
core.exception.AssertError@deneme.d: Assertionの失敗
上記のコンストラクタにはout
ブロックが定義されていないが、コンストラクタ直後にメンバーの一貫性を確保するために定義することは可能である。
invariant()
オブジェクトの一貫性を保つためのブロック
コンストラクタのin
およびout
ブロックは、オブジェクトが一致した状態でその寿命を開始することを保証し、メンバー関数のin
およびout
ブロックは、それらの関数自体が正しく動作することを保証する。
ただし、これらのチェックは、オブジェクトが常に一貫した状態にあることを保証するには適していない。すべてのメンバ関数でout
ブロックを繰り返すことは、過剰であり、エラーの原因となる。
オブジェクトの一貫性および有効性を定義する条件は、そのオブジェクトの不変条件と呼ばれる。例えば、顧客クラスの注文と請求書が1対1で対応している場合、そのクラスの不変条件は、注文配列と請求書配列の長さが等しいことだ。この条件がどのオブジェクトにも満たされない場合、そのオブジェクトは一貫性のない状態になる。
不変条件の例として、カプセル化と保護の属性の章で説明したSchool
クラスを考えてみよう。
このクラスのオブジェクトは、その3つのメンバーを含む不変条件が満たされている場合のみ一貫している。student配列の長さは、女性学生と男性学生の合計と等しくなければなりません:
この条件が偽になる場合、このクラスの実装にバグがある。
invariant()
ブロックは、ユーザー定義型の不変条件を保証するためのものだ。invariant()
ブロックは、struct
またはclass
の本体内で定義される。これらのブロックには、in
およびout
ブロックと同様のassert
チェックが含まれる。
必要に応じて、ユーザー定義型に複数のinvariant()
ブロックを含めることができる。
invariant()
ブロックは、以下のタイミングで自動的に実行される:
- コンストラクタの実行後: これにより、すべてのオブジェクトが一致した状態でライフサイクルを開始することが保証される。
- デストラクタの実行前: これにより、デストラクタが一致したオブジェクトに対して実行されることが保証される。
public
メンバー関数の実行前と実行後:これにより、メンバー関数がオブジェクトの一貫性を無効にしないことが保証される。注釈:この点では、
export
関数はpublic
関数と同じだ。(簡単に言えば、export
関数は、ダイナミックライブラリインターフェイスでエクスポートされる関数だ。)
invariant()
ブロック内のassert
チェックが失敗した場合、AssertError
がスローされる。これにより、プログラムが不正なオブジェクトで実行を継続することがなくなる。
in
およびout
ブロックと同様、invariant()
ブロック内のチェックは、コマンドラインオプション-release
によって無効にすることができる。
契約継承
インターフェースおよびクラスメンバー関数にも、in
およびout
ブロックを指定することができる。これにより、interface
またはclass
で、その派生型が依存する前提条件を定義したり、そのユーザーが依存する後条件を定義したりすることができる。派生型では、それらのメンバー関数のオーバーライド用に、さらにin
およびout
ブロックを定義することができる。オーバーライドされたin
ブロックは前提条件を緩和し、オーバーライドされたout
ブロックはより強力な保証を提供することができる。
ユーザーコードは通常、派生型から抽象化され、階層の最上位の型の前提条件を満たすように記述される。ユーザーコードは、派生型についてまったく知らない。ユーザーコードはインターフェースの契約のために記述されるため、派生型がオーバーライドされたメンバー関数に、より厳しい前提条件を設定することは許されない。ただし、派生型の前提条件は、そのスーパークラスの前提条件よりも寛容にすることができる。
関数に入ると、in
ブロックは、階層の最上位型から最下位型に向かって自動的に実行される。in
ブロックが、assert
の失敗なく成功した場合、前提条件は満たされているとみなされる。
同様に、派生型もout
ブロックを定義することができる。後条件は関数が提供する保証に関するものであるため、派生型のメンバー関数は、その祖先の後条件も遵守しなければならない。一方、追加の保証を提供することもできる。
関数から抜け出すと、out
ブロックは、最上位の型から最下位の型に向かって自動的に実行される。out
ブロックがすべて成功した場合にのみ、その関数は後条件を満たしたと見なされる。
以下の人工的なプログラムは、interface
とclass
でこれらの機能を説明している。class
は呼び出し元に対してより少ない要件を課しながら、より多くの保証を提供する:
Class
のin
ブロックは、Iface
の事前条件を満たさないため、実行されない:
Iface.func.in
Class.func.in ← が成功した場合、実行されない
Class.func.do
Iface.func.out
Class.func.out
[42, 1, 2, 2, 3, 42]
要約
in
out
ブロックはコンストラクタでも有用だ。これらは、オブジェクトが有効な状態で構築されることを保証する。invariant()
ブロックは、オブジェクトがライフサイクルを通じて有効な状態を維持することを保証する。- 派生型は、オーバーライドされたメンバー関数に対して
in
ブロックを定義することができる。派生型の事前条件は、そのスーパークラスの事前条件よりも厳格であってはならない。in
ブロックを定義しない場合は、"事前条件がまったくない"ことを意味し、プログラマの意図とは異なった結果になる可能性があることに注意。 - 派生型は、オーバーライドされるメンバー関数に対して
out
ブロックを定義することができる。派生メンバー関数は、それ自身だけでなく、スーパークラスの事後条件も遵守しなければならない。