foreachループ

Dで最もよく使われる文の一つは、foreachループだ。これは、コンテナ(または範囲)のすべての要素に同じ操作を適用するために使う。

コンテナの要素に操作を適用することは、プログラミングでは非常に一般的である。forループの章では、forループで、各反復で加算されるインデックス値によって配列の要素にアクセスすることを学んだ。

for (int i = 0; i != array.length; ++i) {
    writeln(array[i]);
}
D

すべての要素を反復処理する手順は次の通りだ:

foreachは本質的に同じ動作だが、これらのステップを自動的に処理することでコードを簡素化する:

foreach (element; array) {
    writeln(element);
}
D

foreachの威力の部分は、コンテナの型に関係なく同じように使用できることにある。前の章で見たように、forループで連想配列の値を反復処理する1つの方法は、まず配列の.valuesプロパティを呼び出すことだ。

auto values = aa.values;
for (int i = 0; i != values.length; ++i) {
    writeln(values[i]);
}
D

foreach連想配列に対して特別な処理は必要ない。配列と同じように使用できる:

foreach (value; aa) {
    writeln(value);
}
D
foreachの構文

foreachは3つのセクションから成る:

foreach (names; container_or_range) {
    operations
}
D
continuebreak

これらのキーワードは、forループでの意味と同じだ:continueは、現在の要素の残りの操作を完了する前に次の反復に移動し、breakはループを完全に終了する。

foreach配列の場合

foreachを単純な配列で使用し、namesセクションに1つの名前が指定されている場合、その名前は各反復の要素の値を表す。

foreach (element; array) {
    writeln(element);
}
D

名前セクションに2つの名前が指定されている場合、それらはそれぞれ自動カウンタと要素の値を表す。

foreach (i, element; array) {
    writeln(i, ": ", element);
}
D

カウンターはforeachによって自動的に加算される。任意の名前を付けることができるが、iは自動カウンターとして非常に一般的な名前だ。

foreach文字列とstd.range.stride

文字列は文字の配列であるため、foreachは文字列に対して配列と同じように動作する: 1つの名前は文字を指し、2つの名前はカウンターと文字をそれぞれ指す:

foreach (c; "hello") {
    writeln(c);
}

foreach (i, c; "hello") {
    writeln(i, ": ", c);
}
D

ただし、UTFコード単位であるため、charwcharはUnicodeコードポイントではなく、UTFコード単位を反復処理する:

foreach (i, code; "abcçd") {
    writeln(i, ": ", code);
}
D

ç を構成する2つのUTF-8コード単位は、別々の要素としてアクセスされる:

0a
1b
2c
3
4
5d

foreachループで文字列のUnicode文字を反復処理する1つの方法は、std.rangeモジュールにあるstrideを使うことだ。strideは、文字列をUnicode文字で構成されるコンテナとして表現する。2番目のパラメータは、文字をストライドするステップ数だ。

import std.range;

// ...

    foreach (c; stride("abcçd", 1)) {
        writeln(c);
    }
D

文字列の文字型に関係なく、strideは常にその要素をUnicode文字として表現する。

a
b
c
ç
d

このループに自動カウンタを含めることができなかった理由については、以下で説明する。

foreach連想配列

連想配列でforeachを使用する場合、1つの名前は値を指し、2つの名前はそれぞれキーと値を指す。

foreach (value; aa) {
    writeln(value);
}

foreach (key, value; aa) {
    writeln(key, ": ", value);
}
D

連想配列は、キーと値を範囲として提供することもできる。範囲については、後の章で説明する。.byKey.byValue、および.byKeyValueは、foreachループ以外のコンテキストでも役立つ、効率的な範囲オブジェクトを返す。

.byValueは、上記の通常の値反復よりも、foreachループでは何の利点もない。一方、.byKeyは、連想配列のキーだけを反復する唯一の効率的な方法である。

foreach (key; aa.byKey) {
    writeln(key);
}
D

.byKeyValueは、タプルに似た変数を通じて各キーと値の要素を提供する。キーと値は、その変数の.keyおよび.valueプロパティを通じて個別にアクセスできる。

