VB のたまご

作成日: 2015/12/03, 更新日: 2015/12/29


IEnumerable を勉強して、For Each で操作できるようにしよう

IEnumerable の役割

  •  今でも使う機会があるのか分かりませんが、For Each するための IEnumerable の実装方法についてです。 自前でコレクションデータを作成する際、インデックス操作と For 文と For Each 文を使えるように、いろいろ準備する必要があります。 この記事では、ArrayList を使って省略したパターンと1から自前で用意するパターンとで解説しています。


スポンサーリンク


自前コレクションデータを作ってみよう!

  •  一番簡単なのは、ArrayList を使うパターンです。
  • Class Person
    
        Public Property Name As String = String.Empty
        Public Property Age As Integer = 0
    
        Public Sub New(ByVal name As String, ByVal age As Integer)
            Me.Name = name
            Me.Age = age
        End Sub
    
    End Class
    
    Dim lst As New ArrayList
    lst.Add(New Person("taro", 25))
    lst.Add(New Person("jiro", 35))
    
    For i As Integer = 0 To lst.Count - 1
        Dim p As Person = CType(lst(i), Person)
        Console.WriteLine("Name={0}, Age={1}", p.Name, p.Age)
    Next
    
    For Each p As Person In lst
        Console.WriteLine("Name={0}, Age={1}", p.Name, p.Age)
    Next
    
  •  ただ単純に ArrayList を使うだけなら、今と対して変わらずにできます。 それでは、ArrayList を継承してコレクションデータを作成するパターンを見てみます。

  • Class Persons1
        Inherits ArrayList
    
        Public Shadows Sub Add(ByVal value As Person)
            MyBase.Add(value)
        End Sub
    
    End Class
    
    Dim lst As New Persons1
    lst.Add(New Person("taro", 25))
    lst.Add(New Person("jiro", 35))
    
    For i As Integer = 0 To lst.Count - 1
        Dim p As Person = CType(lst(i), Person)
        Console.WriteLine("Name={0}, Age={1}", p.Name, p.Age)
    Next
    
    For Each p As Person In lst
        Console.WriteLine("Name={0}, Age={1}", p.Name, p.Age)
    Next
    
  •  ほとんどの機能は ArrayList 任せですので簡単に作成できます。 続いて本題となる、自前で実装するパターンを見てみます。

  • Class MyCompanyCollectionBase
        ' ここで、ArrayList を継承したり、IEnumerable の実装をするべきですが、それができなかったと仮定しています。
        ' ここには、その会社独自の共有メンバーの定義とかが書かれているとします。
    End Class
    
    ' ArrayList は使いたくない、または使えないので、ArrayList を継承できない
    Class Persons2
        Inherits MyCompanyCollectionBase
        Implements IEnumerable
    
        ' 内部管理する Person 配列
        Private persons() As Person = Nothing
    
        ' Person データを追加する処理。
        ' lst.Add(New Person("taro", 25)) という風に使う。
        Public Sub Add(value As Person)
    
            If Me.persons Is Nothing Then
                ReDim Me.persons(0)
                Me.persons(0) = value
            Else
                ReDim Preserve Me.persons(Me.persons.Length)
                Me.persons(Me.persons.Length - 1) = value
            End If
    
        End Sub
    
        ' 現在の登録数を返却
        ' For i As Integer = 0 To lst.Count -1 という風に使う。
        Public Function Count() As Integer
            Return Me.persons.Length
        End Function
    
        ' インデックス指定して1つのデータの取得、または設定
        ' For 文の中で、dim d As Object = lst(i) という風に使う。
        Default Public Property Items(ByVal i As Integer) As Person
            Get
                Return Me.persons(i)
            End Get
            Set(value As Person)
                Me.persons(i) = value
            End Set
        End Property
    
        ' For Each ループで1つ1つデータを取り出すための処理を実装
        ' For Each d As Person In lst の時に、内部的に使われるために必要。
        Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
            Return New Persons2Enumerator(Me)
        End Function
    
    End Class
    
    Class Persons2Enumerator
        Implements IEnumerator
    
        Private persons As Persons2 = Nothing ' 内部管理する Person リスト
        Private cnt As Integer = -1           ' 現在位置を管理するインデックス
    
        Public Sub New(lst As Persons2)
            Me.persons = lst
        End Sub
    
        ' 現在位置の値を返却
        Public ReadOnly Property Current As Object Implements IEnumerator.Current
            Get
                Return Me.persons(cnt)
            End Get
        End Property
    
        ' 次の位置の取得が可能かチェック
        Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
            Me.cnt += 1
            Return Me.cnt < Me.persons.Count
        End Function
    
        ' 初期化
        Public Sub Reset() Implements IEnumerator.Reset
            Me.cnt = -1
        End Sub
    
    End Class
    
    Dim lst As New Persons2
    lst.Add(New Person("taro", 25))
    lst.Add(New Person("jiro", 35))
    
    For i As Integer = 0 To lst.Count - 1
        Dim p As Person = CType(lst(i), Person)
        Console.WriteLine("Name={0}, Age={1}", p.Name, p.Age)
    Next
    
    For Each p As Person In lst
        Console.WriteLine("Name={0}, Age={1}", p.Name, p.Age)
    Next
    
  •  ふう、自前で作るというのは、大変ですね。
  • 配列を扱う際は、ReDim と Preserve 命令を使って配列の準備をしてから、データを登録します。 普段リストをメインに操作していると、あまり出てこない命令です。使うの久しぶりでした。

  •  そして、For Each ループでも使えるようにするために、 IEnumerable と IEnumerator のインターフェースを実装して作ることになります。この処理が長い長い。 初めて見た時、なんで、IEnumerable だけで完結しないで IEnumerator との組み合わせなのか、どうして IEnumerator のために、 わざわざクラスを用意しないといけないのか、不思議でした。

スポンサーリンク


For Each の仕組みを見る

  •  For Each は糖衣構文と呼ばれており、その実際の処理が別にあります。 糖衣構文というのは、複雑な処理を簡単に操作できるように包んであげた命令の事を言います。 同じ処理でも、簡単に扱える方が楽できるので嬉しいです。以下のサンプルは、実際の処理です。
  • ' For Each の内部的な処理
    Dim en As IEnumerator = lst.GetEnumerator()
    While en.MoveNext() = True
        Dim p As Person = CType(en.Current, Person)
        Console.WriteLine("Name={0}, Age={1}", p.Name, p.Age)
    End While
    
  •  現在値の管理は IEnumerator の内部管理に任せるので、こちらから変更はできません。できるのは Reset だけです。 MoveNext メソッドが次の値もあるよ。と言ってくれる間だけループします。で現在値を取得します。シンプルですね。
  •  For 文だとインデックス指定なので、やろうと思えばコレクションデータ全体に対してアクセスできます。 例えば、現在値が i なので、lst(i) だけ取得するのではなく、1つ前の値を知りたければ lst(i-1) と指定することで、1つ前の値を取得できます。
  •  しかし、For Each 文では、1ループ1データのみです。前の値とかは取得できませんが、特に不便さは無く使っていますよね。

まとめ

  •  IEnumerable, IEnumerator を実装するのは大変なので、組み込みのリストのありがたみが分かる調査となりました。 【追記】VB2012 からは、Yield を使うことで、IEnumerable, IEnumerator を実装しなくても For Each ループができるようになりました。 詳しくは、Yieldについてをご覧ください。

  •  最後までこの記事を読んでいただき、ありがとうございました。

  • スポンサーリンク