Jeg har brugt en del tid på at tale med flere kolleger om IEnumerable<T>, der jo, som følge af LINQ, har fået en langt mere central placering i vores .NET-værktøjskasse.
En af de misforståelser, der synes at dukke op i forbindelse med IEnumerable<T>, er, at betragte den som en container. IEnumerable<T> er ikke en container, har aldrig været det, og det har LINQ ikke ændret ved. Punktum.
Det er sandt, at mange containere som f.eks. List<T>, T[] osv. af indlysende årsager implementerer IEnumerable<T>, så vi har mulighed for at betragte containeren som en sekvens og løbe elementerne igennem, men det omvendte er ikke nødvendigvis sandt. En IEnumerable<T> behøver ikke at være en container. Nedenstående metode returnerer IEnumerable<int> men ikke en container:
public IEnumerable<int> EndlessStreamOfOnes() {
while (true) yield return 1;
}
Forskelle
Med en container ved vi, at selve containeren samt dens elementer findes. Så hvis en metode returnerer en container, ved vi, at hele containeren og dens indhold vil være til stede, når vi får vores returværdi. Vi ved også, at det er eller i det mindste har været mulig at ændre indholdet af containeren (ReadOnlyCollection<T> er en container, der giver runtime-fejl, hvis vi forsøger at ændre indholdet). Og sidst, men ikke mindst ved vi, at en container har en endelig størrelse.
Det eneste, vi ved om en IEnumerable<T>, er, at vi kan gennemløbe dens elementer. Vi kan ikke tilføje eller fjerne elementer til sekvensen. Vi ved ikke om elementerne i sekvensen findes allerede, eller om de produceres i forbindelse med gennemløb. Vi ved heller ikke hvor mange elementer, der er tale om, og i princippet kan sekvensen være uendelig som i eksemplet ovenfor (hvilket overlader ansvaret for at udvælge det rette antal elementer til brugeren).
Der er altså muligvis subtile, men nok så væsentlige forskelle mellem en container og en IEnumerable<T>.
Databaseanalogien
Hvilket billede kan vi så bruge for at beskrive, hvad det vil sige, at en type implementerer IEnumerable<T>? Jeg har brugt en database cursor som model, men det er strengt taget forkert. IEnumerable<T> giver os nemlig ikke mulighed for løbe noget som helst igennem. IEnumerable<T> specificerer reelt kun en metode, nemlig GetEnumerator().
GetEnumerator() returnerer en instans af en type, der implementerer IEnumerator<T>, og det er denne instans, der faktisk tillader os at gennemløbe sekvensen. Til mit forsvar vil jeg sige, at compilerens mange tiltag for at gøre arbejdet med disse to interfaces så let så mulig har sløret billedet en hel del, og derfor ser det ud som om, at det er IEnumerable<T>, der er cursoren, men det er det altså ikke.
Jon Skeet har i et svar på StackOverflow forsøgt sig med en lignende analogi. Han sammenligner IEnumerable<T> med en tabel og IEnumerator<T> med en cursor. Det er oplagt, at sidstnævnte er et bedre valg end mit, men i forhold til at komme førnævnte misforståelse til livs er en tabel et dårligt billede på IEnumerable<T>. Vi kan jo netop ikke manipulere indholdet via IEnumerable<T>.
Jeg savner derfor en bedre model til at forklare IEnumerable<T> og IEnumerator<T> og forslag modtages gerne.