関数ポインタ、デリゲート、およびラムダ

関数ポインタは、後でその関数を実行するために、関数のアドレスを格納するためのものだ。関数ポインタは、C プログラミング言語の関数ポインタと似ている。

デリゲートは、関数ポインタと、その関数ポインタを実行するためのコンテキストの両方を格納する。格納されるコンテキストは、関数の実行が行われるスコープ、またはstructまたはclassオブジェクトのいずれかだ。

デリゲートは、ほとんどの関数型プログラミング言語でサポートされている概念であるクロージャも実現する。

関数ポインタ

前の章で、&演算子を使用して関数のアドレスを取得できることを学んだ。その例の1つでは、そのようなアドレスを関数テンプレートに渡した。

テンプレート型パラメータは任意の型と一致できることを利用して、関数ポインタをテンプレートに渡して、その.stringofプロパティを出力してその型を確認しよう。

import std.stdio;

int myFunction(char c, double d) {
    return 42;
}

void main() {
    myTemplate(&myFunction);    // 関数のアドレスを取得し
                                // それをパラメータとして渡す
}

void myTemplate(T)(T parameter) {
    writeln("type : ", T.stringof);
    writeln("value: ", parameter);
}

プログラムの出力は、myFunction()の型とアドレスを表している。

int function(char c, double d)
406948
メンバー関数ポインタ

メンバー関数のアドレスは、型または型のオブジェクトのいずれかで取得できるが、結果は異なる。

struct MyStruct {
    void func() {
    }
}

void main() {
    auto o = MyStruct();

    auto f = &MyStruct.func;    // 型の上
    auto d = &o.func;           // オブジェクトの上

    static assert(is (typeof(f) == void function()));
    static assert(is (typeof(d) == void delegate()));
}

上記の2つのstatic assert行から、ffunctionであり、ddelegateであることがわかる。後で説明する通り、dは直接呼び出せるが、fは呼び出す対象となるオブジェクトが必要だ。

定義

通常のポインタと同様に、各関数ポインタ型は特定の関数型を正確に指すことができる。関数ポインタと関数のパラメータリストおよび戻り値の型は一致する必要がある。関数ポインタは、その特定の型の戻り値の型とパラメータリストの間にfunctionキーワードを挿入して定義する。

return_type function(parameters) ptr;
D

パラメータの名前(上記の出力ではcおよびd)はオプションだ。myFunction()charおよびdoubleを受け取り、intを返すため、myFunction()を指すことができる関数ポインタの型は、それに応じて定義する必要がある。

int function(char, double) ptr = &myFunction;
D

上記の行は、ptrを、2つのパラメータ (charおよびdouble) を取り、intを返す関数ポインタとして定義している。その値は、myFunction()のアドレスだ。

関数ポインタの構文は比較的読みにくいので、aliasを使用してコードを読みやすくすることが一般的である。

alias CalculationFunc = int function(char, double);
D

このエイリアスを使用すると、コードが読みやすくなる:

CalculationFunc ptr = &myFunction;
D

他の型と同様に、autoも使用できる。

auto ptr = &myFunction;
D
関数ポインタの呼び出し

関数ポインタは、関数とまったく同じように呼び出すことができる。

int result = ptr('a', 5.67);
assert(result == 42);
D

上記の呼び出しptr('a', 5.67)は、実際の関数myFunction('a', 5.67)を呼び出すことと同じである。

使用する場合

関数ポインタは、呼び出す関数を格納し、それが指す関数とまったく同じように呼び出されるため、関数ポインタは、プログラムの動作を後で使用するために効果的に格納する。

EmployeeDには、プログラムの動作に関する他の多くの機能がある。例えば、従業員の給与を計算するために呼び出す適切な関数は、enumメンバーの値によって決定することができる。

final switch (employee.type) {

case EmployeeType.fullTime:
    fullTimeEmployeeWages();
    break;

case EmployeeType.hourly:
    hourlyEmployeeWages();
    break;
}
D

残念ながら、この方法は、既知のすべての従業員型をサポートしなければならないため、メンテナンスが比較的困難だ。新しい従業員型がプログラムに追加された場合、そのようなswitch文をすべて見つけ、新しい従業員型用の新しいcase句を追加する必要がある。

