IEnumerableとIEnumeratorを使った列挙

注意:
この文書は以前「.NETでいきまっしょい!」で公開していたものですが、公開以降メンテナンスされていません。 今や古い情報となった内容が記載されている場合があるのでご注意ください。

1.ForとFor Each


 VB.NETではFor文には二種類あります。 それは通常の繰り返しのためのForステートメントと、要素の列挙ためのFor Eachステートメントです。 For EachステートメントはVBのころから導入されていましたが、VB.NETでも引き続き使用され、C#でもforeachとして導入されました。 では、そのFor Eachの動作から見てみたいと思います。 For EachステートメントではIn以降で指定されるコレクションや配列の中にある全ての要素の列挙を繰り返します。 Integer型の配列でForとFor Eachを使った例を次に示します。
For と For Each
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
Imports System

' アプリケーションのエントリーポイントのためのクラス
Public NotInheritable Class Enumeration

    ' エントリーポイント
    Public Shared Function Main(ByVal args() As String) As Integer

        Dim arr(4) As Integer

        Dim i As Integer

        ' 配列に値を与える
        For i = 0 To arr.Length - 1

            arr(i) = i

        Next

        ' For ステートメントで配列の値を表示する
        Console.WriteLine("Enumerate by 'For' statement.")

        For i = 0 To arr.Length - 1

            Console.WriteLine(arr(i))

        Next

        ' For Each ステートメントで配列の値を表示する
        Console.WriteLine("Enumerate by 'For Each'statement.")

        For Each i In arr

            Console.WriteLine(i)

        Next

        Return 0

    End Function

End Class
出力結果
Enumerate by 'For' statement.
0
1
2
3
4
Enumerate by 'For Each' statement.
0
1
2
3
4
Press any key to continue

 このように、For も For Eachも同じように列挙できます。 こうなるとFor と For Eachは何が違うのかよくわからなくなります。 そこで、配列の簡易ラッパークラスを使ってFor Eachが使えなくなる状況を作り出してみます。
For Eachが使えない場合
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
Imports System

Public Class IntegerArray

    ' ラップするための内部配列
    Private m_Array() As Integer

    ' コンストラクタ
    Public Sub New()

        ' 既定の配列サイズ
        Me.New(&H10)

    End Sub

    Public Sub New(ByVal capacity As Integer)

        ' capacityで指定されたサイズの配列を作成
        ReDim m_Array(capacity - 1)

    End Sub


    ' 配列へのアクセサとしてのデフォルトプロパティ
    Default Public Property Item(ByVal index As Integer) As Integer

        Get

            If 0 <= index AndAlso index < m_Array.Length Then

                ' 配列のインデックスの範囲内であればそのインデックスの値を返す。
                Return m_Array(index)

            Else

                ' そうでなければ 0 を返すことにする。
                Return 0

            End If

        End Get

        Set(ByVal Value As Integer)

            If 0 <= index AndAlso index < m_Array.Length Then

                ' 配列のインデックスの範囲内であればそのインデックスに代入する
                m_Array(index) = Value

            End If

            ' そうでない場合は無視する

        End Set

    End Property

    ' 長さを返す
    Public ReadOnly Property Length() As Integer

        Get

            Return m_Array.Length

        End Get

    End Property

End Class

' アプリケーションのエントリーポイントのためのクラス
Public NotInheritable Class Enumeration

    ' エントリーポイント
    Public Shared Function Main(ByVal args() As String) As Integer

        Dim arr As New IntegerArray(5)

        Dim i As Integer

        ' 配列に値を与える
        For i = 0 To arr.Length - 1

            arr(i) = i

        Next

        ' For ステートメントで配列の値を表示する
        Console.WriteLine("Enumerate by 'For' statement.")

        For i = 0 To arr.Length - 1

            Console.WriteLine(arr(i))

        Next

        ' For Each ステートメントで配列の値を表示する
        Console.WriteLine("Can not enumerate by 'For Each' statement.")

        'For Each i In arr

        '    Console.WriteLine(i)

        'Next

        Return 0

    End Function

End Class
出力結果
Enumerate by 'For' statement.
0
1
2
3
4
Can not enumerate by 'For Each' statement.
Press any key to continue

 IntegerArrayクラスはふつうのInteger型の配列と同様の振る舞いをします。 しかし、エラーによりFor Eachはできません。 そのエラーを見ると次のようになっています。

 つまり、In 以降で指定すべきオブジェクトはコレクション型でなければならず、arrはコレクション型ではないことになります。

2.IEnumerableの実装


 ヘルプでFor Eachステートメントについて調べると次のようなことが書いてありました。

