Laden...

Hilfreiche Iteratoren / Improving Foreach

Erstellt von herbivore vor 18 Jahren Letzter Beitrag vor 17 Jahren 13.141 Views
herbivore Themenstarter:in
49.485 Beiträge seit 2005
vor 18 Jahren
Hilfreiche Iteratoren / Improving Foreach

Hallo Community,

vorne weg: Der Code entstand zu Zeiten von .NET 2.0. Die hier vorgestellten Iteratoren haben eine ganze Reihe von Parallelen zu LINQ und ermöglichen ähnlich eleganten Code. Mit LINQ schrumpfen entsprechend viele der Vorzüge der Iteratoren. Sie bleiben aber für alle interessant, die noch nicht mit .NET 3.5 arbeiten und für alle, die hinter die Kulissen blicken wollen.

es hat mich gefreut und motiviert, dass meine beiden Enumeratoren (siehe foreach iteration gleichzeitig über mehrere arrays und Objekte identifizieren) so gut angekommen sind. Deshalb habe ich diese Enumeratoren (=Iteratoren) komplett überarbeitet und (angeregt durch Eric Gunnerson, s.u.) weitere nützliche Iteratoren geschrieben, die den Umgang mit foreach-Schleifen noch bequemer machen. Diese Iteratoren stelle ich in diesem Thread zur Verfügung. Den vollständigen Source-Code der Iteratoren und den vollständigen Source-Code eines Demoprogramms findet ihr als Dateianhang am Ende dieses Beitrags. Der Source-Code lässt sich sowohl unter .NET 1.1 als auch unter 2.0 übersetzen.

Hinweis: Gegenüber der letzten Version wurde in Version 0.04 IsoIter.Isolate in IsoIter.All und Iter.StreamReader in Iter.LinesOf umbenannt.

Hier ein paar Beispiel zur Benutzung der Iteratoren. Fangen wir mit eine einer foreach-Schleife über zwei (oder auch mehr) ArrayLists an. Was man für abgefahrene Sachen machen kann, wenn man die Iteratoren verkettet, kommt dann weiter unten. Ich sage nur schon mal: Lottozahlen.


foreach (String str in Iter.Join (al, al2)) {
   Console.WriteLine (str);
}

Eine foreach-Schleife über ausgewählte Elemente der Aufzählung. In Windows-Forms könnte man damit u.a. eine foreach-Schleife über ausgewählte Controls eines Forms (z.B. alle TextBoxen) realisieren. Hier kommt aber erstmal ein Beispiel für Strings:


// Es werden nur die Elemente der Aufzählung aufgezählt,
// für die die Filter-Bedingung true liefert
foreach (String str in Iter.Filter (al, new Predicate (EvenLength))) {
   Console.WriteLine (str);
}

// hier die Filter-Bedingung dazu
// Liefert true, wenn die Länge des String gerade ist
private static bool IsEvenLength (Object obj)
{
   return ((String)obj).Length % 2 == 0;
}

Neben dem Iter.Filter gibt es auch ein Iter.While, der auch eine Bedingung (Predicate) übergeben bekommt und solange aufzählt, solange die Bedingung erfüllt ist. Iter.Filter bzw. Iter.While ließen sich sicher auch durch eine entsprechende Abfrage in der foreach-Schleife ersetzen, mit continue bzw. break, wenn die Begingung nicht erfüllt ist. Aber da man die Iteratoren auch verketten kann, wäre das nur ein schlechter Ersatz.

Oft will man Aufzählungen bei der Ausgabe sortieren. Dafür gibt es die folgenden beiden Iteratoren:


// Hashtable nach Keys sortiert ausgeben geht jetzt
// so einfach wie in Perl ...
foreach (String str in IsoIter.Sort (ht.Keys)) {
   Console.WriteLine ("{0,-7} = {1,2}", str, ht [str]);
}

// ... statt so umständlich wie in C#
String [] astrKeys = new String [ht.Count];
ht.Keys.CopyTo (astrKeys, 0);
Array.Sort (astrKeys);
foreach (String str in astrKeys) {
   Console.WriteLine ("{0,-7} = {1,2}", str, ht [str]);
}

Und hier der zweite für Sortierung nach Werten statt Keys:


// Nach Werten sortiert
foreach (String str in IsoIter.SortDictValue (ht)) {
   Console.WriteLine ("{0,-7} = {1,2}", str, ht [str]);
}