動作の違いを実装するより一般的な代替手段は、多型だ。Employeeインターフェースを定義し、そのインターフェースのさまざまな実装によってさまざまな給与の計算を処理することができる。

interface Employee {
    double wages();
}

class FullTimeEmployee : Employee {
    double wages() {
        double result;
        // ...
        return result;
    }
}

class HourlyEmployee : Employee {
    double wages() {
        double result;
        // ...
        return result;
    }
}

// ...

    double result = employee.wages();
D

関数ポインタも、異なる動作を実装するためのもう1つの代替手段である。これらは、オブジェクト指向プログラミングをサポートしていないプログラミング言語でよく使用される。

パラメータとしての関数ポインタ

配列を受け取り、別の配列を返す関数を設計しよう。この関数は、値が 0 以下の要素をフィルタリングし、それ以外の要素に10を乗算する。

int[] filterAndConvert(const int[] numbers) {
    int[] result;

    foreach (e; numbers) {
        if (e > 0) {                       // フィルタリング、
            immutable newNumber = e * 10;  // および変換
            result ~= newNumber;
        }
    }

    return result;
}

次のプログラムは、ランダムに生成された値を使用してその動作を示している。

import std.stdio;
import std.random;

void main() {
    int[] numbers;

    // ランダムな数字
    foreach (i; 0 .. 10) {
        numbers ~= uniform(0, 10) - 5;
    }

    writeln("input : ", numbers);
    writeln("output: ", filterAndConvert(numbers));
}
D

出力には、元の値が0より大きい数値が元の値の10倍になった数値が含まれている。選択された元の数値はハイライト表示されている:

入力[-2, 2, -2, 3, -2, 2, -1, -4, 0, 0]
出力[20, 30, 20]

filterAndConvert()これは非常に特定のタスク向けだ:常に0より大きい数値を選択し、常にそれらを10倍する。フィルタリングと変換の動作をパラメーター化すれば、より有用になるかもしれない。

フィルタリングも変換の一種(intからboolへ)であることを考慮すると、filterAndConvert()は2つの変換を実行していることになる。

上記の2つの変換に一致する関数ポインタの便利なエイリアスを定義しよう。

alias Predicate = bool function(int);    // intからboolを作る
alias Convertor = int function(int);     // intからintを作る
D

Predicateは、intを受け取り、boolを返す関数の型で、Convertorは、intを受け取り、intを返す関数の型である。

このような関数ポインタをパラメータとして指定すると、filterAndConvert()はその動作中にこれらの関数ポインタを使用することができる。

int[] filterAndConvert(const int[] numbers,
                       Predicate predicate,
                       Convertor convertor) {
    int[] result;

    foreach (number; numbers) {
        if (predicate(number)) {
            immutable newNumber = convertor(number);
            result ~= newNumber;
        }
    }

    return result;
}
D

filterAndConvert() これで、実際のフィルタリングおよび変換操作に依存しないアルゴリズムができた。必要に応じて、以前の動作は次の2つの単純な関数によって実現できる。

bool isGreaterThanZero(int number) {
    return number > 0;
}

int tenTimes(int number) {
    return number * 10;
}

// ...

    writeln("output: ", filterAndConvert(numbers,
                                         &isGreaterThanZero,
                                         &tenTimes));
D

この設計により、任意のフィルタリングおよび変換動作を持つfilterAndConvert()を呼び出すことができる。例えば、次の2つの関数を使用すると、filterAndConvert()は偶数の負の値を生成する。

bool isEven(int number) {
    return (number % 2) == 0;
}

int negativeOf(int number) {
    return -number;
}

// ...

    writeln("output: ", filterAndConvert(numbers,
                                         &isEven,
                                         &negativeOf));
D

出力:

入力[3, -3, 2, 1, -5, 1, 2, 3, 4, -4]
出力[-2, -2, -4, 4]

これらの例でわかるように、このような関数は非常に単純であるため、名前、戻り値の型、パラメータのリスト、および中括弧で囲んだ適切な関数として定義するのは不必要に冗長だ。