For Each...Next ステートメントは、式の要素に基づいてループを実行します。For Each ステートメントの式によって返される値の型は、下で定義する "コレクション型" である必要があります。また、コレクションの要素型から繰り返し変数の型への、暗黙の型変換が必要です。
System.IEnumerable を実装するか、または次の条件をすべて満たしている場合、型 C はコレクション型になります。
・C に、シグネチャが GetEnumerator() で、型 E. を返す Public インスタンス メソッドが含まれる。
・E に、シグネチャが MoveNext() で、戻り値の型が Boolean である Public インスタンス メソッドが含まれる。
・E に、取得関数を持つ Current という名前の Public インスタンス プロパティが含まれる。このプロパティの型は、コレクション型の要素型と呼ばれます。

 つまり簡単にまとめてしまうと、For Eachを使えるようにするにはその型がコレクション型である必要があり、コレクション型にするためにはIEnumerableインターフェイスを実装していればよい、といっていることになります。 それでは早速、先ほどのIntegerArrayクラスにIEnumerableインターフェイスを実装してみます。
IEnumerableの実装
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
Imports System

Public Class IntegerArray

    ' IEnumerableを実装
    Implements IEnumerable

    ' ラップするための内部配列
    Private m_Array() As Integer

    ' コンストラクタ
    Public Sub New()

        ' 既定の配列サイズ
        Me.New(&H10)

    End Sub

    Public Sub New(ByVal capacity As Integer)

        ' capacityで指定されたサイズの配列を作成
        ReDim m_Array(capacity - 1)

    End Sub


    ' 配列へのアクセサとしてのデフォルトプロパティ
    Default Public Property Item(ByVal index As Integer) As Integer

        Get

            If 0 <= index AndAlso index < m_Array.Length Then

                ' 配列のインデックスの範囲内であればそのインデックスの値を返す。
                Return m_Array(index)

            Else

                ' そうでなければ 0 を返すことにする。
                Return 0

            End If

        End Get

        Set(ByVal Value As Integer)

            If 0 <= index AndAlso index < m_Array.Length Then

                ' 配列のインデックスの範囲内であればそのインデックスに代入する
                m_Array(index) = Value

            End If

            ' そうでない場合は無視する

        End Set

    End Property

    ' 長さを返す
    Public ReadOnly Property Length() As Integer

        Get

            Return m_Array.Length

        End Get

    End Property

    ' IEnumerable.GetEnumerator()の実装
    Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator

        ' 配列型のGetEnumerator()を流用する
        Return m_Array.GetEnumerator()

    End Function

End Class
出力結果
Enumerate by 'For' statement.
0
1
2
3
4
Enumerate by 'For Each' statement.
0
1
2
3
4
Press any key to continue

 このサンプルを実行する前に、Main()メソッドでは先ほどまで使えなかったFor Eachを再び使えるようにコメント化を解除しています。 IEnumarableインターフェイスを実装する場合は一つのメソッドGetEnumerator()を実装しなければなりません。 このメソッドは、簡単に言ってしまうとFor Eachをするために必要な処理を行ってくれる「列挙子」というものを取得するためのものです。 この例では配列型のm_Arrayから列挙子を取得していますが、実は配列型がFor Eachする事ができるのは、この列挙子を取得するためのメソッドを持っているためなのです。

3.IEnumeratorの正体


 IEnumerable.GetEnumerator()メソッドは列挙子を取得するためのものと説明しましたが、その戻り値である列挙子はIEnumerator型です。 IEnumerableと名前が似ているので注意して下さい。 IEnumeratorはその名のとおり列挙を行うためのもので、これを取得することができればFor Eachする事ができるとしましたが、IEnumeratorの正体が分からないまま使っていても気分が悪いので、その実体を調べてみたいと思います。
 IEnumeratorを実装するに当たり、実装するクラスは次の三つのメンバを実装しなければなりません。

1.列挙子を初期状態に設定するメソッド/シグニチャ: Public Sub Reset()
 列挙子をコレクションの最初の要素の前の位置に設定する。

2.列挙子を次の要素に移動するメソッド/シグニチャ: Public Function MoveNext() As Boolean
 列挙子をコレクションの次の要素に進め、コレクションの範囲を超えた場合はFalseを返す。

3.現在列挙子が指し示すコレクションの要素を取得する読み取り専用プロパティ/シグニチャ: Public ReadOnly Property Current() As Object
 現在列挙子が指し示すコレクションの要素を返す。

 実際のFor Eachではこれら三つのメンバが使用されます。 まず、For Eachが開始される時点でReset()メソッドが呼び出されます。 続いて、最初の要素が取り出される前にMoveNext()メソッドを呼び出し、列挙子に次の要素、つまり最初の要素を指し示させます。 この時点でCurrentプロパティからコレクションの値が読みとられます。 これ以降はFor EachのNextに達する度にMoveNext()、Currentが呼び出されます。 ここで、MoveNext()がFalseを返したときはコレクションがそれ以上要素を持っていないということなので、この時点で列挙が終了します。
 それでは、IEnumeratorの詳細がわかったところで実装してみます。 ここではIEnumeratorを実装するクラスとしてIntegerArrayEnumeratorクラスを作成しましたが、IntegerArrayクラスの列挙以外には使われる必要がないのでPrivateなクラスとしてIntegerArrayクラスの中で宣言しています。
IEnumerableの実装
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
Imports System

