Die Community zu .NET und Classic VB.
Menü

VB.NET-Tipp 0102: IEnumerable(Of T) implementieren

 von 

Beschreibung

Die Interfaces IEnumerable und IEnumerable(Of T) sind als Vorraussetzung aller For-Each-Schleifen von fundamentaler Bedeutung. Darüber hinaus bauen die LINQ-Features auf diesen Schnittstellen auf.

Leider sind sie aber sehr umständlich und teilweise widersinnig zu implementieren. IEnumerable(Of T) erbt vom älteren IEnumerable und wer es implementieren will muss nun einiges doppelt bereitstellen - einmal für IEnumerable und, in typisierter Fassung, noch einmal für seinen generischen Bruder. Dabei war die alte IEnumerable-Schnittstelle schon nicht einfach zu verwenden, da immer gleichzeitig auch ein IEnumerator als Rückgabewert der von der Schnittstelle vorgeschriebenen Funktion IEnumerable.GetEnumerator() implementiert werden musste.

Der Kern dieses Tipps ist die Klasse Enumerable(Of T). Sie implementiert die Schnittstelle IEnumerable(Of T), und kann sie dadurch "weitervererben". Indem man von ihr erbt muss man nicht mehr 6 Schnittstellen-Member in zwei Klassen entwickeln, sondern nur noch eine Klasse mit einer Funktion und optional die Initialisierungs- und Aufräumfunktionen "Prepare"/"CleanUp", je nach zu lösender Aufgabe.

Der Nutzen wird an zwei sehr unterschiedlichen Beispielen gezeigt: CsvEnumerable enumeriert die Zeilen von CSV-Dateien und stellt die Werte typisiert zur Verfügung. Permutations.Of(T()) enumeriert alle Permutationen beliebiger Arrays (siehe auch IEnumerable implementieren - Array permutieren [Tipp 95]).

Schwierigkeitsgrad:

Schwierigkeitsgrad 2

Framework-Version(en):

.NET Framework 3.5

.NET-Version(en):

Visual Basic 2008

Download:

Download des Beispielprojektes [18,11 KB]

' Dieser Quellcode stammt von http://www.activevb.de
' und kann frei verwendet werden. Für eventuelle Schäden
' wird nicht gehaftet.

' Um Fehler oder Fragen zu klären, nutzen Sie bitte unser Forum.
' Ansonsten viel Spaß und Erfolg mit diesem Source!

' Projektversion:   Visual Studio 2008
' Option Strict:    An
' Option Infer:     An
'
' Referenzen: 
'  - System
'  - System.Core
'  - System.Data
'  - System.Deployment
'  - System.Drawing
'  - System.Windows.Forms
'  - System.Xml
'
' Imports: 
'  - Microsoft.VisualBasic
'  - Microsoft.VisualBasic.ControlChars
'  - System
'  - System.Collections.Generic
'  - System.Data
'  - System.Diagnostics
'  - System.Linq
'  - System.Windows.Forms
'

' ##############################################################################
' ################################ Article.vb ##################################
' ##############################################################################
' Daten-Objekt - mappt die durch den TextFieldParser eingelesenen Zeilen (er 
'  liest eine .csv-Datei ein) auf public deklarierte Variablen. Reihenfolge und 
'  Typ der Variablen müssen zu den Zeilen der .csv - Datei passen
Public Class Article

    Public ProductName As String
    Public QuantityPerUnit As String
    Public UnitPrice As Decimal
    Public UnitsInStock As Short
    Public Discontinued As Boolean

        Public Shared Function GetRecords(ByVal TP As FileIO.TextFieldParser) As  _
                IEnumerable(Of Article)

                Return New CsvEnumerable(Of Article)(TP)
        End Function

End Class
' ##############################################################################
' ############################# CsvEnumerable.vb ###############################
' ##############################################################################
Imports System.Reflection

