Collecties aanpassen in een foreach-loop

In deze blog focussen we op de mogelijkheden om een foreach-constructie te gebruiken om een collectie aan te passen. Het voordeel van een foreach-constructie is dat we niet eerst de grootte van de collectie hoeven op te vragen om door alle elementen heen te gaan en dat we direct een element aangeleverd krijgen waar we iets mee kunnen doen.

Probleem

Om het probleem makkelijk bespreekbaar te maken, gebruiken we een voorbeeld.

We maken een lijst aan met mogelijke kindernamen:

List<String> names = new List<String>();
names.Add("Jeroen");
names.Add("Edwin");
names.Add("Wilco");

Nu vinden we (achteraf) dat de namen in deze lijst zulke belangrijke namen zijn, dat ze wel in hoofdletters hadden mogen staan. We hebben geen zin in overtypen, dus willen we elke naam in de lijst in de hoofdletter-variant veranderen met een foreach loop. De vraag: is dit mogelijk?

Aan de hand van de volgende code-snippets zullen we proberen de vraag te beantwoorden:

Code-snippet A:

foreach (string name in names)
{
name = name.ToUpper();
}

Code-snippet B:

names.ForEach(delegate(String name) { name = name.ToUpper(); });

of met Lambda-Expressie in c# 3.0:

names.ForEach(name =>  name = name.ToUpper());

Code-snippet C:

while(names.GetEnumerator().MoveNext())
{
names.GetEnumerator().Current = names.GetEnumerator().Current.ToUpper();
}

Geen van de code-snippets lost het probleem op. A levert een compile-error op (Cannot assign to ‘name’ because it is a ‘foreach iteration variable’ ), B compileert wel, maar wijzigt niet de lijst en C levert ook een compile-error op (Property or indexer ‘System.Collections.Generic.List<string>.Enumerator.Current’ cannot be assigned to — it is read only ) .

Zowel A en C duiden op hetzelfde probleem, namelijk dat een ‘iterator’ read only is. Over snippet B: volgens MSDN doet de List(T).ForEach(Action<T> action) method het volgende: “The Action<(Of <(T>)>) is a delegate to a method that performs an action on the object passed to it. The elements of the current List<(Of <(T>)>) are individually passed to the Action<(Of <(T>)>) delegate.” . Hierdoor zou men verwachten dat je daadwerkelijk het object zelf (en niet een kopie) aanpast, maar gezien de resultaten moet ik de zin anders interpreteren (reacties zijn welkom!).

Conclusie

Als we B laten voor wat het is, kunnen we concluderen dat het er op lijkt dat een foreach niet de mogelijkheden biedt om een lijst aan te passen, aangezien de iterator read only is. Oplossingen moeten dus worden gezocht in het uitwijken naar een for loop met indexes, een dubbele lijst maken en vervolgens de oorspronkelijke lijst verwijderen of een eigen writable Enumerator te schrijven.

Discussie

.Net heeft er dus klaarblijkelijk voor gekozen om bij een foreach loop de iterator read only te maken, maar de vraag is dan natuurlijk: waarom? Dat we de collectie zelf niet mogen aanpassen in een foreach loop (levert in runtime een InvalidOperationException op (Collection was modified; enumeration operation may not execute.) is een goede keuze, want anders kan je in een endless loop komen door objecten toe te voegen. Maar biedt dat al niet genoeg bescherming, zodat de iterator in een foreach loop wel writable mag zijn? Op het moment dat we een bewerking doen op het element, zal de foreach loop de collectie niet bewerken (in de zin van elementen toevoegen of verwijderen)! Dus van een endless loop is dan geen sprake. Gezien het gemak ervan als het wel mogelijk zou zijn, zou ik ervoor pleiten om iterator in een foreach loop writable te maken (maar nog steeds een exceptie als de collectie wordt aangepast). Wat denk u ervan?