構造体およびクラスを使用したforeach
foreachループの章で覚えているように、foreachの動作、およびそれがサポートするループ変数の型と数は、コレクションの種類によって異なる。スライスの場合、foreachは、カウンタの有無にかかわらず要素へのアクセスを提供する。連想配列の場合、キーの有無にかかわらず値へのアクセスを提供する。数値範囲の場合、個々の値へのアクセスを提供する。ライブラリ型の場合、foreachはその型に固有の動作をする。例えば、Fileの場合、ファイルの各行を提供する。
ユーザー定義型についても、foreachの動作を定義することができる。このサポートを提供するには2つの方法がある。
- 範囲メンバー関数を定義する。これにより、ユーザー定義型を他の範囲アルゴリズムでも使用できるようになる。
- 1つ以上の
opApplyメンバー関数を定義する
2つの方法のうち、opApplyが優先される: opApplyが定義されている場合、コンパイラはopApplyを使用する。それ以外の場合、rangeメンバー関数が考慮される。ただし、ほとんどの場合、rangeメンバー関数で十分であり、より簡単で、より便利だ。
foreachすべての型でサポートする必要はない。オブジェクトを反復処理することは、そのオブジェクトがコレクションの概念を定義している場合にのみ意味がある。
例えば、学生を表すクラスを反復処理する場合、foreachがどの要素を提供すべきかは明確ではないため、このクラスはforeachをまったくサポートしないほうがよい。一方、Studentが成績のコレクションであり、foreachが学生の個々の成績を提供することが設計上必要な場合もある。
どの型がこのサポートを提供すべきか、またその方法は、プログラムの設計によって異なる。
foreach範囲メンバー関数によるサポート
foreachはforと非常に似ているが、forよりも有用で安全であることは知っている。次のループを考えてみよう:
裏では、コンパイラはforeachループをforループに再記述し、おおむね以下のコードと等価なものにする:
foreachをサポートする必要があるユーザー定義型は、前のコードの3つのセクションに対応する3つのメンバー関数を提供することができる。ループが終了したかどうかを判断し、先頭要素をスキップし、先頭要素へのアクセスを提供する。
これら3つのメンバー関数は、それぞれempty、popFront、frontと名付ける必要がある。コンパイラによって生成されるコードは、これらの関数を呼び出す。
この3つの関数は、次の期待どおりに動作する必要がある。
.empty()ループが終了した場合、trueを返す。それ以外の場合、falseを返す。.popFront()次の要素に移動する(つまり、前の要素をスキップする).front()フロント要素を返す
これらのメンバー関数を定義する型は、foreachで使用できる。
例
特定の範囲内の数値を生成するstructを定義する。Dの数値範囲とスライスインデックスと一貫性を保つため、最後の数値は有効な数値の範囲外にする。これらの要件を満たす場合、以下のstructはDの数値範囲と完全に同じ動作をする:
注釈:この実装の安全性は、単一のinvariantブロックのみに依存している。frontおよびpopFrontに追加のチェックを追加して、範囲が空の場合にこれらの関数が決して呼び出されないようにすることができる。
そのstructのオブジェクトは、foreachと一緒に使用できる:
foreachは、バックグラウンドでこれら3つの関数を使用し、empty()がtrueを返すまで反復処理を行う。
| 3 | 4 | 5 | 6 |
std.range.retro 逆順で反復処理を行う
std.rangeモジュールには、多くの範囲アルゴリズムが含まれている。retroは、そのうちの1つで、範囲を逆順に反復する。この関数には、2つの追加の範囲メンバー関数が必要だ。
.popBack()最後の要素の前の要素に移動する(最後の要素をスキップする).back()最後の要素を返す
ただし、逆の反復とは直接関係はないが、retroがこれらの関数を考慮するには、もう1つの関数を定義する必要がある。
.save()このオブジェクトのコピーを返す
これらのメンバー関数については、後で範囲の章で詳しく説明する。
この3つの追加のメンバ関数は、NumberRangeに対して簡単に定義できる。
この型のオブジェクトは、retroで使用できるようになった。
プログラムの出力は逆順になった:
| 6 | 5 | 4 | 3 |
foreach opApplyおよびopApplyReverseメンバー関数によるサポート
このセクションでopApplyについて述べたことは、opApplyReverseにも適用される。opApplyReverseは、foreach_reverseループ内のオブジェクトの動作を定義するためのものだ。
上記のメンバー関数を使用すると、オブジェクトを範囲として使用することができる。この方法は、範囲を反復処理する合理的な方法が1つしかない場合に適している。例えば、Students型の個々の学生にアクセスするのは簡単だ。
一方、同じオブジェクトをさまざまな方法で反復処理するほうが理にかなっている場合もある。これは、値のみ、またはキーと値の両方にアクセスできる連想配列からわかる。
opApply これにより、foreachで、さまざまな、時にはより複雑な方法で、ユーザー定義型を使用することができる。opApplyの定義方法を学ぶ前に、foreachによってそれが自動的に呼び出される仕組みを理解しておこう。
プログラムの実行は、foreachブロック内の式と、opApply()関数内の式とを交互に繰り返す。まず、opApply()メンバー関数が呼び出され、次にopApplyがforeachブロックを明示的に呼び出す。この繰り返しは、ループが最終的に終了するまで続く。このプロセスは、まもなく説明する規約に基づいている。
まず、foreachループの構造をもう一度確認しよう:
ループ変数と一致するopApply()メンバー関数がある場合、foreachブロックはデリゲートとなり、opApply()に渡される。
したがって、上記のループは裏で以下のコードに変換される。デリゲート本体を定義する波括弧がハイライトされている:
つまり、foreachループは、opApply()に渡されるdelegateに置き換えられる。例を示す前に、opApply()がこの規約で遵守しなければならない要件と期待事項を示す。
foreachループの本体はデリゲート本体になる。opApplyは、各反復でこのデリゲートを呼び出す必要がある。- ループ変数はデリゲートパラメーターになる。
opApply()はこれらのパラメーターをrefとして定義する必要がある。(変数はrefキーワードなしで定義することもできるが、その場合、要素を参照して反復処理できなくなる。) - デリゲートは、
int型でなければならない。それに応じて、コンパイラはデリゲートの最後にreturn文を挿入し、ループが(break文またはreturn文によって)終了したかどうかを判断する。戻り値が0の場合、反復は継続し、それ以外の場合は終了する。 - 実際の反復は
opApply()内で実行される。 opApply()は、デリゲートが返すのと同じ値を返す必要がある。
以下の定義は、その規約に従って実装されたNumberRangeの定義だ:
このNumberRangeの定義は、foreachとまったく同じように使用できる:
出力は、rangeメンバー関数によって生成された出力と同じである。
| 3 | 4 | 5 | 6 |
opApplyをオーバーロードして異なる方法で反復処理を行う
異なる型のデリゲートを受け取るopApply()のオーバーロードを定義することで、同じオブジェクトをさまざまな方法で反復処理することができる。コンパイラは、特定のループ変数のセットに一致するオーバーロードを呼び出す。
例として、NumberRangeを2つのループ変数でも反復できるようにしよう。
これは、連想配列がキーと値の両方で反復される方法と似ていることに注意。
この例では、NumberRangeオブジェクトが2つの変数によって反復処理される場合、2つの連続した値を提供し、その値を5ずつ任意に増加させることを要求しよう。したがって、上記のループは次の出力を生成するはずだ。
| 0,1 | 5,6 | 10,11 |
これは、2つのパラメータを取るデリゲートを受け取るopApply()の追加の定義によって実現される。opApply()は、2つの値でそのデリゲートを呼び出す必要がある。
ループ変数が2つある場合、このオーバーロードのopApply()が呼び出される。
opApply()のオーバーロードは、必要に応じて複数定義できる。
どのオーバーロードを選択すべきかをコンパイラにヒントを与えることは可能であり、場合によっては必要になる。これは、ループ変数の型を明示的に指定することで行う。
例えば、教師と生徒を別々に反復処理するSchool型があるとする。
希望するオーバーロードを指定するには、ループ変数を明示的に指定する必要がある:
ループカウンター
スライスの便利なループカウンタは、他の型では自動的に機能しない。ループカウンタは、foreachがrangeメンバー関数によってサポートされているか、opApplyオーバーロードによってサポートされているかによって、ユーザー定義型でさまざまな方法で実現できる。
range関数によるループカウンタ
foreachがrangeメンバー関数によってサポートされている場合、std.rangeモジュールからenumerateを使用するだけでループカウンタを実現できる。
enumerateは、デフォルトで0から連続した数値を生成する範囲である。enumerateは、適用された範囲の要素と各数値を対応付ける。その結果、enumerateが生成する数値と実際の範囲(この場合はNumberRange)の要素が、ループ変数として同期して表示される:
| 0 | 42 |
| 1 | 43 |
| 2 | 44 |
| 3 | 45 |
| 4 | 46 |
ループカウンターがopApply
一方、foreachがopApply()によってサポートされている場合は、ループカウンタを、size_tという型で、デリゲート別のパラメータとして定義する必要がある。これを、色付き多角形を表すstructで見てみよう。
上記で既に説明したように、この多角形のポイントにアクセスするopApply()は、カウンターなしで次のように実装できる。
opApply()自体は、foreachループによって実装されていることに注意。その結果、main()内のforeachは、pointsメンバーに対するforeachを間接的に使用することになる。
また、デリゲートパラメータの型はref const(Point)であることに注意しよう。これは、opApply()のこの定義では、ポリゴンのPoint要素を変更できないことを意味する。ユーザーコードで要素を変更できるようにするには、opApply()関数自体とデリゲートパラメータの両方を、const指定子なしで定義する必要がある。
出力:
const(Point)(0, 0)
const(Point)(1, 1)
当然ながら、このPolygonの定義をループカウンターと組み合わせて使用すると、コンパイルエラーが発生する:
コンパイルエラー:
これを機能させるには、カウンターをサポートする別のopApply()オーバーロードを定義する必要がある:
今回は、foreach変数が新しいopApply()オーバーロードに一致し、プログラムは期待される出力を表示する:
| 0 | const(Point)(0, 0) |
| 1 | const(Point)(1, 1) |
このopApply()の実装は、pointsメンバー上の自動カウンタを利用していることに注意。(デリゲート変数はref size_tとして定義されているが、main()内のforeachループは、points上のカウンタ変数を変更することはできない。
必要に応じて、ループカウンタを明示的に定義して加算することもできる。例えば、次のopApply()はwhile文によって実装されているため、カウンタ用に別の変数を定義する必要がある。
警告: 反復処理中にコレクションは変更されてはならない
反復処理のサポートが範囲メンバー関数によって提供されているか、opApply()関数によって提供されているかに関係なく、コレクション自体は変更してはならない。新しい要素をコンテナに追加したり、既存の要素を削除したりしてはならない。(既存の要素を変更することは許可されている。)
そうしない場合は、未定義の動作になる。
演習
NumberRangeと同様の動作をし、ステップサイズを指定できるstructを設計しよう。ステップサイズは3番目のメンバー変数として指定できる:上記のコードの期待される出力は、0から10までの2番目の数である:
0 2 6 8 - テキストで説明された
Schoolクラスを、foreach変数に応じて学生または教師にアクセスを許可するように実装しよう。