VB のたまご

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


Yield を学んで、実装を楽にしよう

自前コレクションデータの For Each 対応はとても大変

  •  以前、IEnumerableについて(VB2003時代)という記事で、 自前コレクションデータを、For Each ループできるように対応するのは大変だ、という記事を書きました。 まぁリスト系のクラスを継承して作れば楽できるのですが、そうもいかない場合も実際にはあると思います。 以前の記事では、IEnumerable, IEnumerator をそれぞれ実装して実現しました。いろいろ準備が多くてコード量が増えてしまいます。

  • スポンサーリンク


  •  復習すると、以下のようなサンプルです。
  • ' 1つ分のデータクラス
    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
    
    ' コレクションデータクラス
    ' Inherits MyCompanyCollectionBase は省略
    Class Persons2
        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 という風に使う。
        ' Persons2Enumerator メソッド内でも使う。
        Public Function Count() As Integer
            Return Me.persons.Length
        End Function
    
        ' インデックス指定して1つのデータの取得、または設定
        ' For 文の中で、dim d As Object = lst(i) という風に使う。
        ' Persons2Enumerator メソッド内でも使う。
        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
    
    ' コレクションデータを使って、For Each する
    Dim lst As New Persons2
    lst.Add(New Person("taro", 25))
    lst.Add(New Person("jiro", 35))
    
    For Each p As Person In lst
        Console.WriteLine("Name={0}, Age={1}", p.Name, p.Age)
    Next
    
    ' 出力結果
    Name=taro, Age=25
    Name=jiro, Age=35
    

  •  これを、必要な分だけ何回も実装するのは相当大変だったと思います。 ただ内容的に、いったん作成したら以降のメンテナンスはあまり無いというのが救いかなと思います。

スポンサーリンク


Yield で書き直してみよう

  •  時が経過して、VB2012 からは Yield(イールド)命令が使えるようになりました。 これにより、IEnumerable, IEnumerator を実装しなくても、For Each ループに対応できるコードが書けるようになりました。 Yield は、Iterator キーワードと共に使います。

  • ' コレクションデータ
    Class Persons3
    
        ' 内部管理する 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 Each ループで1つ1つデータを取り出すための処理を実装
        ' For Each d As Person In lst の時に、内部的に使われるために必要。
        Public Iterator Function GetEnumerator() As IEnumerator
    
            For Each p In Me.persons
                Yield p
            Next
    
        End Function
    
    End Class
    
    ' コレクションデータを使って、For Each する
    Dim lst As New Persons3
    lst.Add(New Person("taro", 25))
    lst.Add(New Person("jiro", 35))
    
    For Each p As Person In lst
        Console.WriteLine("Name={0}, Age={1}", p.Name, p.Age)
    Next
    
    ' 出力結果
    Name=taro, Age=25
    Name=jiro, Age=35
    

  •  コード量が激減して、あれだけあった命令がスッキリ消えています。でもしっかりとループ機能が効いていて、かなり楽になりました。 Yield が VB2003 からあったなら・・・と思ってしまいますね。

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


スポンサーリンク