C#のforeachの仕組み(2)にコメントしようかと思ったけどこっちへ
#最も原始的なループ処理
int counter = 0;
goto loop_check;
loop_start:
//処理
counter++;
loop_check:
if (counter < 5) goto loop_start;
CPU命令(機械語)やアセンブラで書いていた時代のスタイル。
今でもCPU命令レベルではこれに近いことをやっています。
#for文の登場
ループ処理は頻繁に使う割には原始的スタイルだとけっこうな長さを食うため、「初期化」「終了条件」「次へ進む処理」をまとめて書くfor文が考案されました。
たぶんFORTRANあたりから。
#スクリプトでのループ処理
foreach(のようなもの)はスクリプトが発祥だったと思います。1
- リスト(配列)から一つずつ内容を取り出して順次処理できると便利
- 何回繰り返すかは「リストの要素数」で決めればいい、「終了条件」を別に書くのは冗長
- 「次へ進む処理」は「次の要素をリストから取り出す」に決まってるからこれも省略できる
- そしたらループカウンタも明示的に用意する必要はないよね(内部的にはカウンタ持ってるんだけど)2
昔は処理が遅かったこともあって、スクリプトはインタプリタ処理なので速度が遅いため簡略化した記述が好まれ「リストを渡せばリストの要素数分だけループする」というカウンタ変数や終了条件を省いた記述法が生まれたのでしょう。
#プログラミング言語にもforeachを導入
普通のプログラミング言語でも「配列の中身を一つずつ取り出して処理」というのはよくあります。
ループカウンタは配列のインデックスと終了判定にしか使っていないことも珍しくないです。
なのでそういう処理のための省略記法としてforを簡略化したforeachを導入する言語も出てきました。
#イテレータ誕生
「複数の要素を格納したデータ」は配列が多いけど他の形もあります。
ディクショナリ(ハッシュ)とかスタックとか二分木とかリングバッファとか。
そういうものに対しても「格納された要素を一回ずつ取り出し、全部の要素を取り出し終わったら終了」という統一された手法が使えると便利。
ループカウンタのかわりにイテレータを使うことで同じようにfor文のループで処理できます。
#enumeratorはforeach用のイテレータみたいなもの(だと思う)
「イテレータでぐるぐる回すfor文」というのはだいたい同じような書き方になります。
「次へ進む処理」と「終了条件」はイテレータが処理するので、for文はイテレータが「もうないよ」と言うまで「次の要素を渡して」というお願いを繰り返すだけだから。
だったらforeach文に要素のリストを渡すと勝手にイテレータを作り、繰り返し処理のたびにイテレータに次の要素をもらえばいいだけです。すっきり!
でもこの時点でループカウンタ/イテレータの存在はソースコードから消えてしまうので、古典的なループ処理に慣れている人は「今どこまで処理したかを覚えてる変数がないのになんで『次の要素』がどれなのか把握できるの?」と戸惑います。
私も最初のころは気になってました。単に隠してるだけだったというオチ。
#そしてLINQ to Objectsに
リスト(コレクション)から内容を一つずつ取り出してそれに対して処理を行うなら、SQLクエリの結果リストと同じように処理できる、どんな形で格納されているかは気にする必要はない、必要なのはデータ集合から一つずつ要素を取り出し、全部取り出し終わったら終わることだけ。
つまりSQL文がテーブルの内容やサブクエリの結果データを集計したり加工して別の結果データを作るように、テーブルをコレクションに置き換えてもよく似た処理ができる、元データとなるコレクションに必要なのはアクセスするためのenumeratorを持っていることだけ。
という発想からLINQ to Objectsは生まれたんじゃないかなと思ってます。
#(おまけ)for文は死なず
foreachでかなり便利になってfor文を使う機会は減ったけど、foreachやLinqは「元となるデータ集合ありき」なループ処理です。
ループカウンタを「データ集合のインデックス」以外の目的でも使う場合は、明示的な変数として見えていないと困ることもよくあります。
同じようなことをするのにたくさんの方法があるのは混乱するしめんどくさいですが、上手に使い分けたいですね。
#(おまけその2)foreachとほぼ等価なfor文
foreach (string s in collection)
IEnumerator<string> e;
bool isContinue;
string s;
for (new Action(() => { e = collection.GetEnumerator(); isContinue = e.MoveNext(); if (isContinue) s = e.Current; })() ;
isContinue ;
new Action(() => { isContinue = e.MoveNext(); if (isContinue) s = e.Current; })())
//別の書き方
for (e = collection.GetEnumerator() ;
new Func<bool>(() => { isContinue = e.MoveNext(); if (isContinue) s = e.Current; return isContinue; })() ;)
//enumeratorのDisposeなど知るか