VB.NET-Tipp 0102: IEnumerable(Of T) implementieren
von Spatzenkanonier
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: | Framework-Version(en): .NET Framework 3.5 | .NET-Version(en): Visual Basic 2008 | Download: |
' 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.