作成日: 2017/10/01, 更新日: 2017/10/01
IEnumerable と Yield と遅延評価(2017年10月版)
- LINQ のクエリ構文(SQL っぽい書き方のデータ編集・加工)やメソッド構文(メソッドをドットでつなげる書き方のデータ編集・加工)を使う際、 戻り値が IEnumerable(Of T) というジェネリックインターフェースになっています。LINQ が登場してからあちこちで見かけるインターフェースになりました。
- この IEnumerable(Of T) インターフェースを使い慣れるために、簡単ですが、扱い方を書き直しましたので共有します。
- 参考
Microsoft Docs Iterators (Visual Basic) https://docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/concepts/iterators
IEnumerable インターフェースの役割
- まずは以下を見てみましょう。
Public Sub Test1() ' IEnumerable インターフェースと IEnumerator インターフェースによって、 ' For Each ループが可能になる ' ジェネリック版の IEnumerable(Of T) と IEnumerator(Of T) インターフェースは、 ' IEnumerable インターフェースと IEnumerator インターフェースを継承しているため、 ' For Each ループが可能になる ' 配列やリストは IEnumerable(Of T) を継承しているので For Each できる Dim sources As String() = New String() {"a", "b", "c"} Dim items As IEnumerable(Of String) = sources ' 基底クラスに暗黙的なアップキャスト For Each item In items Console.WriteLine(item) Next ' 実際の処理は、IEnumerator インターフェースが担当している Dim enumerator = items.GetEnumerator() While enumerator.MoveNext() ' 次の値を確認、次の値があれば Current 更新(するだけ。次の次の値についてはまだ知らない) Console.WriteLine(enumerator.Current) End While ' IEnumerator を使っての遅延取得を自作してみる enumerator = Me.GetEnumerator() While enumerator.MoveNext() Console.WriteLine(enumerator.Current) End While ' For Each は IEnumerator インターフェースではできない 'For Each item In enumerator 'Next End Sub ' Yield を使う時は、 ' Iterator キーワードを付ける ' 戻り値は、IEnumerable, または IEnumerator, またはジェネリック版にする Private Iterator Function GetEnumerator() As IEnumerator(Of String) Yield "a" ' "a" を返却したら、ここで中断("b", "c" まで進んでいかない)、後で MoveNext メソッドが呼ばれたら次の行から再開する Yield "b" ' "b" を返却したら、ここで中断("c" まで進んでいかない)、後で MoveNext メソッドが呼ばれたら次の行から再開する Yield "c" ' "c" を返却したら、終わり ' ※中断というのは、プログラムの実行がここで停止してしまうのではなく、 ' 処理が進まないまま戻り値を返すという意味。ただし、ちゃんと次回のスタート位置を覚えている。 ' ループして返却する時も同じ。1つ返却したらループ内で中断。後で MoveNext メソッドが呼ばれたら次のループ処理から再開する Dim items = New List(Of String) From {"d", "e", "f"} For i As Integer = 0 To items.Count - 1 Yield items(i) Next End Function
自作クラスを For Each 対応する
- 自作クラスを対応させるには、IEnumerable インターフェースを実装することで For Each ができるようになります。
' 自作クラスに For Each できるように継承する Class DaysOfWeekList Implements IEnumerable(Of String) Private days As String() = New String() {"日", "月", "火", "水", "木", "金", "土"} Public Iterator Function GetEnumerator() As IEnumerator(Of String) Implements IEnumerable(Of String).GetEnumerator For i As Integer = 0 To days.Count - 1 Yield days(i) Next End Function ' 非ジェネリック版は Private なので非公開。ただしインターフェースなので実装しないといけない Private Iterator Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator Yield GetEnumerator() End Function End Class Public Sub Test2() Dim items1 = New DaysOfWeekList For Each item In items1 Next End Sub
Iterator メソッド内で例外エラーが発生すると、以降の処理が走らなくなる
- これは、サンプルを動かした方が手っ取り早いです。
' Iterator メソッド内で例外エラーが発生すると、以降の処理は走らなくなる Private Iterator Function GetEnumerable() As IEnumerable(Of Integer) Try Yield 1 Yield 2 Throw New Exception("例外エラーが発生すると、以降の Yield は無効になる") Yield 3 Yield 4 Catch ex As Exception Console.WriteLine(ex.Message) Finally Console.WriteLine("Finally は呼び出せる") End Try End Function Public Sub Test3() Dim items = GetEnumerable() For Each item In items Console.WriteLine(item) Next Console.WriteLine("完了") ' 1 ' 2 ' 例外エラーが発生すると、以降の Yield は無効になる ' Finally は呼び出せる ' 完了 End Sub
Dim items2 = New Object() {56, "aaa", True} For i As Integer = 0 To items2.Count - 1 Dim item As Integer = CInt(items2(i)) Next
Yield はラムダ式でも使える
- というだけです。ラムダ式は、無名なだけでメソッドに変わりはないので、そりゃそうですよね。としか・・・。
Public Sub Test4() ' Yield はラムダ式でも使える Dim getEnumerable1 = Iterator Function() As IEnumerable(Of Integer) Yield 1 Yield 2 End Function Dim items = getEnumerable1() For Each item In items Console.WriteLine(item) Next End Sub
Enumerable クラスについて
- 似たような名前で Enumerable クラス(System.Linq 名前空間)があります。 IEnumerable インターフェースを実装して作ったかのように見える Enumerable クラスですが、実装はしていません。
- それでは、何をするクラスなのかと言うと、IEnumerable(Of T) インターフェースを実装したクラスの編集や加工を容易におこなえるように、 大量のサポートメソッドを提供しているクラスになります。
Public Sub Test5() ' Enumerable クラス ' 継承できない ' IEnumerable(Of T) 型のデータを編集するための拡張メソッドがたくさんある ' 拡張メソッドは、変数の後ろにつなげられる→戻り値が、流れるように次のメソッドの入力値になる ' 以下は LINQ のうち、メソッド構文という書き方。 ' それぞれの拡張メソッドの引数に、ラムダ式を渡している(処理内容を渡している)。 ' 見た目は直接渡しているが、実際にはジェネリックデリゲートがラムダ式を受け取っていて、内部で実行している ' つい型推論に頼って、型を省略してしまう。 Dim items9 = Enumerable. Range(1, 10). Where(Function(x) x Mod 2 = 0). Take(3). Select(Function(x) x * x) For Each item In items9 Console.WriteLine(item) Next ' 以下は LINQ のうち、クエリ構文という書き方。 Dim items10 = From x In Enumerable.Range(1, 10) Where x Mod 2 = 0 Take 3 Select x For Each item In items10 Console.WriteLine(item) Next End Sub
List と IEnumerable のメモリ使用量の比較
- List のように一度に全部準備して列挙する系のデータと、Yield を使った1つ分ずつ列挙する系のデータの2つを使って、 速度とメモリ使用量を見てみましょう。
Public Sub Test6() Dim st = New Stopwatch() Dim useMemory = GC.GetTotalMemory(True) st.Start() Dim items1 = GetList() For Each item In items1 Next st.Stop() useMemory = GC.GetTotalMemory(True) - useMemory Console.WriteLine($"{useMemory} byte の使用量で、{st.Elapsed.TotalSeconds} 秒かかった") ' ------------------------------------------------------------- useMemory = GC.GetTotalMemory(True) st.Reset() st.Start() Dim items2 = GetEnumerable() For Each item In items2 Next st.Stop() useMemory = GC.GetTotalMemory(True) - useMemory Console.WriteLine($"{useMemory} byte の使用量で、{st.Elapsed.TotalSeconds} 秒かかった") ' 9324432 byte の使用量で、0.2662424 秒かかった ' 24 byte の使用量で、0.2241702 秒かかった End Sub Private Function GetList() As List(Of String) Dim items = New List(Of String) For i As Integer = 0 To 100000 items.Add(Guid.NewGuid().ToString) Next ' 100000 件分確保してから、やっと返却する Return items End Function Private Iterator Function GetEnumerable() As IEnumerable(Of String) For i As Integer = 0 To 100000 ' 1 件ずつ返却しては停止、返却しては停止、を繰り返す Yield Guid.NewGuid().ToString Next End Function
まとめ
- 駆け足で説明してきましたが、いかがだったでしょうか。特殊な扱い方があるとはいえ、ただの複数データを扱うためのインターフェースなだけです。 いまいちよく分からない、ピンと来ないという方は、LINQ して戻ってくる値が IEnumerable(Of T) 型でコレクションデータを扱うものだということだけ覚えてください。