Visual Basic 2012 Voorbeelden
   

visual basic 2012 broncode voorbeelden

Blijf op de hoogte van de recente aanpassingen op vbvoorbeelden!

Microsoft Visual Studio 2012Microsoft Developers Network - Visual BasicMicrosoft .NET Framework

35.1. Yield Statement - Iterator Method

Print Email Deel op Twitter Deel op Facebook

Dit artikel is gepubliceerd op maandag 15 oktober 2012 op vbvoorbeelden, bezoek de website voor een recente versie van dit artikel of andere artikels.

In volgend voorbeeld vind je een collectietype waarvan de objecten een eenvoudige begrensde ("bounded") wachtrij ("queue") voorstellen.  Elementen kunnen aan deze verzameling toegevoegd worden (Enqueue) en van deze verzameling verwijderd worden (Dequeue).
De via de constructor ingestelde Capacity geeft aan wat het maximum aantal elementen is.  Voor de eenvoud van het voorbeeld worden overige toevoegingen (nadat de capaciteit reeds bereikt werd) gewoon genegeerd.
Visual Basic 2012 Broncode - Codevoorbeeld 828
Namespace UnEnumerable
    Class BoundedQueue
        Protected _Items As Object()
        Protected _Count As Integer
        Public Sub New(ByVal capacity As Integer)
            ReDim _Items(capacity - 1)
        End Sub
        Public ReadOnly Property Count() As Integer
            Get
                Count = _Count
            End Get
        End Property
        Public ReadOnly Property Capacity() As Integer
            Get
                Capacity = _Items.Length
            End Get
        End Property
        Public Sub Enqueue(ByVal value As Object)
            If Count < Capacity Then
                _Items(Count) = value
                _Count += 1
            End If
        End Sub
        Public Function Peek() As Object
            Peek = _Items(0)
        End Function
        Public Sub Dequeue()
            For index As Integer = 0 To Count - 2
                _Items(index) = _Items(index + 1)
            Next
            _Items(Count - 1) = 0
            _Count -= 1
        End Sub
    End Class
End Namespace

35.1.1. Iterator Pattern - IEnumerable en IEnumerator

Om het voor clientcode eenvoudig te maken de elementen van dergelijke verzameling te consumeren zouden we objecten van dit collectietype itereerbaar ("enumereerbaar") moeten maken.  Waarbij de clientcode een For Each ... Next statement kan gebruiken om de elementen van deze verzameling te benaderen:
Visual Basic 2012 Broncode - Codevoorbeeld 829
Namespace UnEnumerable
    Class Client
        Public Shared Sub Main()
            Dim boundedQueue1 As New BoundedQueue(5)
            boundedQueue1.Enqueue(1)
            boundedQueue1.Enqueue(2)
            boundedQueue1.Enqueue(3)
            '
            ' Not possible :
            'For Each element As Object In boundedQueue1      ' (1)
            '    Console.Write(element.ToString() & " ")
            'Next
            '
            Console.ReadLine()
        End Sub
    End Class
End Namespace
Op dit moment is het benaderen van de elementen van een BoundedQueue via een For Each ... Next statement echter nog niet mogelijk.  Regel (1) zou dan ook een compilefout veroorzaken.

Wensen we deze mogelijkheid te voorzien dan implementeren we de IEnumerable en IEnumerator (of IEnumerable(Of T) en IEnumerator(Of T)) interfaces (Iterator pattern):
Interface IEnumerable
    Function GetEnumerator() As IEnumerator
End Interface
Interface IEnumerator
    Function MoveNext() As Boolean
    ReadOnly Property Current() As Object
    Sub Reset()
End Interface

Interface IEnumerable(Of T) : Inherits IEnumerable
    Function GetEnumerator() As IEnumerator(Of T)
End Interface
Interface IEnumerator(Of T) : Inherits IEnumerator, IDisposable
    ReadOnly Property Current() As T
End Interface
Het is immers zo dat een For Each als:
For Each element As Object In boundedQueue1
    Console.Write(element.ToString() & " ")
