Zoek

Uitgebreid zoeken Artikelen per auteur

  

IEnumerable, de basis voor LINQ

IEnumerable, de basis voor LINQ

LINQ is een van de meest interessante nieuwe ontwikkelingen op het .Net platform. Er is al veel geschreven over lambda expressies en extension methods, de twee technieken die aan C# 3.0 zijn toegevoegd om LINQ mogelijk te maken. In dit artikel gaan we in op het IEnumerable interface. Dit interface is al onderdeel van het .Net platform sinds de eerste versie en maakt een aantal interessante programmeertechnieken mogelijk (waaronder LINQ).

Arrays, IEnumerable en Linq

Een stuk code zegt meer dan duizend woorden. Laten we eens kijken, waar we het in dit artikel over gaan hebben: zie listing 1.

int[ ] eenTotTien = new int[ ]
  { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

foreach( var teller in eenTotTien )
  Console.WriteLine( teller );

var even =
  from n in eenTotTien
  where n % 2 == 0
   select n;

foreach( var evenTeller in even )
  Console.WriteLine( evenTeller );

Listing 1

Een dergelijk array lijkt simpel maar schijn bedriegt. In het voorgaande stuk code doen we al twee dingen die je niet met ieder object kunt doen. De compiler weet blijkbaar hoe je met een foreach-lus door de waarden in een array kunt itereren en zelfs hoe je met LINQ een selectie uit het array kunt maken. Het is interessant om te zien wat er hier onder de motorkap precies gebeurt. De eerste hint krijgen we als we kijken naar de interfaces die door de array worden geimplementeerd. In listing 2 staat het in stijl van een LINQ query.

var interfaces =
  from n in typeof( int[ ] ).GetInterfaces( )
  select n.ToString( );

foreach( var name in interfaces )
 
Console.WriteLine( name );

Listing 2

Een dergelijk array lijkt simpel maar schijn bedriegt

Een array is een stuk krachtiger dan je zou verwachten van een primitief datatype. Er worden zeven interfaces ondersteund en daar hebben we geen letter voor hoeven te programmeren. Op de achtergrond doet het .Net framework een heleboel voor ons.

De interessante interfaces (in de context van dit artikel) zijn IEnumerable en zijn generic broertje IEnumerable. In listing 3 staat het IEnumerator interface uitgeschreven.

public interface IEnumerable
{
  IEnumerator GetEnumerator( );
}

public interface IEnumerator : IEnumerator
{
  T Current { get; }
}

public interface IEnumerator
{
  object Current { get; }
  bool MoveNext( );
 
void Reset( );
}

Listing 3

Het iterator pattern

Een array en andere IEnumerable objecten, zoals de List, hebben een GetEnumerator functie. Met deze GetEnumerator functie kan de enumerator van het object opgevraagd worden. Deze enumerators bevatten functionaliteit om één voor één door de elementen heen te lopen en eventueel met Reset weer opnieuw te beginnen. Hierin valt duidelijk het iterator pattern te herkennen.

Het is dus ook mogelijk om meerdere IEnumerators tegelijk op dezelfde collectie te laten werken zoals te zien in listing 4.

int[ ] eenTotTien = new int[ ]
  { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

foreach( var teller1 in eenTotTien )
  foreach( var teller2 in eenTotTien )
    Console.WriteLine(
      string.Format( "{0} - {1}", teller1, teller2 ) );

Listing 4

Zoals uit listing 5 blijkt, vertaalt de C# compiler onder water de twee foreach-lussen.

int[ ] eenTotTien =
  { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

IEnumerator<int> enumerator1 =
  ( eenTotTien as IEnumerable<int> ).GetEnumerator( );
IEnumerator<int> enumerator2 =
  ( eenTotTien as IEnumerable<int> ).GetEnumerator( );
enumerator1.Reset( );
while( enumerator1.MoveNext( ) )
{
  enumerator2.Reset( );
  while( enumerator2.MoveNext( ) )
  {
    Console.WriteLine(
      string.Format(
        "{0} - {1}",
        enumerator1.Current,
        enumerator2.Current ) );
 
}
}

Listing 5

De twee enumerators op hetzelfde array hebben geen invloed op elkaars positie.

Doe het zelf

We kunnen nu in onze eigen klasses IEnumerable implementeren om sequentiele data beschikbaar te maken. In listing 6 staat dit uitgewerkt.

class EenTotTien : IEnumerable
{
  public IEnumerator GetEnumerator( )
  {
    return new TienEnumerator( );
  }

  public class TienEnumerator : IEnumerator
  {
    private int _current = 0;

    public object Current
    {
      get
      {
         return _current;
      }
    }

    public bool MoveNext( )
    {
      _current++;
      return _current <= 10;
   
}

    public void Reset( )
    {
      _current = 0;
    }
  }
}

Listing 6

Listing 7 toont hoe deze enumerator aangeroepen kan worden.

foreach( var teller in new EenTotTien() )
 
Console.WriteLine( teller );

Listing 7

De EenTotTien klasse implementeert het IEnumerator interface en kan in een foreach-lus gebruikt worden. Alle functionaliteit om dit voor elkaar te krijgen zit in TienEnumerator.  Ik heb hier wel een beetje vals gespeeld. De EenTotTien klasse bevat zelf geen collection maar laat alles over aan de TienEnumerator, en deze berekent de getallen op het moment dat ze nodig zijn.

Functioneel programmeren met Yield Return

Tot nu toe hebben we mooie dingen gezien en worden de voorbeelden steeds langer. Er is een maar: we hebben nu dus twee klassen nodig om simpelweg tot tien te kunnen tellen. Gelukkig is er sinds versie 2.0 van het .Net framework een eenvoudiger manier bijgekomen. C# 2.0 implementeert met het yield return keyword zogenaamde continuations. Een functie met een IEnumerable als return type kan in stukjes worden uitgevoerd. Iedere keer dat een waarde uit de IEnumerable wordt teruggegeven of wordt opgevraagd, wordt precies genoeg van de functie uitgevoerd om de volgende waarde te berekenen. Met listing 8 wordt dit duidelijker.

static IEnumerable<int> TelTotTien( )
{
  for( int nummer = 1; nummer <= 10; nummer++ )
  {
    yield return nummer;
 
}
}

Listing 8

Dit kan op ongeveer dezelfde manier worden aangeroepen als het vorige voorbeeld:

var totTien = TelTotTien( );
foreach( int teller in totTien )
{
  Console.WriteLine( teller );
}

Listing 9

Mocht je nog niet bekend zijn met deze constructie, dan is het misschien een goed idee de voorbeeldcode een keer in de debugger uit te voeren en breakpoints te zetten. Hiermee krijg je een duidelijk beeld van de volgorde waarin de code wordt uitgevoerd. Dit heeft mij geholpen om de door te krijgen wat er gebeurt.

Dit begint een beetje te lijken op wat functionele programmeertalen doen. De IEnumerable kan worden doorgegeven, zonder dat de achterliggende code wordt uitgevoerd. Pas op het moment dat er resultaten worden opgevraagd, wordt deze uitgevoerd. Op deze manier is het mogelijk foreach-lussen aan elkaar te hangen en bewerkingen op sequentiele gegevens toe te passen. We kunnen de twee volgende functies aan het vorige voorbeeld toevoegen: zie listing 10 als resultaat.

static IEnumerable<int>
  AlleenEven( IEnumerable<int> input )
{
  foreach( var nummer in input )
  {
    if( nummer % 2 == 0 )
   
{
      yield return nummer;
    }
  }
}

static IEnumerable<int>
  KeerTwee( IEnumerable<int> input )
{
  foreach( var nummer in input )
  {
    yield return nummer * 2;
 
}
}

Listing 10

Deze kunnen we achter elkaar uitvoeren en zo door het resultaat heen itereren (zie listing 11).

var totTien = KeerTwee( AlleenEven( TelTotTien( ) ) );
foreach( int teller in totTien )
{
  Console.WriteLine( teller );
}

Listing 11

Dit geeft ons de mogelijkheid om heel flexibel filters en operaties toe te passen op rijen gegevens. En deze worden pas uitgevoerd als de resultaten ook daadwerkelijk nodig zijn.

En dan nu ... LINQ

Wat het vorige voorbeeld laat zien, lijkt eigenlijk heel erg op wat LINQ doet. In de System.Linq namespace is een klasse Enumerable aanwezig met een hele verzameling extension methods op IEnumerable. Deze kunnen precies zo gebruikt worden als in het vorige voorbeeld. Het enige dat de C# compiler nog hoeft te doen, is de LINQ syntax vertalen naar deze functies. Als je het allereerste voorbeeld in bijv. de reflector stopt, dan zie je het volgende  resultaat:

IEnumerable<int> even =
  eenTotTien.Where<int>( anonymousDelegate );

Listing 12

De where clause is vertaald naar een anonymous delegate die wordt meegegeven aan een where clause. De Where is een weer extension method op IEnumerable, die een IEnumerable teruggeeft.

Conclusie

In dit artikel hebben we met een paar simpele voorbeelden het IEnumerable interface laten zien. Dit interface is voor meer dan alleen arrays of collections interessant. Door het gebruik van dit interface is het mogelijk op een elegante manier flexibele en efficiente code te schrijven. In eerdere versies van het .NET framework was dit nog veel werk, maar door gebruik van yield return en met de komst van LINQ is het implementeren en het gebruiken van het IEnumerable interface een stuk makkelijker en leesbaarder geworden.

We noemden in dit artikel eerder de overeenkomsten met functionele programmeertalen. De grens tussen het resultaat van een bewerking en de bewerking zelf vervaagt. Hierdoor heeft een compiler meer vrijheid om te bepalen wanneer bewerkingen uitgevoerd worden. Ook wordt het makkelijker om bewerkingen in parallel uit te voeren. Met de CTP voor de Parallel Extensions to .Net is dat precies wat Microsoft nu doet. Een onderdeel daarvan is PLINQ, waarmee Linq queries parallel uit te voeren zijn. Voor meer informatie op kijk je op: http://www.microsoft.com/downloads/details.aspx?FamilyID=e848dc1d-5be3-4941-8705-024bc7f180ba&displaylang=en.

Geef feedback:
Verzend Commentaar