Wenn man nur einen Teil der Aufzählung durchgehen oder die Aufzählung in umgekehrter Reihenfolge durchgehen will, musste man bisher auf eine for-Schleife ausweichen. Jetzt geht das bequem mit foreach:


// Die Liste ohne das erste und die beiden letzten Elemente ...
foreach (String str in IsoIter.Trim (al, 1, 2)) {
   Console.WriteLine (str);
}

// oder - was hier dasselbe ist - drei Elemente ab dem Index eins
foreach (String str in IsoIter.Range (al, 1, 3)) {
   Console.WriteLine (str);
}

// Die Liste in umgekehrter Reihenfolge ausgeben
foreach (String str in IsoIter.Reverse (al)) {
   Console.WriteLine (str);
}

Und das beste: man kann diese Iteratoren beliebig kombinieren. (Für das Lottozahlenbeispiel fehlen uns noch ein paar Iteratoren, deshalb kommt es erst weiter unten.)


// Die Liste ohne das erste und die beiden letzten Elemente in
// umgekehrter Reihenfolge (also absteigend) sortieren
foreach (String str in IsoIter.Reverse (IsoIter.Sort (IsoIter.Trim (al, 1, 2)))) {
   Console.WriteLine (str);
}

Diese und einige fortgeschrittene Beispiele finden sich auch im Main des kompletten Source-Codes (s.u.).

Alle Iteratoren haben die nette Eigenschaft, dass man den Index des momentanen Elements abfragen und verwenden kann.


// Paralles Durchgehen von zwei (gleichlangen) ArrayLists
IIterator iter;
foreach (String str in iter = Iter.All (al)) {
   Console.WriteLine (str);
   Console.WriteLine ("   " + al2 [iter.CurrentIndex]);
}

Die Iteratoren, die mit 'Iso' beginnen, sind isolierend, d.h. die iterierte Aufzählung darf in der foreach-Schleife verändert werden - was normalerweise ja nicht erlaubt ist.


// Die Hashtable wird während der Aufzählung verändert
foreach (String str in IsoIter.All (ht.Keys))
{
   if ( (int) ht [str] == 0) {
      ht.Remove (str);
   }
}

Die Isolation wird um den Preis erreicht, dass die Aufzählung selbst in eine interne ArrayList dupliziert wird. Um diesen Aufwand zu vermeiden, gibt viele Iteratoren in einer nicht isolierten Version, die ansonsten genauso wirken, wie ihre isolierenden Pendants. Manche Iteratoren gibt es sogar nur in einer nicht isolierenden Version, wie z.B. Iter.For, mit dem man aus foreach-Schleifen komfortable for-Schleifen machen kann:


// 10 mal
Console.WriteLine ();
foreach (int i in Iter.For (10)) {
   Console.WriteLine (i);
}
// Ausgabe von 0 bis 9

// Von 1 bis 10
Console.WriteLine ();
foreach (int i in Iter.For (1, 10)) {
   Console.WriteLine (i);
}
// Ausgabe von 1 bis 10

// Von 1 bis 100 alle 5
Console.WriteLine ();
foreach (int i in Iter.For (1, 100, 5)) {
   Console.WriteLine (i);
}
// Ausgabe von 1, 6, 11, ..., 96