以下で見るように、=>の匿名関数の構文を使用すると、コードがより簡潔で読みやすくなる。次の行には、isEven()およびnegativeOf()と同等の匿名関数が、適切な関数定義なしで記述されている。

writeln("output: ", filterAndConvert(numbers,
                                     number => (number % 2) == 0,
                                     number => -number));
D
メンバーとしての関数ポインタ

関数ポインタは、構造体やクラスのメンバーとしても格納できる。これを確認するために、後で使用するために、述語と変換子をコンストラクタのパラメータとして受け取るclassを設計しよう。

class NumberHandler {
    Predicate predicate;
    Convertor convertor;

    this(Predicate predicate, Convertor convertor) {
        this.predicate = predicate;
        this.convertor = convertor;
    }

    int[] handle(const int[] numbers) {
        int[] result;

        foreach (number; numbers) {
            if (predicate(number)) {
                immutable newNumber = convertor(number);
                result ~= newNumber;
            }
        }

        return result;
    }
}
D

この型のオブジェクトは、filterAndConvert()と同様に使うことができる。

auto handler = new NumberHandler(&isEven, &negativeOf);
writeln("result: ", handler.handle(numbers));
D
関数

短い関数を適切な関数定義なしで定義すると、コードがより読みやすく簡潔になる。

関数リテラル またはラムダとも呼ばれる匿名関数を使用すると、式の中で関数を定義することができる。匿名関数は、関数ポインタを使用できる場所ならどこでも使用することができる。

そのより短い=>構文については、後で説明する。まず、完全な構文を見てみよう。これは、特に他の式の中に現れる場合、通常、あまりにも冗長になる。

function return_type(parameters) { /* 操作 */ }
D

例えば、2より大きい数値の7倍を生成するNumberHandlerオブジェクトは、次のコードのように、匿名関数を使用して構築することができる。

new NumberHandler(function bool(int number) { return number > 2; },
                  function int(int number) { return number * 7; });
D

上記のコードの2つの利点は、関数が適切な関数として定義されていないことと、その実装がNumberHandlerオブジェクトが構築された場所で直接確認できることである。

匿名関数の構文は、通常の関数の構文とよく似ていることに注意。この一貫性には利点があるが、匿名関数の完全な構文はコードを冗長にしてしまいる。

そのため、匿名関数を定義するより短い方法がいくつかある。

より短い構文

戻り値の型が、匿名関数内のreturn文から推測できる場合は、戻り値の型を指定する必要はない (戻り値の型が通常表示される場所は、コードコメントで強調表示されている)。

new NumberHandler(function /**/(int number) { return number > 2; },
                  function /**/(int number) { return number * 7; });
D

さらに、匿名関数がパラメータを受け取らない場合、そのパラメータリストを指定する必要はない。何も受け取らずdoubleを返す関数ポインタを受け取る関数を考えてみよう。

void foo(double function() func) {
    // ...
}
D

その関数に渡される匿名関数は、空のパラメータリストを持つ必要はない。したがって、次の3つの匿名関数の構文はすべて同等である。

foo(function double() { return 42.42; });
foo(function () { return 42.42; });
foo(function { return 42.42; });
D

1つ目は完全な構文で記述されている。2つ目は、戻り値の型推論を利用して、戻り値の型を省略している。3つ目は、不要なパラメータリストを省略している。

さらに、キーワードfunctionも指定する必要はない。その場合、それが匿名関数か匿名デリゲートかを判断するのはコンパイラに任される。囲んでいるスコープの変数を使用しない限り、それは関数になる。

foo({ return 42.42; });
D

ほとんどの匿名関数は、ラムダ構文を使用することでさらに短く定義できる。

単一のreturn文の代わりにラムダ構文を使用

ほとんどの場合、上記の最も短い構文でさえも不必要に煩雑だ。関数パラメータリストのすぐ内側に中括弧があることでコードが読みにくくなり、関数引数内のreturn文とそのセミコロンが場違いに見える。

returnステートメントが1つだけある匿名関数の完全な構文から始めよう。

function return_type(parameters) { return expression; }
D

functionキーワードは不要であり、戻り値の型は推測できることはすでに説明した。

(parameters) { return expression; }
D

