Collecties aanpassen in een foreach-loop

 
02 april 2008

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?


Werken met ?
Kijk dan bij onze mogelijkheden voor starters en/of ervaren IT'ers.


Categorieën: Development, .Net

Tags: , , ,


Reacties (6)

  • Ralf Wolter schreef:

    Betaald om Roby te promoten? Nee, maar ik vind het wel een erg leuke taal. Het is niet de enige taal die ik leuk vind, maar ik neig wel meer naar dynamische talen, dus weg van C# en Java.

    Maar om het even compleet te maken: Veel van de voorgestelde constructies moken niet uit functionele talen, dus een orginele functionele oplossing kan niet ontbreken:

    lists:map(fun(X) -> string:to_upper(X) end, [“Edwin”, “Jeroen”, “Wilco”]).

    Dit is de Erlang oplossing. Nu nog iemand vinden die mij ervoor wil betalen om dit te promoten. ^_~

    Geplaatst op 03 april 2008 om 9:55 Permalink

  • Goede post! Collectie kopiëren, helemaal mee eens.

    Ralf, krijg jij geld om Ruby te promoten? ;)

    Geplaatst op 03 april 2008 om 8:09 Permalink

  • Ralf Wolter schreef:

    Volgens mij is het ook gewoon een goede programeer stijl om een nieuwe collectie te creeren. De aangepaste waardes staan dan los van de orginele waardes en eventueel nog bestaande gebruikers van de collectie komen niet voor verrassingen te staan.

    Bij entiteiten is het beter te verdedigen dat het object zelf verandert. Persoon ‘Edwin’ blijft namelijk persoon ‘Edwin’, ook al verandert het adres of iets dergelijks. Het is weer een minder goed idee om velden aan te passen die identificerend zijn zoals velden die gebruikt worden in hashcode of equals.

    In andere woorden: zelfs als het zou kunnen, is het niet noodzakelijk een goed idee om het ook te doen.

    In ruby zijn strings bijvoorbeeld niet immutable. Ik kan

    [“Jeroen”, “Edwin”, “Wilco”].each{ | e | e.upcase! }

    schrijven. Dit past de collectie zelf aan. Toch gebruik ik zelf liever de collect versie.

    Dit als kleine introductie voor het aankomende seminar. De 15de meer … ^_^

    Geplaatst op 02 april 2008 om 14:05 Permalink

  • Ralf geeft denk ik inderdaad het punt aan waar het om gaat. Als ik het goed begrijp is het dus zo dat omdat de String immutable is je eigenlijk gewoon een nieuw object toevoegt aan je collectie, ook al hergebruik je de referentie. Wat dat betreft is het ook logisch dat het niet geaccepteerd wordt aangezien je dus toch eigenlijk nieuwe objecten toevoegd aan je collectie, wat onwenselijk is.

    Dus het probleem ligt niet alleen aan de foreach, maar ook aan de “immutable” string. Zelf heb ik al een klasse Test met property String testString gemaakt. Als ik dan een List maak, is het inderdaad wel mogelijk om in een foreach t.testString =”gewijzigde string” uit te voeren, omdat ik dan dus geen nieuw object aanmaak (ook niet onderhuids). Als ik in de foreach t =new Test(); zet, levert dit dus terecht weer een compile-error op.

    Bedankt voor de reacties, het een en ander is weer stukken helderder geworden voor mij (en misschien ook wel anderen).

    Geplaatst op 02 april 2008 om 13:29 Permalink

  • Ralf Wolter schreef:

    Volgens mij gaat ’t niet de originele collectie aanpassen. String is een imutable type en ForEach gaat ervan uit dat je het object achter de referentie kan manipuleren, niet direct de referentie zelf.

    De correcte methode is volgens mij:

    result = names.Select(naam => naam.ToUpper());

    Het past dus niet de bestaande collectie aan, maar maakt een nieuwe en dat is ook het gedrag dat je graag zou willen. Dit soort zaken moet je nauurlijk ook niet in c# doen. In Ruby zou het

    [“Jeroen”, “Edwin”, “Wilco”].collect { | e | e.upcase }

    zijn. Als je dan toch in .net wil blijven, kan IronPython een soort gelijke versie…

    Geplaatst op 02 april 2008 om 12:51 Permalink

  • Ivo Limmen schreef:

    Het feit dat een foreach loop een collectie read-only maakt vind ik niet vreemd. Als Javaan is deze foreach (de ‘enhanced for loop’ in Java) geïntroduceerd in JDK 1.5 en hoger. Bij het introduceren is ook gekozen voor een Iterator bij het gebruik van de ‘enhanced for loop’ en deze is altijd read-only geweest. De keuze in mijn optiek is ook niet onlogisch geweest. Hoe zou de Iterator moeten werken als er gedurende het itereren een element werd toegevoegd? En wat als er een element werd toegevoegd gedurende het itereren terwijl de collectie gesorteerd is en het element bevind zich voor het huidige element?
    Er zijn nog veel meer vragen als deze waarom ikzelf ook zou kiezen voor een read-only for loop. Ik moet hierbij dan ook erkennen dat gewenning ook mee speelt; ik gebruikte altijd een standaard for loop met index en dan is een collectie gewoon te wijzigen.

    Geplaatst op 02 april 2008 om 11:58 Permalink