構造体およびクラスの契約プログラミング

契約プログラミングは、コーディングのエラーを減らすのに非常に効果的だ。契約プログラミングの章で、契約プログラミングの2つの機能について紹介した。inブロックとoutブロックは、関数の入力および出力の契約を保証する。

注釈:この章の"inブロックとenforceチェック"のセクションにあるガイドラインを必ず確認。この章の例は、オブジェクトとパラメータの一貫性の問題はプログラマのエラーによるものという前提に基づいている。そうでない場合は、関数本体内でenforceチェックを使用する必要がある。

念のため、ヘロンの公式を使って三角形の面積を計算する関数を書いてみよう。この関数のinoutブロックは、まもなく構造体のコンストラクタに移動する。

この計算が正しく機能するためには、三角形の各辺の長さがゼロより大きい必要がある。さらに、三角形において、1つの辺が他の2つの辺の和より大きいことは不可能であるため、その条件もチェックする必要がある。

これらの入力条件が満たされると、三角形の面積は0より大きくなる。次の関数は、これらの要件がすべて満たされていることを確認する。

private import std.math;

double triangleArea(double a, double b, double c)
in {
    // すべての辺は0より大きくなければならない
    assert(a > 0);
    assert(b > 0);
    assert(c > 0);

    // すべての辺は他の2つの辺の和よりも小さくなければならない
    assert(a < (b + c));
    assert(b < (a + c));
    assert(c < (a + b));

} out (result) {
    assert(result > 0);

} do {
    immutable halfPerimeter = (a + b + c) / 2;

    return sqrt(halfPerimeter
                * (halfPerimeter - a)
                * (halfPerimeter - b)
                * (halfPerimeter - c));
}
D
メンバー関数の前提条件と後条件

inおよびoutブロックは、メンバー関数でも使用できる。

上記の関数を、Triangle構造体のメンバー関数に変換しよう。

import std.stdio;
import std.math;

struct Triangle {
private:

    double a;
    double b;
    double c;

public:

    double area() const
    out (result) {
        assert(result > 0);

    } do {
        immutable halfPerimeter = (a + b + c) / 2;

        return sqrt(halfPerimeter
                    * (halfPerimeter - a)
                    * (halfPerimeter - b)
                    * (halfPerimeter - c));
    }
}

void main() {
    auto threeFourFive = Triangle(3, 4, 5);
    writeln(threeFourFive.area);
}
D
invariant.1

三角形の辺はメンバー変数になったため、この関数はパラメータを受け取らなった。そのため、この関数にはinブロックはない。その代わりに、メンバーはすでに一貫した値を持っていると仮定している。

オブジェクトの一貫性は、以下の機能によって保証される。

オブジェクトの一貫性に関する事前条件と事後条件

上記のメンバー関数は、オブジェクトのメンバーがすでに一貫した値を持っていることを前提として記述されている。この前提を保証する1つの方法は、オブジェクトが必ず一貫した状態で起動するように、コンストラクタにinブロックを定義することだ。

struct Triangle {
// ...

    this(double a, double b, double c)
    in {
        // すべての辺は0より大きくなければならない
        assert(a > 0);
        assert(b > 0);
        assert(c > 0);

        // すべての辺は他の2つの辺の和よりも小さくなければならない
        assert(a < (b + c));
        assert(b < (a + c));
        assert(c < (a + b));

    } do {
        this.a = a;
        this.b = b;
        this.c = c;
    }

// ...
}
D

これにより、実行時に無効なTriangleオブジェクトが作成されるのを防ぐことができる:

auto negativeSide = Triangle(-1, 1, 1);
auto sideTooLong = Triangle(1, 1, 10);
D

コンストラクタのinブロックは、このような無効なオブジェクトの生成を防止する:

core.exception.AssertError@deneme.d: Assertionの失敗

上記のコンストラクタにはoutブロックが定義されていないが、コンストラクタ直後にメンバーの一貫性を確保するために定義することは可能である。

invariant()オブジェクトの一貫性を保つためのブロック

コンストラクタのinおよびoutブロックは、オブジェクトが一致した状態でその寿命を開始することを保証し、メンバー関数のinおよびoutブロックは、それらの関数自体が正しく動作することを保証する。

ただし、これらのチェックは、オブジェクトが常に一貫した状態にあることを保証するには適していない。すべてのメンバ関数でoutブロックを繰り返すことは、過剰であり、エラーの原因となる。

オブジェクトの一貫性および有効性を定義する条件は、そのオブジェクトの不変条件と呼ばれる。例えば、顧客クラスの注文と請求書が1対1で対応している場合、そのクラスの不変条件は、注文配列と請求書配列の長さが等しいことだ。この条件がどのオブジェクトにも満たされない場合、そのオブジェクトは一貫性のない状態になる。

不変条件の例として、カプセル化と保護の属性の章で説明したSchoolクラスを考えてみよう。

class School {
private:

    Student[] students;
    size_t femaleCount;
    size_t maleCount;

// ...
}
D