foreach (element; aa.byKeyValue) {
    writefln("The value for key %s is %s",
             element.key, element.value);
}
D
foreach数値範囲

数値範囲は、スライスとその他の配列機能の章で既に説明した。数値範囲は、container_or_rangeセクションで指定できる:

foreach (number; 10..15) {
    writeln(number);
}
D

10は範囲に含まれるが、15は含まれないことに注意。

foreach構造体、クラス、および範囲

foreachは、foreachループで独自の反復を定義するユーザー定義型のオブジェクトにも使用できる。構造体およびクラスは、opApply()メンバー関数、または一連のrangeメンバー関数によって、foreach反復をサポートしている。これらの機能については、後の章で説明する。

カウンターは配列の場合のみ自動である

自動カウンターは、配列を反復処理する場合にのみ提供される。他のコンテナには2つのオプションがある

このような変数は、特定の条件もカウントする場合にも必要である。例えば、次のコードは、10で割り切れる値のみをカウントする。

import std.stdio;

void main() {
    auto numbers = [ 1, 0, 15, 10, 3, 5, 20, 30 ];

    size_t count = 0;
    foreach (number; numbers) {
        if ((number % 10) == 0) {
            ++count;
            write(count);

        } else {
            write(' ');
        }

        writeln(": ", number);
    }
}

出力:


 : 1
1: 0
 : 15
2: 10
 : 3
 : 5
3: 20
4: 30
要素のコピーではなく、要素そのもの

foreachループは通常、コンテナに格納されている実際の要素ではなく、要素のコピーを提供する。これはバグの原因となる可能性がある。

この例を見るために、配列の要素の値を2倍にする次のプログラムを見てみよう。

import std.stdio;

void main() {
    double[] numbers = [ 1.2, 3.4, 5.6 ];

    writefln("Before: %s", numbers);

    foreach (number; numbers) {
        number *= 2;
    }

    writefln("After : %s", numbers);
}

プログラムの出力は、foreachの本体内で各要素に代入された値が、コンテナ内の要素に影響を与えないことを示している:

[1.2, 3.4, 5.6]
[1.2, 3.4, 5.6]

これは、numberが配列の実際の要素ではなく、各要素のコピーだからだ。実際の要素を操作するには、refキーワードを使用して、実際の要素への参照として名前を定義する必要がある:

foreach (ref number; numbers) {
    number *= 2;
}
D

新しい出力では、代入が配列の実際の要素を変更することがわかる:

[1.2, 3.4, 5.6]
[2.4, 6.8, 11.2]

refキーワードは、各反復でnumberを実際の要素の別名にする。その結果、numberを通じた変更は、コンテナの実際の要素を変更する。

コンテナの整合性は保持されなければならない

ref変数を使用してコンテナの要素を変更することは問題ないが、コンテナの反復実行中にコンテナの構造を変更してはならない。例えば、foreachループの実行中に、コンテナから要素を削除したり、要素を追加したりしてはならない。

このような変更は、ループの反復処理の内部動作を混乱させ、不正なプログラム状態を引き起こす可能性がある。

foreach_reverse 逆方向に反復する

foreach_reverse foreachと同じように機能するが、逆方向に反復する:

auto container = [ 1, 2, 3 ];

foreach_reverse (element; container) {
    writefln("%s ", element);
}
D

出力:

3 
2 
1 

foreach_reverseは、range関数retro()と同じ目的を果たすため、あまり使用されない。retro()については、後の章で説明する。

演習

連想配列は、キーから値へのマッピングを提供することを知っている。このマッピングは単方向で、値はキーによってアクセスできるが、その逆は不可能だ。

次のような連想配列が既に存在すると仮定する:

string[int] names = [ 1:"one", 7:"seven", 20:"twenty" ];
D

この連想配列とforeachループを使用して、valuesという名前の別の連想配列を埋めてみよう。新しい連想配列は、名前に対応する値を提供する必要がある。例えば、次の行は20を出力する必要がある。

writeln(values["twenty"]);
D