構造体およびクラスを使用したforeach

foreachループの章で覚えているように、foreachの動作、およびそれがサポートするループ変数の型と数は、コレクションの種類によって異なる。スライスの場合、foreachは、カウンタの有無にかかわらず要素へのアクセスを提供する。連想配列の場合、キーの有無にかかわらず値へのアクセスを提供する。数値範囲の場合、個々の値へのアクセスを提供する。ライブラリ型の場合、foreachはその型に固有の動作をする。例えば、Fileの場合、ファイルの各行を提供する。

ユーザー定義型についても、foreachの動作を定義することができる。このサポートを提供するには2つの方法がある。

2つの方法のうち、opApplyが優先される: opApplyが定義されている場合、コンパイラはopApplyを使用する。それ以外の場合、rangeメンバー関数が考慮される。ただし、ほとんどの場合、rangeメンバー関数で十分であり、より簡単で、より便利だ。

foreachすべての型でサポートする必要はない。オブジェクトを反復処理することは、そのオブジェクトがコレクションの概念を定義している場合にのみ意味がある。

例えば、学生を表すクラスを反復処理する場合、foreachがどの要素を提供すべきかは明確ではないため、このクラスはforeachをまったくサポートしないほうがよい。一方、Studentが成績のコレクションであり、foreachが学生の個々の成績を提供することが設計上必要な場合もある。

どの型がこのサポートを提供すべきか、またその方法は、プログラムの設計によって異なる。

foreach範囲メンバー関数によるサポート

foreachforと非常に似ているが、forよりも有用で安全であることは知っている。次のループを考えてみよう:

foreach (element; myObject) {
    // ... 式 ...
}
D

裏では、コンパイラはforeachループをforループに再記述し、おおむね以下のコードと等価なものにする:

for ( ; /* 完了しない間 */; /* 最初の要素をスキップ */) {

    auto element = /* 最初の要素 */;

    // ... 式 ...
}
D

foreachをサポートする必要があるユーザー定義型は、前のコードの3つのセクションに対応する3つのメンバー関数を提供することができる。ループが終了したかどうかを判断し、先頭要素をスキップし、先頭要素へのアクセスを提供する。

これら3つのメンバー関数は、それぞれemptypopFrontfrontと名付ける必要がある。コンパイラによって生成されるコードは、これらの関数を呼び出す。

for ( ; !myObject.empty(); myObject.popFront()) {

    auto element = myObject.front();

    // ... 式 ...
}
D

この3つの関数は、次の期待どおりに動作する必要がある。

これらのメンバー関数を定義する型は、foreachで使用できる。

特定の範囲内の数値を生成するstructを定義する。Dの数値範囲とスライスインデックスと一貫性を保つため、最後の数値は有効な数値の範囲外にする。これらの要件を満たす場合、以下のstructはDの数値範囲と完全に同じ動作をする:

struct NumberRange {
    int begin;
    int end;

    invariant() {
        // beginがendより大きい場合、バグが発生する
        assert(begin <= end);
    }

    bool empty() const {
        // beginがendと等しい場合、範囲が消費される
        return begin == end;
    }

    void popFront() {
        // 最初の要素をスキップするには、
        // 範囲の開始位置を加算する
        ++begin;
    }

    int front() const {
        // 最初の要素は、範囲の開始位置にある要素だ
        return begin;
    }
}
D

注釈:この実装の安全性は、単一のinvariantブロックのみに依存している。frontおよびpopFrontに追加のチェックを追加して、範囲が空の場合にこれらの関数が決して呼び出されないようにすることができる。

そのstructのオブジェクトは、foreachと一緒に使用できる:

foreach (element; NumberRange(3, 7)) {
    write(element, ' ');
}
D

foreachは、バックグラウンドでこれら3つの関数を使用し、empty()trueを返すまで反復処理を行う。

3456
std.range.retro 逆順で反復処理を行う

std.rangeモジュールには、多くの範囲アルゴリズムが含まれている。retroは、そのうちの1つで、範囲を逆順に反復する。この関数には、2つの追加の範囲メンバー関数が必要だ。

ただし、逆の反復とは直接関係はないが、retroがこれらの関数を考慮するには、もう1つの関数を定義する必要がある。

これらのメンバー関数については、後で範囲の章で詳しく説明する。

この3つの追加のメンバ関数は、NumberRangeに対して簡単に定義できる。

struct NumberRange {
// ...

    void popBack() {
        // 最後の要素をスキップするには、
        // 範囲の終了位置を1減算する。
        --end;
    }

    int back() const {
        // 'end'の値は範囲外であるため、
        // 最後の要素はそれより1少ない
        return end - 1;
    }

    NumberRange save() const {
        // この構造体オブジェクトのコピーを返す
        return this;
    }
}
D

この型のオブジェクトは、retroで使用できるようになった。

import std.range;

// ...

    foreach (element; NumberRange(3, 7).retro) {
        write(element, ' ');
    }
D

プログラムの出力は逆順になった:

6543
foreach opApplyおよびopApplyReverseメンバー関数によるサポート