その定義の同等物は、次の=>構文で表すことができる。ここで、=>文字は波括弧、returnキーワード、およびセミコロンを置き換える:

(parameters) => expression
D

この構文の意味は、"これらのパラメータが与えられた場合、この式 (値) を生成する"と表現できる。

さらに、パラメーターが1つの場合、パラメーターリストの周囲の括弧も省略できる:

single_parameter => expression
D

一方、文法上の曖昧さを避けるため、パラメーターが全くない場合でも、パラメーターリストは空の丸括弧で表す必要がある:

() => expression
D

他の言語でラムダ式を知っているプログラマは、=>の後に中括弧を使用してしまうミスを犯す可能性がある。これは、別の意味を持つD構文として有効である。

// 'a + 1'を返すラムダ式
auto l0 = (int a) => a + 1

// 'a + 1'を返すパラメータのない
// ラムダ式を返すラムダ式
auto l1 = (int a) => { return a + 1; }

assert(l0(42) == 43);
assert(l1(42)() == 43);    // l1が返すものを実行する
D

std.algorithm.filterに渡される述語でラムダ構文を使ってみよう。filter()は、テンプレートパラメータとして述語、関数パラメータとして範囲を受け取る。述語を範囲の各要素に適用し、述語を満たす要素を返す。述語を指定するいくつかの方法のうちの1つがラムダ構文だ。

(注釈: 範囲については後の章で説明する。この時点では、Dスライスは範囲であるということを知っておけば十分だ。)

次のラムダ式は、10より大きい要素に一致する述語である:

import std.stdio;
import std.algorithm;

void main() {
    int[] numbers = [ 20, 1, 10, 300, -2 ];
    writeln(numbers.filter!(number => number > 10));
}

出力には、述語を満たす要素のみが含まれる:

[20, 300]

比較のために、同じラムダ式を最長構文で記述しよう。匿名関数の本体を定義する中括弧が強調表示されている。

writeln(numbers.filter!(function bool(int number) {
                            return number > 10;
                        }));
D

別の例として、今回は2つのパラメータを取る匿名関数を定義しよう。次のアルゴリズムは、2つのスライスを受け取り、それに対応する要素を1つずつ、2つのパラメータを取るfunctionに渡す。そして、結果を別のスライスとして収集して返す。

import std.exception;

int[] binaryAlgorithm(int function(int, int) func,
                      const int[] slice1,
                      const int[] slice2) {
    enforce(slice1.length == slice2.length);

    int[] results;

    foreach (i; 0 .. slice1.length) {
        results ~= func(slice1[i], slice2[i]);
    }

    return results;
}

上記のfunctionパラメーターが2つのパラメーターを取るため、binaryAlgorithm()に渡すラムダ式も2つのパラメーターを取らなければならない:

import std.stdio;

void main() {
    writeln(binaryAlgorithm((a, b) => (a * 10) + b,
                            [ 1, 2, 3 ],
                            [ 4, 5, 6 ]));
}
D

出力には、最初の配列の要素の10倍に2番目の配列の要素を加えたものが含まれる(例:14は10 × 1 + 4):

[14, 25, 36]
デリゲート

デリゲートは、関数ポインタと、その関数が実行されるコンテキストとの組み合わせだ。デリゲートはDでもクロージャをサポートしている。クロージャは、多くの関数型プログラミング言語でサポートされている機能だ。

ライフタイムと基本操作の章で見たように、変数のライフタイムは、それが定義されたスコープを離れると終了する:

{
    int increment = 10;
    // ...
} // ← 'increment'の寿命はここで終了する
D

そのため、このようなローカル変数のアドレスは関数から返すことができない。

incrementが、functionを返す関数のローカル変数であると想像しよう。返されるラムダが、そのローカル変数を使用するようにしよう。

alias Calculator = int function(int);

Calculator makeCalculator() {
    int increment = 10;
    return value => increment + value;    // ← コンパイルエラー
}
D

このコードはエラーである。返されたラムダ式が、スコープ外に出ようとしているローカル変数を使用しているからだ。このコードがコンパイル可能だった場合、ラムダ式はincrementにアクセスしようとし、その変数のライフタイムは既に終了しているため、エラーが発生する。

