構造体およびクラスを使用した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
変数に応じて学生または教師にアクセスを許可するように実装しよう。