Next
Op de achtergrond wordt omgezet naar iets als:
Dim enumerator As IEnumerator = boundedQueue1.GetEnumerator()
Do While enumerator.MoveNext()
    Console.Write(enumerator.Current.ToString() & " ")
Loop
enumerator.Result()
Ons BoundedQueue voorbeeld breiden we hier uit met de implementatie van de IEnumerable en IEnumerator interfaces:
Visual Basic 2012 Broncode - Codevoorbeeld 830
Namespace ReturnInstanceOfUserDefinedIEnumerator
    Class BoundedQueue : Inherits UnEnumerable.BoundedQueue
        Implements IEnumerable
        Public Sub New(ByVal capacity As Integer)
            MyBase.New(capacity)
        End Sub
        Public Overridable Function GetEnumerator() As IEnumerator _
                                        Implements IEnumerable.GetEnumerator
            GetEnumerator = New BoundedQueueEnumerator(Me)
        End Function
        Class BoundedQueueEnumerator : Implements IEnumerator
            Private _BoundedQueue As BoundedQueue
            Private _Cursor As Integer = -1
            Public Sub New(boundedQueue As BoundedQueue)
                _BoundedQueue = boundedQueue
            End Sub
            Public Function MoveNext() As Boolean _
                                                 Implements IEnumerator.MoveNext
                _Cursor += 1
                MoveNext = (_Cursor < _BoundedQueue.Count)
            End Function
            Public ReadOnly Property Current() As Object _
                                                  Implements IEnumerator.Current
                Get
                    Current = _BoundedQueue._Items(_Cursor)
                End Get
            End Property
            Public Sub Reset() Implements IEnumerator.Reset
                _Cursor = -1
            End Sub
        End Class
    End Class
End Namespace
Hierbij wordt het nu wel mogelijk om aan de hand van een For Each de elementen van boundedQueue1 te benaderen:
Visual Basic 2012 Broncode - Codevoorbeeld 831
Namespace ReturnInstanceOfUserDefinedIEnumerator
    Class Client1
        Public Shared Sub Main()
            Dim boundedQueue1 As New BoundedQueue(5)
            boundedQueue1.Enqueue(1)
            boundedQueue1.Enqueue(2)
            boundedQueue1.Enqueue(3)
            '
            For Each element As Object In boundedQueue1
                Console.Write(element.ToString() & " ")
            Next
            '
            Console.ReadLine()
        End Sub
    End Class
End Namespace
Console Application Output
1 2 3
Natuurlijk kan het zelfde effect bekomen door rechtstreeks gebruik te maken van de toegevoegde members:
Visual Basic 2012 Broncode - Codevoorbeeld 832
Namespace ReturnInstanceOfUserDefinedIEnumerator
    Class Client2
        Public Shared Sub Main()
            Dim boundedQueue1 As New BoundedQueue(5)
            boundedQueue1.Enqueue(1)
            boundedQueue1.Enqueue(2)
            boundedQueue1.Enqueue(3)
            '
            Dim enumerator As IEnumerator = boundedQueue1.GetEnumerator()
            Do While enumerator.MoveNext()
                Console.Write(enumerator.Current.ToString() & " ")
            Loop
            '
            Console.ReadLine()
        End Sub
    End Class
End Namespace
Console Application Output
1 2 3
Tot zover niets nieuws.  Het implementeren van deze interfaces is echter een omslachtig en vaak nogal repetitieve bezigheid.  Voor vele collectietypes is de implementatie immers gelijkaardig.

35.1.2. Yield Statement ter vervanging van Iterator Pattern

Je zou kunnen stellen dat de noodzaak tot het toepassen van design patterns (als het hiervoor toegepaste iterator pattern) een teken is dat duidt op zwaktes in de programmeertaal.  Bij het optreden van bepaalde - vaak voorkomende - problemen, waarvoor geen taalkundige ondersteuning is, wordt de programmeur verplicht een bepaalde constructie (pattern) op te stellen.