このコードをコンパイルして正しく動作させるには、incrementの有効期間が、それを使用するラムダの有効期間以上である必要がある。デリゲートは、関数が使用するローカル状態が有効であり続けるように、ラムダのコンテキストの有効期間を延長する。

delegate functionの構文と似ているが、唯一の違いはキーワードである。この変更だけで、以前のコードがコンパイル可能になる:

alias Calculator = int delegate(int);

Calculator makeCalculator() {
    int increment = 10;
    return value => increment + value;
}
D

デリゲートによって使用されたローカル変数incrementは、そのデリゲートが存続する限り存続する。この変数は、他の変数と同様にデリゲートから利用でき、必要に応じて変更可能だ。この例については、次の章で、opApply()メンバー関数とデリゲートを使用する場合に説明する。

以下のコードは、上記のデリゲートをテストするコードだ:

auto calculator = makeCalculator();
writeln("The result of the calculation: ", calculator(3));
D

makeCalculator()は、匿名デリゲートを返すことに注意。上記のコードは、そのデリゲートを変数calculatorに割り当て、calculator(3)で呼び出している。デリゲートは、そのパラメータと変数incrementの和を返すように実装されているため、コードは3と10の和を出力する。

計算結果13
より短い構文

前の例で既に使用したように、デリゲートはより短い構文も利用できる。functiondelegateも指定されていない場合、ラムダの型は、ラムダがローカル状態にアクセスするかどうかによって、コンパイラによって決定される。アクセスする場合は、delegateになる。

次の例は、パラメータを受け取らないデリゲートがある。

int[] delimitedNumbers(int count, int delegate() numberGenerator) {
    int[] result = [ -1 ];
    result.reserve(count + 2);

    foreach (i; 0 .. count) {
        result ~= numberGenerator();
    }

    result ~= -1;

    return result;
}
D

関数delimitedNumbers()は、最初と最後の要素が-1のスライスを生成する。この関数は、最初と最後の要素の間にくる他の要素を指定する2つのパラメータを取る。

常に同じ値を返す単純なデリゲートを使って、この関数を呼び出そう。パラメータがない場合、ラムダのパラメータリストは空として指定する必要があることを覚えておいてほしい。

writeln(delimitedNumbers(3, () => 42));
D

出力:

-1424242-1

今度は、ローカル変数を使用するデリゲートでdelimitedNumbers()を呼び出そう:

int lastNumber;
writeln(delimitedNumbers(
            15, () => lastNumber += uniform(0, 3)));

writeln("Last number: ", lastNumber);
D

このデリゲートはランダムな値を生成するが、値は最後の値に追加されるため、生成される値はいずれも前の値よりも小さくならない。

-102346689991012141517-1
最後の数字17
オブジェクトとメンバー関数をデリゲートとして

デリゲートは、関数ポインタとそれが実行されるコンテキストにすぎないことをこれまで見てきた。この2つの代わりに、デリゲートは、メンバー関数と、そのメンバー関数が呼び出される既存のオブジェクトで構成することもできる。

オブジェクトからこのようなデリゲートを定義する構文は次の通りだ:

&object.member_function
D

まず、この構文が実際にdelegateを定義していることを、string表現で出力して確認しよう:

import std.stdio;

struct Location {
    long x;
    long y;

    void moveHorizontally(long step) { x += step; }
    void moveVertically(long step)   { y += step; }
}

void main() {
    auto location = Location();
    writeln(typeof(&location.moveHorizontally).stringof);
}

出力によると、locationで呼び出されるmoveHorizontally()の型は、確かにdelegateである。

void delegate(long step)

&構文は、デリゲートを構築するためだけのものだということに注意。デリゲートは、後で関数呼び出し構文によって呼び出される。

// delegate変数の定義:
auto directionFunction = &location.moveHorizontally;

// 関数呼び出し構文によるdelegateの呼び出し:
directionFunction(3);

writeln(location);
D

delegateは、locationオブジェクトとmoveHorizontally()メンバー関数を組み合わせたものなので、デリゲートを呼び出すことは、locationmoveHorizontally()を呼び出すことと同じになる。出力は、オブジェクトが実際に水平方向に 3 ステップ移動したことを示している。