このセクションでopApplyについて述べたことは、opApplyReverseにも適用される。opApplyReverseは、foreach_reverseループ内のオブジェクトの動作を定義するためのものだ。

上記のメンバー関数を使用すると、オブジェクトを範囲として使用することができる。この方法は、範囲を反復処理する合理的な方法が1つしかない場合に適している。例えば、Students型の個々の学生にアクセスするのは簡単だ。

一方、同じオブジェクトをさまざまな方法で反復処理するほうが理にかなっている場合もある。これは、値のみ、またはキーと値の両方にアクセスできる連想配列からわかる。

string[string] dictionary;    // 英語からトルコ語へ

// ...

foreach (inTurkish; dictionary) {
    // ... 値のみ ...
}

foreach (inEnglish, inTurkish; dictionary) {
    // ... キーと値 ...
}
D

opApply これにより、foreachで、さまざまな、時にはより複雑な方法で、ユーザー定義型を使用することができる。opApplyの定義方法を学ぶ前に、foreachによってそれが自動的に呼び出される仕組みを理解しておこう。

プログラムの実行は、foreachブロック内の式と、opApply()関数内の式とを交互に繰り返す。まず、opApply()メンバー関数が呼び出され、次にopApplyforeachブロックを明示的に呼び出す。この繰り返しは、ループが最終的に終了するまで続く。このプロセスは、まもなく説明する規約に基づいている。

まず、foreachループの構造をもう一度確認しよう:

// プログラム作成者が記述したループ:

    foreach (/* ループ変数 */; myObject) {
        // ... foreachブロック内の式 ...
    }
D

ループ変数と一致するopApply()メンバー関数がある場合、foreachブロックはデリゲートとなり、opApply()に渡される。

したがって、上記のループは裏で以下のコードに変換される。デリゲート本体を定義する波括弧がハイライトされている:

// コンパイラがバックグラウンドで生成するコード:

    myObject.opApply(delegate int(/* ループ変数 */) {
        // ... foreachブロック内の式 ...
        return hasBeenTerminated;
    });
D

つまり、foreachループは、opApply()に渡されるdelegateに置き換えられる。例を示す前に、opApply()がこの規約で遵守しなければならない要件と期待事項を示す。

  1. foreachループの本体はデリゲート本体になる。opApplyは、各反復でこのデリゲートを呼び出す必要がある。
  2. ループ変数はデリゲートパラメーターになる。opApply()はこれらのパラメーターをrefとして定義する必要がある。(変数はrefキーワードなしで定義することもできるが、その場合、要素を参照して反復処理できなくなる。)
  3. デリゲートは、int型でなければならない。それに応じて、コンパイラはデリゲートの最後にreturn文を挿入し、ループが(break文またはreturn文によって)終了したかどうかを判断する。戻り値が0の場合、反復は継続し、それ以外の場合は終了する。
  4. 実際の反復はopApply()内で実行される。
  5. opApply()は、デリゲートが返すのと同じ値を返す必要がある。

以下の定義は、その規約に従って実装されたNumberRangeの定義だ:

struct NumberRange {
    int begin;
    int end;
                         //    (2)       (1)
    int opApply(int delegate(ref int) operations) const {
        int result = 0;

        for (int number = begin; number != end; ++number) { // (4)
            result = operations(number);  // (1)

            if (result) {
                break;                    // (3)
            }
        }

        return result;                    // (5)
    }
}
D

このNumberRangeの定義は、foreachとまったく同じように使用できる:

foreach (element; NumberRange(3, 7)) {
    write(element, ' ');
}
D

出力は、rangeメンバー関数によって生成された出力と同じである。

3456
opApplyをオーバーロードして異なる方法で反復処理を行う

異なる型のデリゲートを受け取るopApply()のオーバーロードを定義することで、同じオブジェクトをさまざまな方法で反復処理することができる。コンパイラは、特定のループ変数のセットに一致するオーバーロードを呼び出す。

例として、NumberRangeを2つのループ変数でも反復できるようにしよう。

foreach (first, second; NumberRange(0, 15)) {
    writef("%s,%s ", first, second);
}
D

これは、連想配列がキーと値の両方で反復される方法と似ていることに注意。

この例では、NumberRangeオブジェクトが2つの変数によって反復処理される場合、2つの連続した値を提供し、その値を5ずつ任意に増加させることを要求しよう。したがって、上記のループは次の出力を生成するはずだ。

0,15,610,11

これは、2つのパラメータを取るデリゲートを受け取るopApply()の追加の定義によって実現される。opApply()は、2つの値でそのデリゲートを呼び出す必要がある。

int opApply(int delegate(ref int, ref int) dg) const {
    int result = 0;

    for (int i = begin; (i + 1) < end; i += 5) {
        int first = i;
        int second = i + 1;

        result = dg(first, second);

        if (result) {
            break;
        }
    }

    return result;
}
D

ループ変数が2つある場合、このオーバーロードのopApply()が呼び出される。

opApply()のオーバーロードは、必要に応じて複数定義できる。

どのオーバーロードを選択すべきかをコンパイラにヒントを与えることは可能であり、場合によっては必要になる。これは、ループ変数の型を明示的に指定することで行う。

