いつもお世話になっている @Fujiwo さんより、「ForEachはLINQじゃない」とのご指摘を頂き、記事タイトルを「LINQのForEach」から「ListのForEach」に変え、記事中の表現も少し修正しました。ご指摘ありがとうございます!
LINQを使っていると、とにかく全てをLINQで処理したくなってくる病に侵されます。
LINQの大きな利点の一つは、変数のスコープがLINQの中で閉じている為、余計なローカル変数を増やさず、従って管理する対象が大幅に減ってバグの混入も減らせるという事だと思います。
データはまずリストとして定義し、LINQの便利なメソッドを使って「絞り込み(抽出)」「整形(射影)」「集計」等をし、最後にForEachで出力なり処理なりをすると、とってもスッキリ書けるし、LINQのライブラリに殆ど任せているので、素早くバグの少ないコーディングが可能です。
精神衛生上も、とってもスッキリ! 先日、PL/SQLで動的SQLを作る2000行のソースコードを書いたのですが、「これLINQ使ったらどんだけ楽になるんだろ…」と思ってしまってストレスがマッハでした。
ただ、そんなLINQでの処理にも、ちょっとだけ面倒な点があるんですよね。
それは、LINQで処理した後、**ForEachの中でIndexが取れない!**という点。
次のプログラムは「カレントフォルダの画像ファイルを更新日付順にソートして、ファイル名を連番で振り直す」というものです。
Dim folder As New IO.DirectoryInfo(".\")
Dim count As Integer = 0
folder.GetFiles().
Where(Function(file) {".jpg", ".png", "bmp"}.Contains(file.Extension.ToLower())).
OrderBy(Function(file) file.LastWriteTime).
ToList().
ForEach(
Sub(file)
count += 1
System.IO.File.Move(file.FullName, IO.Path.Combine(folder.FullName, count.ToString("0000") + file.Extension))
End Sub
)
見えますか、この汚らわしい「count」変数が…。せっかくの美しい(別に美しくもないですが)LINQが、このcount変数のせいで台無しです。いや、count変数に別に罪はないんですけど。悪いのは私です。
それもこれも、ForEachでとってこれるのは「リストの要素」だけで、Indexが取れないからなんです。でも、Selectにはあるんですよね、Indexを渡してくれるオーバーライドが。なので、ForEachでIndexを取得したければ、一般的にはこれを使ってこんな風に書けと言われています。
folder.GetFiles().
Where(Function(file) {".jpg", ".png", "bmp"}.Contains(file.Extension.ToLower())).
OrderBy(Function(file) file.LastWriteTime).
Select(Function(file, index) New With {Key .Index = index, .File = file}).
ToList().
ForEach(
Sub(item)
System.IO.File.Move(item.File.FullName, IO.Path.Combine(folder.FullName, item.Index.ToString("0000") + item.File.Extension))
End Sub
)
何をしているかというと、4行目の「Select」で、itemと一緒にindexを受け取れるので、それをIndexとFileというプロパティを持つ匿名型のオブジェクトに変換してるんです。
で、それをForEachで受け取れば、item.Fileとitem.Indexとしてそれぞれ取り出せる、という寸法ですね。
でも、その為にいちいちこんな書き方をしなければならないのは、美しくありません。別の言い方をすれば、コーディング量が増え、その分バグの混入可能性が増え、ソースコードの可読性も下がります。
そこで、この部分を勝手にやってくれる拡張メソッドを勝手に作ってしまいます。
Module MyLinqExtension
<System.Runtime.CompilerServices.Extension()>
Sub ForEach(Of T)(list As List(Of T), proc As Action(Of T, Integer))
list.Select(Function(item, index) New With {.Item = item, Key .Index = index}).
ToList().
ForEach(Sub(item) proc(item.Item, item.Index))
End Sub
End Module
使うときはこんな感じです。
Dim list As New List(Of String) From {"a", "b", "c"}
list.ForEach(Sub(item, index) Debug.WriteLine("{0}:{1}", index, item))
もしIEnumerableでも使いたいなら、ForEachのlistパラメータの型をList(Of T)っではなくIEnumerable(Of T)にすれば良いでしょう。
とにかくこのForEachを使うと、こんな風に書けるようになります。
folder.GetFiles().
Where(Function(file) {".jpg", ".png", "bmp"}.Contains(file.Extension.ToLower())).
OrderBy(Function(file) file.LastWriteTime).
ToList().
ForEach(
Sub(file, index)
System.IO.File.Move(file.FullName, IO.Path.Combine(folder.FullName, index.ToString("0000") + file.Extension))
End Sub
)
意図も明確になってコード量も減って、スッキリ!
恐らくですが、元のForEachにindex渡しのオーバーライドが無いのは、ForEachはあくまでもイテレーターパターンの実装であり、要素以外の余計な内部構造情報(この場合はindex)を渡すのはパターンに反するという事なのだろうと思います。
ただ、便利なものは便利なので、できればMicrosoftさん、このForEachの実装、よろしくお願い致します。
もしくは、普通のFor Each構文でも、indexを簡単に取れるようになると良いなぁ。JavaScriptのArray.forEachには、ありますよね。