プロパティ

プロパティを使用すると、メンバー関数をメンバー変数のように使用することができる。

この機能は、スライスでよく知られている。スライスのlengthプロパティは、そのスライスの要素数を返す。

int[] slice = [ 7, 8, 9 ];
assert(slice.length == 3);
D

この使用方法だけを見ると、.lengthがメンバー変数として実装されているように思えるかもしれない:

struct SliceImplementation {
    int length;

    // ...
}
D

しかし、このプロパティの他の機能を見ると、これがメンバー変数ではないことがわかる。.lengthプロパティに新しい値を代入すると、実際にはスライスの長さが変わり、場合によっては基になる配列に新しい要素が追加される。

slice.length = 5;    // スライスは現在5つの要素を持つ
assert(slice.length == 5);
D

注釈:固定長配列の.lengthプロパティは変更できない。

上記の.lengthへの代入は、単純な値の変更よりも複雑な操作を伴う。配列に新しい長さの容量があるかどうかを判断し、ない場合はメモリを追加で割り当て、既存の要素を新しい場所に移動し、最後に.initによって追加された各要素を初期化する。

明らかに、.lengthへの代入は関数のように動作する。

プロパティは、メンバー変数のように使用されるメンバー関数である。

括弧を使用しない関数の呼び出し

前の章で述べたように、渡す引数がない場合、関数は括弧なしで呼び出すことができる。

writeln();
writeln;      // 前の行と同じ
D

この機能は、プロパティはほとんどの場合括弧なしで使うため、プロパティと密接に関連している。

値を返すプロパティ関数

簡単な例として、2つのメンバーで構成される長方形構造体を考えてみよう。

struct Rectangle {
    double width;
    double height;
}
D

この型に、長方形の面積を提供する3番目のプロパティが必要になったとしよう。

auto garden = Rectangle(10, 20);
writeln(garden.area);
D

この要件を満たす一つの方法は、3つ目のメンバーを定義することだ:

struct Rectangle {
    double width;
    double height;
    double area;
}
D

この設計の欠点は、オブジェクトが容易に一貫性を失う可能性があることだ。長方形は常に"幅 × 高さ == 面積"という不変条件を満たす必要があるが、メンバーが自由に独立して変更可能だと、この一貫性が破られる可能性がある。

極端な例としては、オブジェクトが矛盾した状態で誕生することさえあり得る。

// オブジェクトに矛盾がある: 面積は10 * 20 == 200ではない。
auto garden = Rectangle(10, 20, 1111);
D

より良い方法は、面積の概念をプロパティとして表現することだ。追加のメンバーを定義する代わりに、そのメンバーの値は、それが表す概念と同じ名前の関数areaによって計算される。

struct Rectangle {
    double width;
    double height;

    double area() const {
        return width * height;
    }
}
D

注釈: const refパラメータとconstメンバー関数の章で説明したように、関数宣言のconst指定子は、この関数によってオブジェクトが変更されないことを保証する。

このプロパティ関数により、構造体は3番目のメンバー変数があるかのように使用することができる。

auto garden = Rectangle(10, 20);
writeln("The area of the garden: ", garden.area);
D

areaプロパティの値は、長方形の高さと幅を乗算して計算されるため、今回は常に一貫性がある。

庭の面積200
代入で使用されるプロパティ関数

スライスのlengthプロパティと同様に、ユーザー定義型のプロパティも代入操作で使用することができる。

garden.area = 50;
D

この代入によって実際に長方形の面積を変更するには、構造体の2つのメンバーを適宜変更する必要がある。この機能を有効にするには、長方形が柔軟であり、"width * height == area"という不変条件を維持するために、長方形の辺を変更できると仮定する。

このような代入構文を可能にする関数も、areaという名前である。代入の右側で使用される値は、この関数の唯一のパラメータになる。

area()を次のように追加定義すると、代入操作でこのプロパティを使用し、Rectangleオブジェクトの面積を効果的に変更できるようになる。

import std.stdio;
import std.math;

struct Rectangle {
    double width;
    double height;

    double area() const {
        return width * height;
    }

    void area(double newArea) {
        auto scale = sqrt(newArea / area);

        width *= scale;
        height *= scale;
    }
}