' Dieses Enumerable kann nur einmal durchlaufen werden, da der TextFieldParser 
'  nicht rückstellbar ist. Es ist geeignet für beliebig große Csv-Dateien, da 
'  immer nur eine Zeile eingelesen wird
Public Class CsvEnumerable(Of T As New) : Inherits Enumerable(Of T)

    Private _TP As FileIO.TextFieldParser

    Private Shared _Flds As FieldInfo() = _
        GetType(T).GetFields(BindingFlags.Instance Or BindingFlags.Public)
    Private Shared _Converters As System.ComponentModel.TypeConverter()

    Shared Sub New()
        ' Shared Sub New() - wird in der gesamten Laufzeit höchstens
        '  einmal durchlaufen
        ' Der VBCodeProvider liefert für jedes in T gefundene Feld den 
        '  geeigneten TypConverter
        Using Prov = New VBCodeProvider
            _Converters = _Flds.Select(Function(fld As FieldInfo) _
                Prov.GetConverter(fld.FieldType)).ToArray
        End Using
    End Sub

    Public Sub New(ByVal TP As FileIO.TextFieldParser)
        _TP = TP
    End Sub

    ''' <summary> 
    '''  Ermittelt das nächste Element der Enumeration 
    ''' </summary>
    ''' <returns> 
    '''  True: Element wurde ermittelt 
    '''  False: Enumeration beendet
    ''' </returns>
    Protected Overrides Function TryMovenext(ByRef Current As T) As Boolean
        TryMovenext = Not _TP.EndOfData
        If TryMovenext Then
            Current = New T
            Dim Row As String() = _TP.ReadFields

            ' Ausgelesene Werte nach Current übertragen
            For I = 0 To _Flds.Length - 1
                _Flds(I).SetValue(Current, _
                    _Converters(I).ConvertFromString(Row(I)))
            Next
        End If
    End Function
End Class

' ##############################################################################
' ############################### Enumerable.vb ################################
' ##############################################################################
Imports System.Collections.Generic
Imports System.Collections

''' <summary>
'''  Abstrakte Basisklasse, die die Schnittstelle IEnumerable(Of T) vererbt, 
'''   und so derenImplementation vereinfacht
''' </summary>
Public MustInherit Class Enumerable(Of T) : Implements IEnumerable(Of T)
    Private _enrt As New Enumerator(Me)
    Private _IsPrepared As Boolean

    ''' <summary>
    '''  Fordert vom Erben das nächste Element der Enumeration an
    ''' </summary>
    ''' <returns>
    '''  True: wenn ein Element gegeben wurde
    '''  False: wenn die Enumeration beendet ist
    ''' </returns>
    Protected MustOverride Function TryMovenext(ByRef Current As T) As Boolean

    ''' <summary>
    ''' ein Enumerable(Of T) - Erbe muß entweder Prepare() oder CleanUp() 
    '''  überschreiben, um seinen Ausgangszustand wieder herzustellen
    ''' </summary>
    Protected Overridable Sub Prepare()
    End Sub

    ''' <summary>
    ''' Ein Enumerable(Of T) - Erbe muß entweder Prepare() oder CleanUp() 
    '''  überschreiben, um seinen Ausgangszustand wieder herzustellen
    ''' </summary>
    Protected Overridable Sub CleanUp()
        _IsPrepared = False
    End Sub

    Public Function GetEnumerator() As IEnumerator(Of T) _
        Implements IEnumerable(Of T).GetEnumerator

        If _IsPrepared Then Throw New Exception(String.Concat( _
            "IEnumerable(Of ", GetType(T).Name, ") ", _
            "kann nicht mehrere Enumeratoren gleichzeitig bereitstellen."))
        Prepare()
        _IsPrepared = True
        Return _enrt
    End Function

    Private Function GetEnumerator1() As IEnumerator _
        Implements IEnumerable.GetEnumerator

        Return GetEnumerator()
    End Function

    ' --------- nested Class: Enumerable(Of T).Enumerator -----------
    ' Der Enumerator macht nicht viel: Er leitet nur Aufrufe an die 
    '  Schnittstelle um
    Public Class Enumerator : Implements IEnumerator(Of T)
        Private _Current As T = Nothing

        Private _enmbl As Enumerable(Of T)

        Public Sub New(ByVal enmbl As Enumerable(Of T))
            _enmbl = enmbl
        End Sub

        Private ReadOnly Property Current() As T _
            Implements IEnumerator(Of T).Current

            Get
                Return _Current
            End Get
        End Property

        Private ReadOnly Property Current1() As Object _
            Implements IEnumerator.Current

            Get
                Return _Current
            End Get
        End Property

        Private Function MoveNext() As Boolean Implements IEnumerator.MoveNext
            Return _enmbl.TryMovenext(_Current)
        End Function

        Private Sub Prepare() Implements IEnumerator.Reset
            ' Habe noch nie mitbekommen, dass dieser Schnittstellenmember 
            ' aufgerufen wurde
            _enmbl.Prepare()
        End Sub

        Private Sub Dispose() Implements IDisposable.Dispose
            _enmbl.CleanUp()
        End Sub
    End Class 'Enumerable(Of T).Enumerator
End Class 'Enumerable(Of T)

' ##############################################################################
' ############################ frmIEnumerableOf.vb #############################
' ##############################################################################
Imports System.IO
Imports System.Data
Imports System.Data.OleDb

