foreach mit Index (Laufvariable)
-
Hallo.
Das Iterator-Pattern ist mit IEnumerable tief in .NET verankert und ich möchte mich auch daran halten. Trotzdem benötige ich übermäßig oft eine Laufvariable die in irgendwelche Berechnungen einfließt. Raus kommen solche Monster:
IEnumerable<string> myList = getEnumerableFromHell(); int index = 0; foreach (var item in myList) { workload(item, index); ++index; }
Ich finde das hässlich und nervig. Außerdem verpestet index den Scope der Methode. Sehr häufig neige ich dann dazu auf IEnumerable zu verzichten und stattdessen konkrete Typen (z.B. List) zu verwenden um mit einer klassischen for-Schleife zu iterieren. Nur wegen dem Index!
Welche elegante Möglichkeit fällt euch ein?
Hier mein erster Versuch. Hab das ganze noch ein wenig aufgebohrt um Startindex, Stepping und eine Func<int,int> als Index-Transformation angeben zu können.
using System; using System.Linq; using System.Collections.Generic; namespace IndexForeach { class Program { static void Main(string[] args) { var myList = new List<string>(new[] { "Hallo", "wie", "gehts", "denn", "so" }); Console.WriteLine("---> Standardindizierung"); foreach (var pair in myList.ToIndexedEnumerable()) Console.WriteLine("index = {0}, value = {1}", pair.Index, pair.Item); Console.WriteLine("\n---> Ungerade Zahlen: Startindex = 1, Step = 2"); foreach (var pair in myList.ToIndexedEnumerable(1, 2)) Console.WriteLine("index = {0}, value = {1}", pair.Index, pair.Item); Console.WriteLine("\n---> Quadratzahlen als Index"); foreach (var pair in myList.ToIndexedEnumerable(1, i => i * i)) Console.WriteLine("index = {0}, value = {1}", pair.Index, pair.Item); Console.ReadKey(true); } } static class Extensions { public static IndexedEnumerable<T> ToIndexedEnumerable<T>(this IEnumerable<T> collection) { return new IndexedEnumerable<T>(collection, 0, 1, null); } public static IndexedEnumerable<T> ToIndexedEnumerable<T>(this IEnumerable<T> collection, int start, int step) { return new IndexedEnumerable<T>(collection, start, step, null); } public static IndexedEnumerable<T> ToIndexedEnumerable<T>(this IEnumerable<T> collection, int start, Func<int, int> indexMapper) { return new IndexedEnumerable<T>(collection, start, 1, indexMapper); } } class IndexedItem<T> { public T Item; public readonly int Index; public IndexedItem(int index, T item) { Index = index; Item = item; } } class IndexedEnumerable<T> : IEnumerable<IndexedItem<T>> { int start, step; IEnumerable<T> enumerable; Func<int, int> indexMapper; public IndexedEnumerable(IEnumerable<T> enumerable, int start, int step, Func<int, int> indexMapper) { this.start = start; this.step = step; this.enumerable = enumerable; this.indexMapper = indexMapper ?? (i => i); } public IEnumerator<IndexedItem<T>> GetEnumerator() { int index = start; foreach (var item in enumerable) { yield return new IndexedItem<T>(indexMapper(index), item); index += step; } } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw new NotImplementedException(); } } }
Bisschen viel Code. Mit dem Konstanten Overhead kann ich leben. Nicht gut fände ich linearen Aufwand gemäß einer IndexOf-Methode.
Bin auf eure Ideen gespannt.
-
Tolle Idee.
Möglich wäre Stepping rausnehmen, das kann durch indexMapper simuliert werden, und anstatt IndexedItem<T> vielliecht KeyValuePair<int, T>.
Also
yield return new IndexedItem<T>(indexMapper(index), item);
durch
yield return new KeyValuePair<int, T>(indexMapper(index), item);
Dies entspräche aber auch (mit Linq)
foreach (var pair in getEnumerableFromHell().Select((element, i) => new KeyValuePair(i, element))) /*...*/;
(ungetestet)
Die üblichen SecurityChecks fehlen aber auch noch.
Ein kommplett anderer Weg wäre noch
//TODO: testen //TODO: Documentation public static void Each<T>(this IEnumerable<T> source, Action<T, int> action) { //TODO: Nullcheck source & action IEnumerator<T> e = source.GetEnumerator(); int i = 0; while(e.MoveNext) action(e.Current, i++); } //Verwendung an deinem Beispiel getEnumerableFromHell().Each((worload, index) => worload(item, index));
-
Hmm ich bin mir nich ganz sicher was ich davon halten soll.
Ich sag mal so: Wenn du nen Index brauchst, dann ist die for-Schleife in meinen Augen die perfekte Wahl.Ich halte dahingegen deins etwas Oversized, zumal du ja jetzt überall wo du deine IndexedEnumerable benutzen willst den Quellcode mit ausliefern oder einbinden musst. Ist es dir das wirklich Wert wegen einer anderen Schleife?
Hässlich hin oder her, die for-Schleife ist ja nichts böses oder so.
-
mylist.Zip(Enumerable.Range(0, <ziemlich viel>), (a,b) => new { item = a; index = b; });
Mein C# ist schon wieder extrem eingerostet und mein LINQ-Buch liegt am Arbeitsplatz, aber ich hoffe, man versteht ungefähr die Idee. Leider gibt es Range standardmäßig nur in endlich, man müsste sich da noch was eigenes bauen.
-
Bashar schrieb:
mylist.Zip(Enumerable.Range(0, <ziemlich viel>), (a,b) => new { item = a; index = b; });
Mein C# ist schon wieder extrem eingerostet und mein LINQ-Buch liegt am Arbeitsplatz, aber ich hoffe, man versteht ungefähr die Idee. Leider gibt es Range standardmäßig nur in endlich, man müsste sich da noch was eigenes bauen.
Es geht einfacherer:
getEnumerableFromHell().Select((e, i) => new (index = i, element = e)))
Die Select-Methode ruft die Transformationsfunktion, der zweite Parameter, schon mit dem jeweiligen Index auf.
-
Rhombicosidodecahedron schrieb:
Tolle Idee.
Danke
Ich lese eure Vorschläge interessiert.
Aber Linq-Konstrukte wie hier vorgestellt sind zu viel Schreibarbeit. Die einfachste, natürlichste Erweiterung für foreach ist gesucht.Firefighter schrieb:
Ich sag mal so: Wenn du nen Index brauchst, dann ist die for-Schleife in meinen Augen die perfekte Wahl.
Wenn aber wirklich nur ein IEnumerable und keine List o.Ä. zur Verfügung steht, ist eine for-Schleife inakzeptabel.
-
Rhombicosidodecahedron schrieb:
Die üblichen SecurityChecks fehlen aber auch noch.
]Welchen sinnvollen Fehlerfall, der sich nicht schon mit einer bekannten Exception bemerkbar macht, wäre denn noch abzufangen?
-
Rhombicosidodecahedron schrieb:
Die Select-Methode ruft die Transformationsfunktion, der zweite Parameter, schon mit dem jeweiligen Index auf.
Danke
Aber typisch MS mal wieder ...
-
Firefighter schrieb:
Ich halte dahingegen deins etwas Oversized, zumal du ja jetzt überall wo du deine IndexedEnumerable benutzen willst den Quellcode mit ausliefern oder einbinden musst. Ist es dir das wirklich Wert wegen einer anderen Schleife?
Das ist eigentlich ein interessanter Punkt. Hältst Du es für fragwürdig solchen Code in jedes Projekt, das IndexedEnumerable verwendet, einzubinden? Warum? Und Oversized: Stimmt schon. Aber schau Dir mal den LINQ-Overhead im Allgemeinen an. Für performancekritischen Code würde ich sowas auch nicht verwenden.
Jedes größere Projekt hat Tool-, Helper-Klassen oder (in C#) Extension-Methoden. Ich habe z.B. eine Reihe Extensions für string und es tat mir bisher nicht weh, diese in alle Projekte einzubinden.
-
Und jetzt nochmal in "kurz". Danke für die Inspiration
using System; using System.Linq; using System.Collections.Generic; namespace IndexForeach { class Program { static void Main(string[] args) { var myList = new List<string>(new[] { "Hallo", "wie", "gehts", "denn", "so" }); Console.WriteLine("---> Standardindizierung"); foreach (var pair in myList.ToIndexedEnumerable()) Console.WriteLine("index = {0}, value = {1}", pair.Key, pair.Value); Console.WriteLine("\n---> Ungerade Zahlen: Startindex = 1, Step = 2"); foreach (var pair in myList.ToIndexedEnumerable(1, 2)) Console.WriteLine("index = {0}, value = {1}", pair.Key, pair.Value); Console.WriteLine("\n---> Quadratzahlen als Index"); foreach (var pair in myList.ToIndexedEnumerable(1, i => i * i)) Console.WriteLine("index = {0}, value = {1}", pair.Key, pair.Value); Console.ReadKey(true); } } static class Extensions { public static IEnumerable<KeyValuePair<int, T>> ToIndexedEnumerable<T>(this IEnumerable<T> collection) { return collection.ToIndexedEnumerable(0, 1, i => i); } public static IEnumerable<KeyValuePair<int, T>> ToIndexedEnumerable<T>(this IEnumerable<T> collection, int start, int step) { return collection.ToIndexedEnumerable(start, step, i => i); } public static IEnumerable<KeyValuePair<int, T>> ToIndexedEnumerable<T>(this IEnumerable<T> collection, Func<int, int> indexMapper) { return collection.ToIndexedEnumerable(0, 1, indexMapper); } public static IEnumerable<KeyValuePair<int, T>> ToIndexedEnumerable<T>(this IEnumerable<T> collection, int start, Func<int, int> indexMapper) { return collection.ToIndexedEnumerable(start, 1, indexMapper); } public static IEnumerable<KeyValuePair<int, T>> ToIndexedEnumerable<T>(this IEnumerable<T> collection, int start, int step, Func<int, int> indexMapper) { return collection.Select((item, index) => new KeyValuePair<int, T>(indexMapper(start + index * step), item)); } } }
---> Standardindizierung
index = 0, value = Hallo
index = 1, value = wie
index = 2, value = gehts
index = 3, value = denn
index = 4, value = so---> Ungerade Zahlen: Startindex = 1, Step = 2
index = 1, value = Hallo
index = 3, value = wie
index = 5, value = gehts
index = 7, value = denn
index = 9, value = so---> Quadratzahlen als Index
index = 1, value = Hallo
index = 4, value = wie
index = 9, value = gehts
index = 16, value = denn
index = 25, value = so
-
µ schrieb:
Firefighter schrieb:
Ich halte dahingegen deins etwas Oversized, zumal du ja jetzt überall wo du deine IndexedEnumerable benutzen willst den Quellcode mit ausliefern oder einbinden musst. Ist es dir das wirklich Wert wegen einer anderen Schleife?
Das ist eigentlich ein interessanter Punkt. Hältst Du es für fragwürdig solchen Code in jedes Projekt, das IndexedEnumerable verwendet, einzubinden? Warum? Und Oversized: Stimmt schon. Aber schau Dir mal den LINQ-Overhead im Allgemeinen an. Für performancekritischen Code würde ich sowas auch nicht verwenden.
Jedes größere Projekt hat Tool-, Helper-Klassen oder (in C#) Extension-Methoden. Ich habe z.B. eine Reihe Extensions für string und es tat mir bisher nicht weh, diese in alle Projekte einzubinden.
Dieses Argument befördert dich auf das Siegertreppchen. Du hast mich überzeugt. Eigentlich hast du Recht das es nicht so dramatisch ist das einfach mit einzufügen.
-
Ich hätte da mal eine andere Frage: Wozu brauchst du diesen Index regelmässig? Kannst du da Beispiele nennen?
Grüssli
-
Dravere schrieb:
Ich hätte da mal eine andere Frage: Wozu brauchst du diesen Index regelmässig? Kannst du da Beispiele nennen?
Ich brauchte das, um Sequenzen von einer API in eine (nicht ganz so tolle) andere API umzuschaufeln, oder irgendwelche IDs dazu zu generieren, oder einfach um sie auszugeben, sowas in der Art. Klar, dass man das normalerweise nicht brauchen sollte, aber es kommt halt immer mal wieder vor.
-
Was spricht denn gegen eine for-Schleife und einer Liste, wenn du den index benötigst? Ich als Anfänger hätte das einfach folgendermaßen gelöst.
List<string> myList = getEnumerableFromHell().ToList();
-
Noob3453 schrieb:
Was spricht denn gegen [...]
Dass man die Sequenz dazu erstmal kopieren muss. Außerdem: Der Stolz
-
Lesbarkeit geht vor.
Ich habe die hier aufgezeigten Möglichkeiten gesehen und halte sie auch stilvoll usw, aber ich würde lieber zu einer for greifen, ist zum einen bekannter (lernt schon jeder Anfänger) und fördert es eher die Lesbarkeit.Im übrigen, "Sequenz kopieren"? Wenn du eine IEnumerable hast daraus eine Liste machst werden auch nur die Referenzen geholt und gesammelt in einer Liste angeboten. Die Leistung dürfte zu vernachlässigen sein, außer eventuell das alle Referenzen direkt aufgelöst werden.
-
David W schrieb:
Lesbarkeit geht vor.
Ich habe die hier aufgezeigten Möglichkeiten gesehen und halte sie auch stilvoll usw, aber ich würde lieber zu einer for greifen, ist zum einen bekannter (lernt schon jeder Anfänger) und fördert es eher die Lesbarkeit.for(int i = 0; enumerator.MoveNext(); ++i) { // ... }
Hat was
David W schrieb:
Im übrigen, "Sequenz kopieren"? Wenn du eine IEnumerable hast daraus eine Liste machst werden auch nur die Referenzen geholt und gesammelt in einer Liste angeboten. Die Leistung dürfte zu vernachlässigen sein, außer eventuell das alle Referenzen direkt aufgelöst werden.
Mit solchen Aussagen wäre ich vorsichtig. Vor allem wenn es um allgemeine Aussagen geht. Man sollte nie vergessen, dass es auch Linq to SQL gibt. Ein
IEnumerable
könnte plötzlich Abfragen in einer Datenbank machen. DasToList
führt dann alle Abfragen durch, womöglich ungewollt.Grüssli
-
Dravere schrieb:
Ich hätte da mal eine andere Frage: Wozu brauchst du diesen Index regelmässig? Kannst du da Beispiele nennen?
GrüssliIch komme immer wieder in die Situation, dass ich gegen die IEnumerable-Schnittstelle programmiere und mit foreach darüber iteriere. Im Laufe der Entwicklung sich aber zeigt, dass ein Index notwendig ist. Zum Beispiel für IDs oder andere Berechnungen. Bisher hab ich dann oft mit der Schnittstellenprogrammierung gebrochen und auf Lists oder andere konkrete, indizierbare Typen zurück gegriffen. Oder eine Laufvariable außerhalb des foreach-scope eingeführt. Alles nicht das Wahre.
ToIndexedEnumerable() gefällt mir jetzt sehr gut (auch bei der Eingabe: ".ToI" <Tab>. Null Aufwand). Der umfangreiche Code hat mich anfangs noch gestört aber mit ein wenig Inspiration von diesem Thread hat sich das auf ein akzeptables Niveau reduziert. Ich denke mal am Montag fließt das in den Produktivcode ein
Es gibt noch zwei Sonderfälle einer Iteration: Das erste und das letzte Element müssen häufig gesondert von den restlichen Elementen einer Enumeration behandelt werden. Das erste Element kann mit dem Code jetzt abgefangen werden. Der Index steht ja zur Verfügung und man kennt den Startindex. Für das letzte Element hab ich noch keine elegante und performante Lösung. Count() von LINQ ist inakzeptabel wegen dem linearen Aufwand.
Mal schauen....
-
Count() von LINQ ist inakzeptabel wegen dem linearen Aufwand.
Huh? Hat es das auch wenn darunter eine Collection liegt die ihre größte in O(1) weiß? Meiner einer muss am Montag sein Projekt durchsuchen. Hab bestimmt Count() verwendet...
MfG SideWinder
-
Wenn Count() gegen IEnumerable aufgerufen wird, hat es linearen Aufwand. Unabhängig von der konkreten Klasse dahinter.
EDIT: Jetzt komme ich auch ins Schleudern. Wurde Count() für z.B. List überschrieben?