例えば、教師と生徒を別々に反復処理するSchool型があるとする。

class School {
    int opApply(int delegate(ref Student) dg) const {
        // ...
    }

    int opApply(int delegate(ref Teacher) dg) const {
        // ...
    }
}
D

希望するオーバーロードを指定するには、ループ変数を明示的に指定する必要がある:

foreach (Student student; school) {
    // ...
}

foreach (Teacher teacher; school) {
    // ...
}
D
ループカウンター

スライスの便利なループカウンタは、他の型では自動的に機能しない。ループカウンタは、foreachがrangeメンバー関数によってサポートされているか、opApplyオーバーロードによってサポートされているかによって、ユーザー定義型でさまざまな方法で実現できる。

range関数によるループカウンタ

foreachがrangeメンバー関数によってサポートされている場合、std.rangeモジュールからenumerateを使用するだけでループカウンタを実現できる。

import std.range;

// ...

    foreach (i, element; NumberRange(42, 47).enumerate) {
        writefln("%s: %s", i, element);
    }
D

enumerateは、デフォルトで0から連続した数値を生成する範囲である。enumerateは、適用された範囲の要素と各数値を対応付ける。その結果、enumerateが生成する数値と実際の範囲(この場合はNumberRange)の要素が、ループ変数として同期して表示される:

042
143
244
345
446
ループカウンターがopApply

一方、foreachopApply()によってサポートされている場合は、ループカウンタを、size_tという型で、デリゲート別のパラメータとして定義する必要がある。これを、色付き多角形を表すstructで見てみよう。

上記で既に説明したように、この多角形のポイントにアクセスするopApply()は、カウンターなしで次のように実装できる。

import std.stdio;

enum Color { blue, green, red }

struct Point {
    int x;
    int y;
}

struct Polygon {
    Color color;
    Point[] points;

    int opApply(int delegate(ref const(Point)) dg) const {
        int result = 0;

        foreach (point; points) {
            result = dg(point);

            if (result) {
                break;
            }
        }

        return result;
    }
}

void main() {
    auto polygon = Polygon(Color.blue,
                           [ Point(0, 0), Point(1, 1) ] );

    foreach (point; polygon) {
        writeln(point);
    }
}
D
foreach_opapply.1

opApply()自体は、foreachループによって実装されていることに注意。その結果、main()内のforeachは、pointsメンバーに対するforeachを間接的に使用することになる。

また、デリゲートパラメータの型はref const(Point)であることに注意しよう。これは、opApply()のこの定義では、ポリゴンのPoint要素を変更できないことを意味する。ユーザーコードで要素を変更できるようにするには、opApply()関数自体とデリゲートパラメータの両方を、const指定子なしで定義する必要がある。

出力:

const(Point)(0, 0)
const(Point)(1, 1)

当然ながら、このPolygonの定義をループカウンターと組み合わせて使用すると、コンパイルエラーが発生する:

foreach (i, point; polygon) {    // ← コンパイルエラー
    writefln("%s: %s", i, point);
}
D

コンパイルエラー:

エラー: foreachの引数の型を一意に推測できない
Undefined

これを機能させるには、カウンターをサポートする別のopApply()オーバーロードを定義する必要がある:

int opApply(int delegate(ref size_t,
                         ref const(Point)) dg) const {
    int result = 0;

    foreach (i, point; points) {
        result = dg(i, point);

        if (result) {
            break;
        }
    }

    return result;
}
D

今回は、foreach変数が新しいopApply()オーバーロードに一致し、プログラムは期待される出力を表示する:

0const(Point)(0, 0)
1const(Point)(1, 1)

このopApply()の実装は、pointsメンバー上の自動カウンタを利用していることに注意。(デリゲート変数はref size_tとして定義されているが、main()内のforeachループは、points上のカウンタ変数を変更することはできない。

必要に応じて、ループカウンタを明示的に定義して加算することもできる。例えば、次のopApply()while文によって実装されているため、カウンタ用に別の変数を定義する必要がある。

int opApply(int delegate(ref size_t,
                         ref const(Point)) dg) const {
    int result = 0;
    bool isDone = false;

    size_t counter = 0;
    while (!isDone) {
        // ...

        result = dg(counter, nextElement);

        if (result) {
            break;
        }

        ++counter;
    }

    return result;
}
D
警告: 反復処理中にコレクションは変更されてはならない

反復処理のサポートが範囲メンバー関数によって提供されているか、opApply()関数によって提供されているかに関係なく、コレクション自体は変更してはならない。新しい要素をコンテナに追加したり、既存の要素を削除したりしてはならない。(既存の要素を変更することは許可されている。)

そうしない場合は、未定義の動作になる。

演習
  1. NumberRangeと同様の動作をし、ステップサイズを指定できるstructを設計しよう。ステップサイズは3番目のメンバー変数として指定できる:
    foreach (element; NumberRange(0, 10, 2)) {
        write(element, ' ');
    }
    D

    上記のコードの期待される出力は、0から10までの2番目の数である:

    0268
  2. テキストで説明されたSchoolクラスを、foreach変数に応じて学生または教師にアクセスを許可するように実装しよう。