このクラスのオブジェクトは、その3つのメンバーを含む不変条件が満たされている場合のみ一貫している。student配列の長さは、女性学生と男性学生の合計と等しくなければなりません:

assert(students.length == (femaleCount + maleCount));
D

この条件が偽になる場合、このクラスの実装にバグがある。

invariant()ブロックは、ユーザー定義型の不変条件を保証するためのものだ。invariant()ブロックは、structまたはclassの本体内で定義される。これらのブロックには、inおよびoutブロックと同様のassertチェックが含まれる。

class School {
private:

    Student[] students;
    size_t femaleCount;
    size_t maleCount;

    invariant() {
        assert(students.length == (femaleCount + maleCount));
    }

// ...
}
D

必要に応じて、ユーザー定義型に複数のinvariant()ブロックを含めることができる。

invariant()ブロックは、以下のタイミングで自動的に実行される:

invariant()ブロック内のassertチェックが失敗した場合、AssertErrorがスローされる。これにより、プログラムが不正なオブジェクトで実行を継続することがなくなる。

inおよびoutブロックと同様、invariant()ブロック内のチェックは、コマンドラインオプション-releaseによって無効にすることができる。

dmd deneme.d -w -release
Bash
契約継承

インターフェースおよびクラスメンバー関数にも、inおよびoutブロックを指定することができる。これにより、interfaceまたはclassで、その派生型が依存する前提条件を定義したり、そのユーザーが依存する後条件を定義したりすることができる。派生型では、それらのメンバー関数のオーバーライド用に、さらにinおよびoutブロックを定義することができる。オーバーライドされたinブロックは前提条件を緩和し、オーバーライドされたoutブロックはより強力な保証を提供することができる。

ユーザーコードは通常、派生型から抽象化され、階層の最上位の型の前提条件を満たすように記述される。ユーザーコードは、派生型についてまったく知らない。ユーザーコードはインターフェースの契約のために記述されるため、派生型がオーバーライドされたメンバー関数に、より厳しい前提条件を設定することは許されない。ただし、派生型の前提条件は、そのスーパークラスの前提条件よりも寛容にすることができる。

関数に入ると、inブロックは、階層の最上位型から最下位型に向かって自動的に実行される。inブロックがassertの失敗なく成功した場合、前提条件は満たされているとみなされる。

同様に、派生型もoutブロックを定義することができる。後条件は関数が提供する保証に関するものであるため、派生型のメンバー関数は、その祖先の後条件も遵守しなければならない。一方、追加の保証を提供することもできる。

関数から抜け出すと、outブロックは、最上位の型から最下位の型に向かって自動的に実行される。outブロックがすべて成功した場合にのみ、その関数は後条件を満たしたと見なされる。

以下の人工的なプログラムは、interfaceclassでこれらの機能を説明している。classは呼び出し元に対してより少ない要件を課しながら、より多くの保証を提供する:

interface Iface {
    int[] func(int[] a, int[] b)
    in {
        writeln("Iface.func.in");

        /* このインターフェースメンバー関数は、
         * 2つのパラメータの長さが等しいことを要求する。 */
        assert(a.length == b.length);

    } out (result) {
        writeln("Iface.func.out");

        /* このインターフェースメンバー関数は、
         * 結果の要素数が偶数になることを保証する。
         * (空のスライスも、
         * 要素数が偶数であるとみなされることに注意する。) */
        assert((result.length % 2) == 0);
    }
}

class Class : Iface {
    int[] func(int[] a, int[] b)
    in {
        writeln("Class.func.in");

        /* このクラスメンバー関数は、
         * 少なくとも1つのパラメーターが空である限り、
         * 長さが異なるパラメータを許可することで、祖先の前提条件を緩和する。 */
        assert((a.length == b.length) ||
               (a.length == 0) ||
               (b.length == 0));

    } out (result) {
        writeln("Class.func.out");

        /* このクラスメンバー関数は、追加の
         * 保証を提供する: 結果は空ではなく、
         * 最初の要素と最後の要素が等しいことを保証する。 */
        assert((result.length != 0) &&
               (result[0] == result[$ - 1]));

    } do {
        writeln("Class.func.do");

        /* これは、'in'と'out'ブロックが
         * どのように実行されるかを示すための
         * 人工的な実装である。 */

        int[] result;

        if (a.length == 0) {
            a = b;
        }

        if (b.length == 0) {
            b = a;
        }

        foreach (i; 0 .. a.length) {
            result ~= a[i];
            result ~= b[i];
        }

        result[0] = result[$ - 1] = 42;

        return result;
    }
}

import std.stdio;

void main() {
    auto c = new Class();

    /* 以下の呼び出しはIfaceの事前条件を満たさないが、
     * Classの事前条件を満たしているため受け入れられる。 */
    writeln(c.func([1, 2, 3], []));
}
D
invariant.2

Classinブロックは、Ifaceの事前条件を満たさないため、実行されない:

Iface.func.in
Class.func.in  ← が成功した場合、実行されない
Class.func.do
Iface.func.out
Class.func.out
[42, 1, 2, 2, 3, 42]
要約