Location(3, 0)

関数ポインタ、ラムダ、およびデリゲートは式である。これらは、その型の値が期待される場所で使用できる。例えば、delegateオブジェクトのスライスは、オブジェクトとそのさまざまなメンバー関数から構築されたデリゲートから、以下のように初期化される。スライスのdelegate要素は、後で関数と同じように呼び出される。

auto location = Location();

void delegate(long)[] movements =
    [ &location.moveHorizontally,
      &location.moveVertically,
      &location.moveHorizontally ];

foreach (movement; movements) {
    movement(1);
}

writeln(location);
D

スライスの要素によると、位置は水平方向に2回、垂直方向に1回変更されている:

Location(2, 1)
デリゲートプロパティ

デリゲート関数の関数ポインタとコンテキストポインタは、それぞれ.funcptrプロパティと.ptrプロパティからアクセスできる。

struct MyStruct {
    void func() {
    }
}

void main() {
    auto o = MyStruct();

    auto d = &o.func;

    assert(d.funcptr == &MyStruct.func);
    assert(d.ptr == &o);
}

これらのプロパティを明示的に設定することで、delegateを最初から作成することができる。

struct MyStruct {
    int i;

    void func() {
        import std.stdio;
        writeln(i);
    }
}

void main() {
    auto o = MyStruct(42);

    void delegate() d;
    assert(d is null);    // 最初はnull

    d.funcptr = &MyStruct.func;
    d.ptr = &o;

    d();
}

上記のデリゲートをd()として呼び出すことは、式o.func()(つまり、oMyStruct.funcを呼び出すこと)と同等だ。

42
遅延パラメーターはデリゲートだ

関数パラメータの章で、lazyキーワードを見た。

void log(Level level, lazy string message) {
    if (level >= interestedLevel) {
        writeln(message);
    }
}

// ...

    if (failedToConnect) {
        log(Level.medium,
            format("Failure. The connection state is '%s'.",
                   getConnectionState()));
    }
D

messageは上記のlazyパラメータであるため、そのパラメータがlog()内で使用された場合に、format式全体 (getConnectionState()の呼び出しを含む)が評価される。

内部的には、遅延パラメーターは実際にはデリゲートであり、遅延パラメーターに渡される引数は、コンパイラーによって自動的に作成されるデリゲートオブジェクトである。以下のコードは、上記のコードと等価だ:

void log(Level level, string delegate() lazyMessage) {  // (1)
    if (level >= interestedLevel) {
        writefln("%s", lazyMessage());                  // (2)
    }
}

// ...

    if (failedToConnect) {
        log(Level.medium,
            delegate string() {                         // (3)
                return format(
                    "Failure. The connection state is '%s'.",
                    getConnectionState());
            });
    }
D
  1. lazyパラメータはstringではなく、stringを返すデリゲートだ。
  2. そのデリゲートが呼び出されて、その戻り値が取得される。
  3. 式全体がデリゲートで囲まれ、そこから返される。
遅延可変長引数関数

関数に可変数の遅延パラメータが必要な場合、その未知のパラメータ数を lazyとして指定することは必然的に不可能だ。

この問題を解決するには、可変長のdelegateパラメータを使用する。このようなパラメータは、それらのデリゲートと同じ戻り値の型を持つ式をいくつでも受け取ることができる。デリゲートはパラメータを受け取ることができない。

import std.stdio;

void foo(double delegate()[] args...) {
    foreach (arg; args) {
        writeln(arg());     // 各デリゲートを呼び出す
    }
}

void main() {
    foo(1.5, () => 2.5);    // 'double'をデリゲートとして渡す
}

double式とラムダ式の両方が可変長引数とどのように一致するかに注意。double式は自動的にデリゲート内にラップされ、関数はそのすべての事実上遅延パラメータの値を出力する。

1.5
2.5

このメソッドの制限として、すべてのパラメータは同じ型(上記のdouble)でなければならない。この制限を解除するには、後述のその他のテンプレートの章で、タプルテンプレートパラメータを利用する方法を見ていく。

toString() delegateパラメーター付き