void main() {
    auto garden = Rectangle(10, 20);
    writeln("The area of the garden: ", garden.area);

    garden.area = 50;

    writefln("New state: %s x %s = %s",
             garden.width, garden.height, garden.area);
}
D
property.1

新しい関数は、std.mathモジュールにあるsqrt関数を利用しており、指定された値の平方根を返す。長方形の幅と高さが、その比率の平方根でスケーリングされた場合、面積は希望する値になる。

その結果、現在の値の4分の1をareaに割り当てると、長方形の2辺の長さが半分になる。

庭の面積200
新しい状態5 x 10 = 50
プロパティは必ずしも必要ではない

上記では、Rectangleが3番目のメンバー変数があるかのように使用できることを紹介した。ただし、プロパティの代わりに通常のメンバー関数を使用することもできる。

import std.stdio;
import std.math;

struct Rectangle {
    double width;
    double height;

    double area() const {
        return width * height;
    }

    void setArea(double newArea) {
        auto scale = sqrt(newArea / area);

        width *= scale;
        height *= scale;
    }
}

void main() {
    auto garden = Rectangle(10, 20);
    writeln("The area of the garden: ", garden.area());

    garden.setArea(50);

    writefln("New state: %s x %s = %s",
             garden.width, garden.height, garden.area());
}
D
property.2

さらに、関数のオーバーロードの章で見たように、これら2つの関数は同じ名前でもかまわない。

double area() const {
    // ...
}

void area(double newArea) {
    // ...
}
D
使用タイミング

通常のメンバー関数とプロパティのどちらを選ぶべきかは、簡単には決められないかもしれない。通常のメンバー関数の方が自然である場合もあれば、プロパティの方が自然である場合もある。

しかし、カプセル化と保護属性の章で見たように、メンバー変数への直接アクセスを制限することは重要だ。ユーザーコードがメンバー変数を自由に変更できると、必ずコードのメンテナンスに問題が発生する。そのため、メンバー変数は、通常のメンバー関数またはプロパティ関数によってカプセル化することをお勧めする。

widthheightなどのメンバーをpublicアクセス可能のままにしておくことは、非常に単純な型の場合にのみ許容される。ほとんどの場合、プロパティ関数を使用する方法の方が優れた設計だ。

struct Rectangle {
private:

    double width_;
    double height_;

public:

    double area() const {
        return width * height;
    }

    void area(double newArea) {
        auto scale = sqrt(newArea / area);

        width_ *= scale;
        height_ *= scale;
    }

    double width() const {
        return width_;
    }

    double height() const {
        return height_;
    }
}
D

メンバーが、対応するプロパティ関数からのみアクセスできるように、privateになっていることに注意。

また、メンバー関数との名前が混同されないように、メンバー変数の名前に_文字が追加されていることに注意。メンバー変数の名前に装飾を施すことは、オブジェクト指向プログラミングでは一般的な手法である。

このRectangleの定義では、widthheightがメンバー変数として表示されていることに注意しよう:

auto garden = Rectangle(10, 20);
writefln("width: %s, height: %s",
         garden.width, garden.height);
D

メンバー変数を変更するプロパティ関数がない場合、そのメンバーは外部からは事実上読み取り専用になる。

garden.width = 100;    // ← コンパイルエラー
D

これは、メンバーの改変を制御するために重要だ。メンバー変数は、オブジェクトの一貫性を確保するために、Rectangle型自体によってのみ変更できる。

後で、メンバー変数を外部から変更できるようにすることが妥当になった場合は、そのメンバーに対して別のプロパティ関数を定義するだけで済む。

@property

プロパティ関数は、@property属性を使用して定義することもできる。ただし、ベストプラクティスとしては、この属性の使用は推奨されない。

import std.stdio;

struct Foo {
    @property int a() const {
        return 42;
    }

    int b() const {    // ← @propertyなしで定義
        return 42;
    }
}

void main() {
    auto f = Foo();

    writeln(typeof(f.a).stringof);
    writeln(typeof(f.b).stringof);
}
D
property.3

@property属性の唯一の効果は、構文上プロパティ関数の呼び出しである可能性のある式の型を決定する場合だけだ。以下の出力でわかるように、式f.af.bの型は異なる。

int            ← 式f.aの型(戻り値の型)
const int()    ← メンバー関数Foo.bの型