Gelukkig wordt vanaf Visual Basic 2012 (ook wel Visual Basic 11 genoemd) een taalkundige oplossing geboden die de noodzaak aan het uitwerken van een iterator pattern sterk doet afnemen.
Net als in C# (2.0 of hoger) wordt er nu ook in Visual Basic 2012 een Yield statement voorzien:
Visual Basic 2012 Broncode - Codevoorbeeld 833
Namespace IteratorFunction
    Class BoundedQueue : Inherits UnEnumerable.BoundedQueue
        Implements IEnumerable
        Public Sub New(ByVal capacity As Integer)
            MyBase.New(capacity)
        End Sub
        Public Overridable Iterator Function GetEnumerator() As IEnumerator _
                                           Implements IEnumerable.GetEnumerator
            Dim index As Integer                                          ' (1)
            index = 0                                                     ' (2)
            Do While index < Count                                        ' (3)
                Yield _Items(index)                                       ' (4)
                index += 1                                                ' (5)
            Loop
        End Function
    End Class
    Class Client
        Public Shared Sub Main()
            Dim boundedQueue1 As New BoundedQueue(5)
            boundedQueue1.Enqueue(1)
            boundedQueue1.Enqueue(2)
            boundedQueue1.Enqueue(3)
            '
            For Each element As Object In boundedQueue1
                Console.Write(element.ToString() & " ")
            Next
            ' or:
            Dim enumerator As IEnumerator = boundedQueue1.GetEnumerator()
            Do While enumerator.MoveNext()
                Console.Write(enumerator.Current.ToString() & " ")
            Loop
            '
            Console.ReadLine()
        End Sub
    End Class
End Namespace
Console Application Output
1 2 3
Deze oplossing functioneert net zo goed en is vele malen eenvoudiger in vergelijking met onze ReturnInstanceOfUserDefinedIEnumerator.BoundedQueue uitwerking.

Een Yield statement is een eigenlijk een speciaal soort Return statement en zal per keer één element van de collectie gaan opleveren.

Elke iteratie van de For Each roept de iterator functie aan.  Als hierin het Yield statement wordt bereikt, wordt de waarde die de expressie naast het Yield keyword uitdrukt, opgeleverd.  De huidige positie in deze uitvoering van deze code wordt ook onthouden.  Bij de volgende iteratie van de For Each (de volgende keer dat MoveNext wordt aangeroepen) wordt verder gegaan van deze onthouden positie.

Het datatype van de expressie die naast het Yield keyword staat moet converteerbaar zijn naar het return type (Current datatype) van de iterator.

35.1.3. IEnumerable(Of T) of IEnumerator(Of T) als Iterator Return Type

Een Yield statement kan enkel gebruikt worden in een iterator functie (of iterator property getter).  Deze member (Function of Property) moet dan ook gemarkeerd zijn met de Iterator modifier.
Het return type van deze member moet IEnumerable, IEnumerable(Of T), IEnumerator of IEnumerator(Of T) zijn.

Nog eens een eenvoudig voorbeeld om de werking van het Yield statement te illustreren:
Visual Basic 2012 Broncode - Codevoorbeeld 834
Class IteratorFunctionAsIEnumerableExample
    Public Shared Sub Main()
        Dim nbs As IEnumerable(Of Integer) = Numbers()
        For Each number As Integer In nbs
            'or: For Each number As Integer In Numbers()
            '
            Console.WriteLine(number)
            '
        Next
        '
        Console.ReadLine()
    End Sub
    Public Shared Iterator Function Numbers() As IEnumerable(Of Integer)
        Yield 10
        Yield 20
    End Function
End Class
Console Application Output
10
20
Bij het initiëel evalueren van Iterator Function Numbers, bij de eerste iteratie van de For Each, wordt aangegeven via het Yield statement dat 10 het eerste element is die benaderd wordt.  De uitvoering van de Numbers functie wordt verder onderbroken.  Pas bij de volgende iteratie van de For Each wordt verder gegaan daar waar vorige iteratie halt hield en wordt er dus aangegeven dat 20 de volgende opgeleverde element waarde is.

