C#では2.0より、VBではVB11(VB2012)よりイテレータ構文が導入されています。 これはIEnumerator<T>(またはIEnumerable)の実装をより簡単に記述できるものです。 C#ではyield
キーワード、VBではYield
ステートメントを用いることでイテレータを実装することができます。
イテレータ (反復子)
イテレータがどのようなものか具体的な例を見てみます。
using System;
using System.Collections.Generic;
class Sample {
// イテレータメソッド
static IEnumerable<string> Enumerate()
{
// yield returnでイテレータが列挙する値を返す
yield return "foo";
yield return "bar";
yield return "baz";
}
static void Main()
{
// 通常のIEnumerable<T>と同様に列挙することができる
foreach (var e in Enumerate()) {
Console.WriteLine(e);
}
}
}
Imports System
Imports System.Collections.Generic
Class Sample
' イテレータメソッド
Shared Iterator Function Enumerate() As IEnumerable(Of String)
' Yieldでイテレータが列挙する値を返す
Yield "foo"
Yield "bar"
Yield "baz"
End Function
Shared Sub Main()
' 通常のIEnumerable(Of Integer)と同様に列挙することができる
For Each e As String In Enumerate()
Console.WriteLine(e)
Next
End Sub
End Class
foo bar baz
このようにイテレータ構文を使うとIEnumerable・IEnumeratorを実装しなくても列挙ができるようになっています。 C#ではyield return 値
、VBではYield 値
とすることで列挙される値を返すことができます。 VBでは、イテレータとなるメソッドにIterator
修飾子を付ける必要があります。
もうひとつ違いと相違点を明らかにするための別の例を挙げます。 イテレータとIEnumerable・IEnumeratorのそれぞれで同じ要素を列挙するクラスを実装すると次のようになります。 (ここでは記述をシンプルにするためにIEnumerable<T>ではなく非ジェネリックなIEnumerableを実装しています)
using System;
using System.Collections;
class IntIterator : IEnumerable {
public IEnumerator GetEnumerator()
{
yield return "foo";
yield return "bar";
yield return "baz";
}
}
class Sample {
static void Main()
{
foreach (var e in new IntIterator()) {
Console.WriteLine(e);
}
}
}
Imports System
Imports System.Collections
Class IntIterator
Implements IEnumerable
Public Iterator Function GetEnumerator() As IEnumerator _
Implements IEnumerable.GetEnumerator
Yield "foo"
Yield "bar"
Yield "baz"
End Function
End Class
Class Sample
Shared Sub Main()
For Each e As String In New IntIterator()
Console.WriteLine(e)
Next
End Sub
End Class
using System;
using System.Collections;
class IntEnumerable : IEnumerable {
public IEnumerator GetEnumerator()
{
return new IntEnumerator();
}
}
class IntEnumerator : IEnumerator {
private int index = -1;
public object Current {
get {
switch (index) {
case 0: return "foo";
case 1: return "bar";
case 2: return "baz";
}
throw new InvalidOperationException();
}
}
public bool MoveNext()
{
index++;
if (index <= 2)
return true;
else
return false;
}
public void Reset()
{
throw new NotSupportedException();
}
}
class Sample {
static void Main()
{
foreach (var e in new IntEnumerable()) {
Console.WriteLine(e);
}
}
}
Imports System
Imports System.Collections
Class IntEnumerable
Implements IEnumerable
Public Function GetEnumerator() As IEnumerator _
Implements IEnumerable.GetEnumerator
Return New IntEnumerator()
End Function
End Class
Class IntEnumerator
Implements IEnumerator
Private index As Integer = -1
Public ReadOnly Property Current As Object _
Implements IEnumerator.Current
Get
Select Case index
Case 0 : Return "foo"
Case 1 : Return "bar"
Case 2 : Return "baz"
End Select
Throw New InvalidOperationException()
End Get
End Property
Public Function MoveNext() As Boolean _
Implements IEnumerator.MoveNext
index += 1
If index <= 2 Then
Return True
Else
Return False
End If
End Function
Public Sub Reset() Implements _
IEnumerator.Reset
Throw New NotSupportedException()
End Sub
End Class
Class Sample
Shared Sub Main()
For Each e As String In New IntEnumerable()
Console.WriteLine(e)
Next
End Sub
End Class
foo bar baz
イテレータを使ったコードでは、コンパイル時にCurrentプロパティやMoveNextメソッドなどIEnumerator(またはIEnumerator<T>)に必要なメンバが自動的に実装されます。
遅延実行
イテレータの動作はIEnumerable<T>
を実装した場合と変わりないように見えますが、異なる動作となる部分もあります。
using System;
using System.Collections.Generic;
class Sample {
// 0から4の値を生成するメソッド
static IEnumerable<int> GenerateNumbers()
{
yield return 0;
yield return 1;
yield return 2;
yield return 3;
yield return 4;
}
// 列挙される値を倍にして返すメソッド
static IEnumerable<int> Double(IEnumerable<int> list)
{
foreach (var e in list) {
yield return e * 2;
}
}
static void Main()
{
foreach (var e in Double(GenerateNumbers())) {
Console.WriteLine(e);
}
}
}
Imports System
Imports System.Collections.Generic
Class Sample
' 0から4の値を生成するメソッド
Shared Iterator Function GenerateNumbers() As IEnumerable(Of Integer)
Yield 0
Yield 1
Yield 2
Yield 3
Yield 4
End Function
' 列挙される値を倍にして返すメソッド
Shared Iterator Function [Double](ByVal list As IEnumerable(Of Integer)) As IEnumerable(Of Integer)
For Each e As Integer In list
Yield e * 2
Next
End Function
Shared Sub Main()
For Each e As Integer In [Double](GenerateNumbers())
Console.WriteLine(e)
Next
End Sub
End Class
0 2 4 6 8
この結果を見ると普通にListなどを列挙する場合と変わらないように見えますが、yield
は結果が生成されるタイミングが違います。 GenerateNumbers
メソッドを次のように変えてyield
が実行されるタイミングを調べてみると違いが明らかになります。
using System;
using System.Collections.Generic;
class Sample {
// 0から4の値を生成するメソッド
static IEnumerable<int> GenerateNumbers()
{
Console.WriteLine("yield 0"); yield return 0;
Console.WriteLine("yield 1"); yield return 1;
Console.WriteLine("yield 2"); yield return 2;
Console.WriteLine("yield 3"); yield return 3;
Console.WriteLine("yield 4"); yield return 4;
}
// 列挙される値を倍にして返すメソッド
static IEnumerable<int> Double(IEnumerable<int> list)
{
foreach (var e in list) {
yield return e * 2;
}
}
static void Main()
{
foreach (var e in Double(GenerateNumbers())) {
Console.WriteLine(e);
}
}
}
Imports System
Imports System.Collections.Generic
Class Sample
' 0から4の値を生成するメソッド
Shared Iterator Function GenerateNumbers() As IEnumerable(Of Integer)
Console.WriteLine("yield 0") : Yield 0
Console.WriteLine("yield 1") : Yield 1
Console.WriteLine("yield 2") : Yield 2
Console.WriteLine("yield 3") : Yield 3
Console.WriteLine("yield 4") : Yield 4
End Function
' 列挙される値を倍にして返すメソッド
Shared Iterator Function [Double](ByVal list As IEnumerable(Of Integer)) As IEnumerable(Of Integer)
For Each e As Integer In list
Yield e * 2
Next
End Function
Shared Sub Main()
For Each e As Integer In [Double](GenerateNumbers())
Console.WriteLine(e)
Next
End Sub
End Class
yield 0 0 yield 1 2 yield 2 4 yield 3 6 yield 4 8
この動作は遅延実行と呼ばれるものです。 通常のメソッドとは異なり、イテレータとなるメソッドでは呼び出された時点でIEnumerable
が返されるものの、イテレータメソッド内のyield
文は実際に値が列挙されるまでは呼び出されません。
イテレータ実装時の注意点
イテレータ構文は、戻り値の型がIEnumerable
(IEnumerable<T>
)もしくはIEnumerator
(IEnumerator<T>
)であるメソッドまたはプロパティで記述することができます。
ただし、匿名メソッドでは使用できない、例外処理に制限があるなど、イテレータ構文特有の注意点があります。 詳細は反復子 (C# プログラミング ガイド)を参照してください。