Einen Iterator habe ich mir bis jetzt aufgehoben, weil dessen Verwendung vermutlich ziemlich spezifisch ist, aber in den entsprechenden Spezialfällen (Spielen 🙂 nicht weniger nützlich:


// Gibt die die Liste in zufälliger Reihenfolge aus
foreach (String str in IsoIter.Shuffle (al)) {
   Console.WriteLine (str);
}

So, jetzt haben wir alle Iteratoren zusammen und können und dem Lottozahlenbeispiel zuwenden, das sechs zufällige Lottozahlen ausgibt. Ich denke, kürzer geht das nicht mehr!


// Kombination von mehreren Iteratoren
// Ausgabe von sechs zufõlligen Lottozahlen (6 aus 49)
foreach (int i in IsoIter.Range (IsoIter.Shuffle (Iter.For (1, 49)), 0, 6)) {
   Console.WriteLine ("{0,2}", i);
}

In Version 0.03 ist der Iter.StreamReader hinzugekommen, der in Version 0.04 in Iter.LinesOf umbenannt wurde. Damit lassen sich Dateien bequem zeilenweise verarbeiten:


// Zeilen zählen
iNumLines = 0;
foreach (String str in IsoIter.LinesOf ("read.me")) {
   ++iNumLines;
}
Console.WriteLine (iNumLines);

Ich habe mich von den Klassen im Abschnitt "Improving Foreach" in "Tips and Tricks" von Eric Gunnerson inspirieren lassen.

Allerdings waren dort noch einige Fehler enthalten (z.B. funktioniert IterReverse nur, wenn es in einer Verkettung ganz außen steht) und einige Parametertypen waren zu spezifisch (Hashtable statt IDictionary). Außerdem ließen sich die Klassen durch das Weglassen der inneren Klassen und anderen Änderungen deutlich vereinfachen. Zu guter Letzt war der Aufwand für den IterRandom quadratisch, was sich bei großen Aufzählungen störend bemerkbar machen kann. Mein IsoIter.Shuffle hat lineare Laufzeit.

Das alles soll aber keinesfalls die geniale Idee und den Verdienst von Eric Gunnerson schmälern. Ohne ihn gäbe es diesen Beitrag vermutlich gar nicht. Und vielleicht findest ihr ja auch noch ein paar Fehler oder Verbesserungsmöglichkeiten bei meinen Klassen. 🙂

Ein Wort noch zu SW_STRICT, das als Schalter im Source-Code verwendet wird. Alle isolierenden Iteratoren teilen sich die ArrayList alItems, so dass bei Verkettung der Iteratoren diese ArrayList nur einmal angelegt wird. Bei SW_STRICT wird durch alle Iteratoren immer diese originale alItems modifiziert. Lässt man den Schalter weg, werden einige Performance-Verbesserungen aktiviert, die dieses Prinzip durchbrechen. Das wirkt sich jedoch überhaupt nur dann aus, wenn man die Iteratoren verkettet und außerhalb von foreach-Schleifen verwendet.

herbivore

Schlagwörter: foreach, Enumeration, enumerieren, Aufzählung, aufzählen, Collection, Iterator, iterieren, Enumerator, IEnumerator<T>, IEnumerable <T>, 1000 Worte

herbivore Themenstarter:in
49.485 Beiträge seit 2005
vor 17 Jahren

Hallo Community,

es gibt eine neue Version. Aktuell ist Version 0.04.

Den vollständigen Source-Code der Iteratoren und den vollständigen Source-Code eines Demoprogramms findet ihr als Dateianhang am Ende dieses Beitrags.

Gegenüber der letzten geposteten Version 0.03 gab es folgende Änderungen:*IsoIter.Isolate wurde in IsoIter.All umbennant. *Iter.StreamReader wurde in Iter.LinesOf umbennant. *Neu hinzugekommen sind Iter.All, Iter.Range (IsoIter.Range gab es schon in Version 0.03) und IsoIter.LinesOf. *ZUsätzlich zu dem Iterface IIterator gibt es jetzt noch die spezifischen Interfaces IIsolatedIterator und IDirectIterator. *Die Methoden der Iter- bzw. IsoIter-Klasse haben entsprechend das zugehörige spezifische Interface als Rückgabetyp. *Die Iteratoren implementieren jetzt die spezifischen Interfaces. *Neue Klasse IterDirectBase als Oberklasse aller direkten Interatoren hinzugefügt. *IsoIterTrim ist jetzt tollerant gegen Timm-Werte, die außerhalb des Array-Bereichs liegen oder sich überlappen würden. *IsoIterRange ist jetzt tollerant gegen Range-Werte, die außerhalb des Array-Bereichs liegen. *Iter.Filter, Iter.Join und Iter.While implementieren jetzt auch Current, CurrentIndex und GetEnumerator. *In IterStreamReader wird CurrentIndex nicht mehr erhöht, wenn weiter MoveNext aufgerufen wird, obwohl das Ende der Datei schon ereicht wurde. *In Iter.For wird der automatischen Vergabe von Step dieser Wert jetzt auch dann richtig gesetzt, wenn From > To.

Solltet ihr die Version 0.01 oder 0.02 haben, gibt es eine Reihe weiterer inkompatibler Änderungen - inbesondere der Iterator-Namen -, die ich hier nicht aufliste. Ab Version 0.04 halte ich die Schnittstelle für einigermaßen stabil.

herbivore

PS: Fehler und Ungereimtheiten sind natürlich nicht ausgeschlossen. Für entsprechende Hinweise bin ich dankbar.