Het gebruik van IEnumerable(Of Integer) in plaats van IEnumerable zorgt ervoor dat we de elementen niet in Object, maar in dit geval Integer vorm, kunnen benaderen.

35.1.4. Deferred Evaluation

De opgeleverde waardes worden pas bij het itereren bepaald.  Dit kan als voordeel hebben dat afhankelijk van de iteratielogica niet alle waardes van de verzameling worden bepaald.

Zo zal in volgend voorbeeld 10 fibonacci getallen worden berekend omdat we slecht over de eerste 10 getallen gaan itereren.
Visual Basic 2012 Broncode - Codevoorbeeld 835
Class IteratorFunctionAsIEnumerableExample
    Public Shared Sub Main()
        Dim enumerator As IEnumerator(Of Integer) = Fibo().GetEnumerator()
        Dim count As Integer
        Do While count < 10 AndAlso enumerator.MoveNext()
            Console.Write(enumerator.Current & " ")
            count += 1
        Loop
        '
        Console.ReadLine()
    End Sub
    Public Shared Iterator Function Fibo() As IEnumerable(Of Integer)
        Yield 1
        Yield 1
        Dim value1 As Short = 1
        Dim value2 As Short = 1
        Dim count As Integer = 2
        Do
            Dim backup As Integer = value2
            value2 += value1
            Yield value2
            value1 = backup
            count += 1
        Loop
    End Function
End Class
Console Application Output
1 1 2 3 5 8 13 21 34 55
Bemerk hoe de lus van Iterator Function Fibo oneindig is, er kan hier in theorie een oneindig grote verzameling van waardes worden opgeleverd.

Als je met de debugger stap voor stap door vorig voorbeeld zou lopen zie je goed hoe MoveNext steeds de activatie overbrengt naar de Iterator en hoe de control wordt teruggeven naar Main bij een Yield statement in deze Iterator.  Als MoveNext opnieuw de activatie overbrengt naar Fibo wordt de draad opgepikt waar de laatste Yield de control had teruggegeven.

35.1.5. State Machine

Het belangrijkste voor het begrip en gebruik van het Yield statement voor het creëren van iterators is reeds verteld.  Om toch nog wat meer inzicht te verschaffen gaan we nog even dieper in op hoe het Yield statement functioneert.

Als de Visual Basic compiler een Iterator method opmerkt, zoals deze in het IteratorFunction.BoundedQueue voorbeeld, dan wordt deze op de achtergrond omgezet naar een "state machine".
In volgend voorbeeld is een vereenvoudigde Visual Basic weergave opgesteld die probeert te schetsen wat voor opbouw in de MSIL door de compiler wordt geïnjecteerd.
Aan de hand van de corresponderende regelnummers (1) tot en met (5) in IteratorFunction.BoundedQueue en volgende ReturnInstanceOfCompilerGeneratedIEnumerator.BoundedQueue wordt aangegeven hoe de omzetting werd geconstrueerd.

De essentie is dat een IEnumerator implementatie wordt gegenereerd, net als een IEnumerable implementatie indien dit het return type is van de Iterator method.  Vanaf hier noemen we deze implementatie simpelweg de "iterator".  De iterator houdt de toestand ("state") van de iteraties bij en zorgt ervoor dat correct wordt overgegaan naar een volgende state.
De Iterator method zelf zal dan een instantie van die IEnumerator (en IEnumerable) implementerende datatype opleveren:
Visual Basic 2012 Broncode - Codevoorbeeld 836
Namespace ReturnInstanceOfCompilerGeneratedIEnumerator
    Class BoundedQueue : Inherits UnEnumerable.BoundedQueue
        Implements IEnumerable
        Public Sub New(ByVal capacity As Integer)
            MyBase.New(capacity)
        End Sub
        Public Overridable Function GetEnumerator() As IEnumerator _
                                           Implements IEnumerable.GetEnumerator
            GetEnumerator = New BoundedQueueEnumerator(0, Me)
        End Function
        Class BoundedQueueEnumerator : Implements IEnumerator
            Private _State As Integer
            Private _Current As Object
            Private _Collection As BoundedQueue
            Public Sub New(state As Integer, collection As BoundedQueue)
                _State = state
                _Collection = collection
            End Sub
            Public Function MoveNext() As Boolean _
                                                Implements IEnumerator.MoveNext
                Static index As Integer                                   ' (1)
                Select Case _State
                    Case 0
                        _State = -1
                        index = 0                                         ' (2)
                        Do While index < _Collection.Count                ' (3)
                            _Current = _Collection._Items(index)          ' (4)
                            _State = 1
                            Return True
