構造体およびクラスの契約プログラミング
契約プログラミングは、コーディングのエラーを減らすのに非常に効果的だ。契約プログラミングの章で、契約プログラミングの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]
要約
inoutブロックはコンストラクタでも有用だ。これらは、オブジェクトが有効な状態で構築されることを保証する。invariant()ブロックは、オブジェクトがライフサイクルを通じて有効な状態を維持することを保証する。- 派生型は、オーバーライドされたメンバー関数に対して
inブロックを定義することができる。派生型の事前条件は、そのスーパークラスの事前条件よりも厳格であってはならない。inブロックを定義しない場合は、"事前条件がまったくない"ことを意味し、プログラマの意図とは異なった結果になる可能性があることに注意。 - 派生型は、オーバーライドされるメンバー関数に対して
outブロックを定義することができる。派生メンバー関数は、それ自身だけでなく、スーパークラスの事後条件も遵守しなければならない。