Public Class IntegerArray

    ' IEnumerableを実装
    Implements IEnumerable




    ' IntegerArray用の列挙子 ( 外部に公開する必要はない )
    Private Class IntegerArrayEnumerator

        ' IEnumeratorを実装
        Implements IEnumerator

        ' 列挙子の指す要素の位置
        Private m_Pointer As Integer

        ' 列挙すべき配列
        Private m_Array As Integer()

        ' コンストラクタ
        Public Sub New(ByVal array As Integer())

            m_Array = array

            ' 念のため列挙子を初期化
            MyClass.Reset()

        End Sub

        ' 現在列挙子の指す要素を取得する
        Public ReadOnly Property Current() As Object Implements IEnumerator.Current

            Get

                Return m_Array(m_Pointer)

            End Get

        End Property

        ' 列挙子をリセットする
        Public Sub Reset() Implements IEnumerator.Reset

            m_Pointer = m_Array.GetLowerBound(0) - 1

        End Sub

        ' 列挙子を次の要素に移動する
        Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext

            m_Pointer += 1

            ' 列挙子の位置が配列の範囲を超えていたらFalseを返し、列挙を終了させる
            If m_Pointer <= m_Array.GetUpperBound(0) Then

                Return True

            Else

                Return False

            End If

        End Function

    End Class




    ' ラップするための内部配列
    Private m_Array() As Integer

    ' コンストラクタ
    Public Sub New()

        ' 既定の配列サイズ
        Me.New(&H10)

    End Sub

    Public Sub New(ByVal capacity As Integer)

        ' capacityで指定されたサイズの配列を作成
        ReDim m_Array(capacity - 1)

    End Sub


    ' 配列へのアクセサとしてのデフォルトプロパティ
    Default Public Property Item(ByVal index As Integer) As Integer

        Get

            If 0 <= index AndAlso index < m_Array.Length Then

                ' 配列のインデックスの範囲内であればそのインデックスの値を返す。
                Return m_Array(index)

            Else

                ' そうでなければ 0 を返すことにする。
                Return 0

            End If

        End Get

        Set(ByVal Value As Integer)

            If 0 <= index AndAlso index < m_Array.Length Then

                ' 配列のインデックスの範囲内であればそのインデックスに代入する
                m_Array(index) = Value

            End If

            ' そうでない場合は無視する

        End Set

    End Property

    ' 長さを返す
    Public ReadOnly Property Length() As Integer

        Get

            Return m_Array.Length

        End Get

    End Property

    ' IEnumerable.GetEnumerator()の実装
    Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator

        ' 独自に作成した IntegerArrayEnumerator を使用する
        Return New IntegerArrayEnumerator(m_Array)

    End Function

End Class
出力結果
Enumerate by 'For' statement.
0
1
2
3
4
Enumerate by 'For Each' statement.
0
1
2
3
4
Press any key to continue

 実行に際してMain()メソッドには変更を加えていません。 出力結果を見てわかるとおり、配列からIEnumeratorを取得した場合と全く変わっていませんが、確実にFor Eachにより列挙が成されています。 今回作成したIntegerArrayEnumeratorの動作は恐らくほとんど配列のIEnumeratorの実装と変わらないと思います。 しかし、IEnumerator実装をカスタマイズすることで、様々なクラスでFor Eachが使えるようになることは非常に有益なことではないでしょうか。 また、今回は全てInteger型の配列を扱ってきましたが、IEnumerator及びIEnumerableはObject型で扱うのでどのような型でもコレクションにできることは言うまでもありません。

4.For Eachを自作する


 最後に、IEnumeratorとFor Eachの関係が理解できたので、IEnumeratorを取得してFor Eachだけを自作してみることにします。 IEnumeratorの挙動は、言い換えればFor Eachの挙動でもあるので、IEnumeratorの動作に従ってコーディングすればFor Eachを使わなくてもFor Eachに変わるものを作ることができます。 次のサンプルでは先ほどのIntegerArrayとIntegerArrayEnumeratorを用いてWhileステートメントで列挙を行っています。
IEnumerableの実装
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
Imports System

' アプリケーションのエントリーポイントのためのクラス
Public NotInheritable Class Enumeration

    ' エントリーポイント
    Public Shared Function Main(ByVal args() As String) As Integer

        Dim arr As New IntegerArray(5)

        Dim i As Integer

        ' 配列に値を与える
        For i = 0 To arr.Length - 1

            arr(i) = i

        Next

        ' While ステートメントで配列の値を表示する
        Console.WriteLine("Enumerate by 'While' statement.")

        ' 列挙子を取得
        Dim enumerator As IEnumerator = arr.GetEnumerator()

        ' 列挙子をリセット
        enumerator.Reset()

        ' Falseが返されるまでループ
        While enumerator.MoveNext()

            ' 要素を取得
            i = CType(enumerator.Current, Integer)

            Console.WriteLine(i)

        End While

        Return 0

    End Function

End Class
出力結果
Enumerate by 'While' statement.
0
1
2
3
4
Press any key to continue