proceedDoLoop:
                            _State = -1
                            index += 1                                    ' (5)
                        Loop
                    Case 1
                        GoTo proceedDoLoop
                End Select
            End Function
            Public ReadOnly Property Current As Object _
                                                 Implements IEnumerator.Current
                Get
                    Current = _Current
                End Get
            End Property
            Public Sub Reset() Implements IEnumerator.Reset
            End Sub
        End Class
    End Class
    Class Client
        Public Shared Sub Main()
            Dim boundedQueue1 As New BoundedQueue(5)
            boundedQueue1.Enqueue(1)
            boundedQueue1.Enqueue(2)
            boundedQueue1.Enqueue(3)
            '
            For Each element As Object In boundedQueue1
                Console.Write(element.ToString() & " ")
            Next
            '
            Console.ReadLine()
        End Sub
    End Class
End Namespace
Console Application Output
1 2 3
Itereren aan de hand van For Each in de Client over boundedQueue1 zorgt ervoor dat de GetEnumerator() method aangeroepen.  Een For Each als:
For Each element As Object In boundedQueue1
    Console.Write(element.ToString() & " ")
...
Wordt immers omgezet naar iets als:
Dim enumerator As IEnumerator = boundedQueue1.GetEnumerator()
Do While enumerator.MoveNext()
    Console.Write(enumerator.Current.ToString() & " ")
...
De call naar GetEnumerator() zal een iterator instantiëren.  Bemerk hoe aan de constructor voor parameter state waarde 0 wordt doorgegeven, dit om aan te geven dat vanuit de initiële toestand wordt gestart.
Aan de constructor wordt hier ook nog een referentie van het orginele IEnumerable object (Me) doorgegeven aan de gecreëerde iterator.  Hierdoor kan de iterator in zijn implementaties, in hoofdzaak in de MoveNext method, gebruik maken van informatie van het orginele IEnumerable object (regels (3) en (4)).
Doordat BoundedQueueEnumerator in BoundedQueue is genest, kan BoundedQueueEnumerator ook gebruik maken van de ingekapselde members van BoundedQueue (regel (4)).

Het iterator type heeft naast _BoundedQueue nog twee velden.  _State dient om tussen verschillende uitvoeringen van MoveNext door bij te houden waar in de iteratie logica MoveNext bij de volgende uitvoering moet verder gaan.  Nadat bijvoorbeeld op regel (4) de return waarde werd ingesteld, hiervoor wordt het overige veld _Current gebruikt, wordt _State ingesteld op 1 en de huidige uitvoering via Return gestopt.  Een volgende call naar MoveNext weet nu aan de hand van deze _State waarde 1, de corresponderen Case 1 en GoTo instructie waar in de iteratie logica er moet worden verden gegaan.
We merken op hoe de "state machine" logica dus is opgenomen in de MoveNext method.  De implementatie van MoveNext is eigenlijk altijd een Select Case die op basis van de ingestelde toestand (_State) zal beslissen hoe de iteratie logica moet vervolgen.

Bovenstaande schets is sterk vereenvoudigd, gebruik eventueel zelf ILDasm of Reflector om na te gaan hoe een volledige Iterator vertaling er uitziet.
Een uitstekend artikel over iterators en de automatisch gegenereerde state machines vind je terug op onderstaande link.  De uitleg wordt gegeven aan de hand van C# voorbeelden maar is ook van toepassing op de nieuwste versie van Visual Basic.

Dit artikel is gepubliceerd op maandag 15 oktober 2012 op vbvoorbeelden, bezoek de website voor een recente versie van dit artikel of andere artikels.