この本では、オブジェクトを文字列として表現するために、これまで多くのtoString()関数を定義してきた。これらのtoString()定義は、いずれもパラメータを受け取らずにstringを返していた。以下のコメント行で指摘されているように、構造体およびクラスは、それぞれのメンバーをformat()に渡すだけで、それぞれのメンバーのtoString()関数を利用していた。

import std.stdio;
import std.string;

struct Point {
    int x;
    int y;

    string toString() const {
        return format("(%s,%s)", x, y);
    }
}

struct Color {
    ubyte r;
    ubyte g;
    ubyte b;

    string toString() const {
        return format("RGB:%s,%s,%s", r, g, b);
    }
}

struct ColoredPoint {
    Color color;
    Point point;

    string toString() const {
        /* Color.toStringおよび
         * Point.toStringを活用: */
        return format("{%s;%s}", color, point);
    }
}

struct Polygon {
    ColoredPoint[] points;

    string toString() const {
        /* ColoredPoint.toStringを活用: */
        return format("%s", points);
    }
}

void main() {
    auto polygon = Polygon(
        [ ColoredPoint(Color(10, 10, 10), Point(1, 1)),
          ColoredPoint(Color(20, 20, 20), Point(2, 2)),
          ColoredPoint(Color(30, 30, 30), Point(3, 3)) ]);

    writeln(polygon);
}

プログラムの最後の行で、polygonstringとして出力されるためには、PolygonColoredPointColor、およびPointのすべてのtoString()関数が間接的に呼び出され、その過程で合計10個の文字列が作成される。下位レベルの関数によって構築され、返される文字列は、それらを呼び出したそれぞれの上位レベルの関数によって1回だけ使用されることに注意しよう。

ただし、合計で10個の文字列が作成されるものの、出力に表示されるのは最後の1つだけだ。

[{RGB:10,10,10;(1,1)}, {RGB:20,20,20;(2,2)}, {RGB:30,30,30;(3,3)}]

この方法は実用的だが、多くのstringオブジェクトが構築され、すぐに破棄されるため、プログラムのパフォーマンスが低下する可能性がある。

toString()のオーバーロードは、delegateパラメータを取ることでこのパフォーマンス問題を回避する:

void toString(void delegate(const(char)[]) sink) const;
D

宣言からわかるように、このオーバーロードされたtoString()stringを返さない。代わりに、出力される文字列はdelegateパラメーターに渡しされる。出力される単一のstringにこれらの文字列をappendする責任は、delegateにある。

プログラマーが変更しなければならないのは、std.string.formatの代わりにstd.format.formattedWriteを呼び出し、delegateパラメータを最初のパラメータとして渡すことだけだ(以下のUFCSを参照)。また、formattedWriteのコンパイル時のフォーマット文字列チェックを利用するために、以下の呼び出しではフォーマット文字列をテンプレート引数として指定していることに注意。

import std.stdio;
import std.format;

struct Point {
    int x;
    int y;

    void toString(void delegate(const(char)[]) sink) const {
        sink.formattedWrite!"(%s,%s)"(x, y);
    }
}

struct Color {
    ubyte r;
    ubyte g;
    ubyte b;

    void toString(void delegate(const(char)[]) sink) const {
        sink.formattedWrite!"RGB:%s,%s,%s"(r, g, b);
    }
}

struct ColoredPoint {
    Color color;
    Point point;

    void toString(void delegate(const(char)[]) sink) const {
        sink.formattedWrite!"{%s;%s}"(color, point);
    }
}

struct Polygon {
    ColoredPoint[] points;

    void toString(void delegate(const(char)[]) sink) const {
        sink.formattedWrite!"%s"(points);
    }
}

void main() {
    auto polygon = Polygon(
        [ ColoredPoint(Color(10, 10, 10), Point(1, 1)),
          ColoredPoint(Color(20, 20, 20), Point(2, 2)),
          ColoredPoint(Color(30, 30, 30), Point(3, 3)) ]);

    writeln(polygon);
}

このプログラムの利点は、さまざまなtoString()関数に対して合計10件の呼び出しが行われているにもかかわらず、これらの呼び出しはまとめて1つのstringを生成し、10件生成しないことだ。

要約