Public Class frmIEnumerableOf
    Private Animals As IEnumerable(Of String()) = _
        Permutations.Of("ant bat cow dog".Split(" "c))

    Private Sub Button_Click(ByVal sender As Object, ByVal e As EventArgs) _
        Handles btAllPermutations.Click, btAntAsSecondMember.Click, _
        btPriceOfAll.Click

        Select Case True
            Case sender Is btAllPermutations
                Dim All = From P In Animals Select String.Join(" "c, P)
                MsgBox(String.Join(NewLine, All.ToArray), , _
                    "Alle Permutationen")

            Case sender Is btAntAsSecondMember
                Dim AntsOnSecond = From P In Animals _
                    Where P(1) = "ant" Select String.Join(" "c, P)
                MsgBox(String.Join(NewLine, AntsOnSecond.ToArray), , _
                    "'ant' an zweiter Stelle")

            Case sender Is btPriceOfAll
            Using TP = New Microsoft.VisualBasic.FileIO.TextFieldParser( _
                "..\..\Article.csv")

                With TP
                    .HasFieldsEnclosedInQuotes = True
                    .TextFieldType = FileIO.FieldType.Delimited
                    .Delimiters = New String() {";"}
                    .ReadLine() ' Header überspringen
                End With
                Dim Val = Article.GetRecords(TP).Sum( _
                    Function(A) A.UnitPrice * A.UnitsInStock)
                MsgBox(String.Format("Der gesamte Warenbestand hat einen " & _
                    "Verkaufswert von {0:c}", Val))
            End Using
        End Select
    End Sub
End Class

' ##############################################################################
' ############################## Permutations.vb ###############################
' ##############################################################################
Public Class Permutations

    ''' <summary> 
    '''  Erzeugt eine Enumeration aller Permutationen von InitArray 
    ''' </summary>
    Public Shared Function [Of](Of T)( _
        ByVal ParamArray InitArray As T()) As IEnumerable(Of T())

        Return From Indices In New IndexPermutations(InitArray.Length) _
            Select ApplyIndicees(InitArray, Indices)
    End Function

    ''' <summary>
    '''  Kopiert Arr in ein neues Array um, mit der durch Indices gegebenen 
    '''  Reihenfolge
    ''' </summary>
    Private Shared Function ApplyIndicees(Of T)( _
        ByVal Arr As T(), ByVal Indices As Integer()) As T()

        Dim RetVal(Arr.Length - 1) As T
        For I = 0 To Arr.Length - 1
            RetVal(I) = Arr(Indices(I))
        Next
        Return RetVal
    End Function

End Class 'Permutations

' Diese Enumerable ist quasi "virtuell", da immer dasselbe Array zurückgegeben 
'  wird
Public Class IndexPermutations : Inherits Enumerable(Of Integer())
    Private _Ubound As Integer

    Public Sub New(ByVal Length As Integer)
        _Ubound = Length - 1
    End Sub

    Protected Overrides Function TryMovenext( _
        ByRef Current() As Integer) As Boolean

        If Current Is Nothing Then
            ReDim Current(_Ubound)
            ' Initial-Indices: 0, 1, 2, 3,...
            For I = 0 To _Ubound
                Current(I) = I
            Next
            Return True
        End If

        ' Es wird ein linker und ein rechter Wert gesucht, die vertauscht 
        ' werden. Anschließend die Reihenfolge rechts vom Links-Wert umkehren
        Dim Left As Integer, Right As Integer

        ' Links-Wert: muss kleiner sein als sein rechter Nachbar
        For Left = _Ubound - 1 To 0 Step -1
            If Current(Left) < Current(Left + 1) Then Exit For
        Next

        If Left < 0 Then
            ' Eine weitere Permutation kann nicht berechnet werden, Current 
            '  zurücksetzen - Damit sind die Aufräumarbeiten schon erledigt, und 
            '  Mybase.CleanUp braucht nicht überschrieben zu werden
            Current = Nothing
            Return False
        End If
        Dim LeftVal As Integer = Current(Left)

        ' Rechts-Wert: größer als der Links-Wert (ist nicht immer o.g. Nachbar!)
        For Right = _Ubound To 0 Step -1
            If Current(Right) > LeftVal Then Exit For
        Next

        ' Links-Wert und Rechts-Wert tauschen
        Current(Left) = Current(Right)
        Current(Right) = LeftVal

        ' Array rechts der Links-Position umkehren
        Array.Reverse(Current, Left + 1, _Ubound - Left)

        Return True
    End Function

End Class 'IndexPermutations

Ihre Meinung  

Falls Sie Fragen zu diesem Artikel haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.