Gedanken zu foreach mit Enumerable.Range vs traditional for loop

74

In C # 3.0 gefällt mir dieser Stil:

// Write the numbers 1 thru 7
foreach (int index in Enumerable.Range( 1, 7 ))
{
    Console.WriteLine(index);
}

über die traditionelle forSchleife:

// Write the numbers 1 thru 7
for (int index = 1; index <= 7; index++)
{
    Console.WriteLine( index );
}

Unter der Annahme, dass 'n' klein ist, sodass Leistung kein Problem darstellt, hat jemand Einwände gegen den neuen Stil gegenüber dem traditionellen Stil?

Marcel Lamothe
quelle
1
Nun ist diese Frage, einschließlich der ZUSAMMENFASSUNG AUS VIELEN ANTWORTEN, WIRKLICH relevant, fast ein Best-Practice-Modell für Fragen in SO. Es sollte gerahmt sein!
Heltonbiker
4
@heltonbiker Falsch --- tatsächlich passt eine Zusammenfassung nicht zum SO-Modell. Die Zusammenfassung sollte entfernt werden. Als Q & A-Site werden Fragen und Antworten getrennt, anstatt eine allgemeine Vorstellung von einem "Beitrag" zu haben.
Keith Pinson
Das Verhalten beider unterscheidet sich je nachdem, mit welcher Version von .Net Sie kompilieren. Die for-Schleifenversion behält möglicherweise nicht immer den Ausführungskontext bei, insbesondere wenn Sie eine Yield-Anweisung haben.
Surya Pratap
Mit Range () gibt es noch einen weiteren Vorteil: Sie können den Wert des Index innerhalb der Schleife ändern, ohne dass die Schleife unterbrochen wird.
Vincent
1
Ich freue mich darauf, diesem Thread neue C # 8.0-Funktionen hinzuzufügen! docs.microsoft.com/en-us/dotnet/csharp/whats-new/…
Andrew

Antworten:

57

Ich finde das "Minimum-to-Maximum" -Format des letzteren viel klarer als Rangeden "Minimum-Count" -Stil für diesen Zweck. Ich denke auch nicht, dass es wirklich eine gute Praxis ist, eine solche Änderung von der Norm vorzunehmen, die nicht schneller, nicht kürzer, nicht vertrauter und nicht offensichtlich klarer ist.

Das heißt, ich bin nicht gegen die Idee im Allgemeinen. Wenn Sie mit einer Syntax auf mich zukommen würden, die ungefähr so foreach (int x from 1 to 8)aussieht, würde ich wahrscheinlich zustimmen, dass dies eine Verbesserung gegenüber einer forSchleife wäre. Allerdings Enumerable.Rangeist ziemlich klobig.

mqp
quelle
Mit der Funktion für benannte Argumente von C # 4 könnten wir eine Erweiterungsmethode hinzufügen und sie so aufrufen (ich habe momentan keinen guten Namen dafür): foreach (int x in Enumerable.Range2 (von = 1 bis = 8) ) {} Ist das besser oder schlechter;)
Marcel Lamothe
Entschuldigung, das ist "(von: 1 bis: 8)"
Marcel Lamothe
4
Persönlich wäre das zu ausführlich und klobig für mich (im Gegensatz zu einer for-Schleife), aber ich verstehe, dass es bis zu einem gewissen Grad Geschmackssache ist.
mqp
1
@mquander Das sieht der Syntax von VB sehr ähnlich:For i = 1 To 8
MEMark
1
Wie wäre es mit einem Diebstahl foreach(var x in (1 to 8))von Scala? Es ist ein Aufzählungstyp.
Mateen Ulhaq
42

Das ist nur zum Spaß. (Ich würde nur das Standardschleifenformat for (int i = 1; i <= 10; i++)selbst verwenden.)

foreach (int i in 1.To(10))
{
    Console.WriteLine(i);    // 1,2,3,4,5,6,7,8,9,10
}

// ...

public static IEnumerable<int> To(this int from, int to)
{
    if (from < to)
    {
        while (from <= to)
        {
            yield return from++;
        }
    }
    else
    {
        while (from >= to)
        {
            yield return from--;
        }
    }
}

Sie können auch eine StepErweiterungsmethode hinzufügen :

foreach (int i in 5.To(-9).Step(2))
{
    Console.WriteLine(i);    // 5,3,1,-1,-3,-5,-7,-9
}

// ...

public static IEnumerable<T> Step<T>(this IEnumerable<T> source, int step)
{
    if (step == 0)
    {
        throw new ArgumentOutOfRangeException("step", "Param cannot be zero.");
    }

    return source.Where((x, i) => (i % step) == 0);
}
LukeH
quelle
public static IEnumerable <int> To (dieses int von, int bis) {return Enumerable.Range (von, bis); }
THX-1138
2
@unknown, Enumerable.Range kann nur vorwärts zählen, meine Version zählt auch rückwärts. Ihr Code erzeugt auch eine andere Sequenz als meine: Versuchen Sie "foreach (int i in 5.To (15))".
LukeH
Sehr gut, so etwas wollte ich hören, als ich die Frage gestellt habe!
Marcel Lamothe
Schritt () sieht falsch aus. 5.Zu (10) .Schritt (2) führt zu [6, 8, 10], wenn [5, 7, 9] erwartet wird. Außerdem ist Step () in Bezug auf die Leistung schrecklich. Was ist, wenn Schritt 10 ist?
Vincent
1
@wooohoh: Die StepMethode funktioniert korrekt: iist der Index, nicht der Wert. Sie haben Recht, dass die Leistung nicht großartig ist, aber dies ist eine Proof-of-Concept-Demo, kein Produktionscode.
LukeH
20

In C # 6.0 mit der Verwendung von

using static System.Linq.Enumerable;

Sie können es vereinfachen

foreach (var index in Range(1, 7))
{
    Console.WriteLine(index);
}
Mike Tsayper
quelle
Ich denke, dass die 7 die Anzahl ist: public static IEnumerable <int> Range (int start, int count)
user3717478
1
Zuerst habe ich nicht verstanden, was ist using static. Es sollte erwähnt werden, dass lesbarer zu schreiben:Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine);
Yitzchak
9

Sie können dies in C # tatsächlich tun (durch die Bereitstellung Tound Doals Erweiterungsmethoden auf intund IEnumerable<T>jeweils):

1.To(7).Do(Console.WriteLine);

SmallTalk für immer!

THX-1138
quelle
Schön - ich nehme an, Sie würden nur ein paar Erweiterungsmethoden für "To" und "Do" erstellen?
Marcel Lamothe
1
Ich dachte, dass C ++ Syntax schlecht ist LOL
Artyom
Hmmm ... To and Do funktioniert bei mir nicht? Zu welcher Bibliothek gehört das? Ich bin auf .Net 4.0.
BlueVoodoo
2
@BlueVoodoo sind Erweiterungsmethoden, die Sie definieren müssen.
Brian Rasmussen
1
Ich mag die DoErweiterungsmethode nicht. Es ist klarer, wenn dies in eine foreachSchleife geht , wie andere vorgeschlagen haben.
Arturo Torres Sánchez
8

Ich mag die Idee irgendwie. Es ist Python sehr ähnlich. Hier ist meine Version in ein paar Zeilen:

static class Extensions
{
    public static IEnumerable<int> To(this int from, int to, int step = 1) {
        if (step == 0)
            throw new ArgumentOutOfRangeException("step", "step cannot be zero");
        // stop if next `step` reaches or oversteps `to`, in either +/- direction
        while (!(step > 0 ^ from < to) && from != to) {
            yield return from;
            from += step;
        }
    }
}

Es funktioniert wie bei Python:

  • 0.To(4)[ 0, 1, 2, 3 ]
  • 4.To(0)[ 4, 3, 2, 1 ]
  • 4.To(4)[ ]
  • 7.To(-3, -3)[ 7, 4, 1, -2 ]
Kache
quelle
Ein Python-Bereich ist also end-exklusiv wie C ++ STL- und D-Bereiche? Ich mag das. Als Anfänger bevorzugte ich zuerst ... zuletzt (Ende inklusive), bis ich mit komplexeren verschachtelten Schleifen und Bildern arbeitete, und erkannte schließlich, wie elegant Ende exklusiv (Anfang Ende) war, indem ich all diese +1 und -1s eliminierte, die verschmutzt waren der Code. Im normalen Englisch bedeutet "to" jedoch tendenziell inklusive, sodass die Benennung Ihrer Parameter "start" / "end" möglicherweise klarer ist als "from" / "to".
Dwayne Robinson
!(step > 0 ^ from < to)könnte umgeschrieben werden step > 0 == from < to. Oder noch knapper: Verwenden Sie eine Do-While-Schleife mit Endbedingungfrom += step != to
EriF89
7

Es scheint eine ziemlich langwierige Herangehensweise an ein Problem zu sein, das bereits gelöst ist. Es gibt eine ganze Zustandsmaschine hinter der Enumerable.Range, die nicht wirklich benötigt wird.

Das traditionelle Format ist grundlegend für die Entwicklung und allen bekannt. Ich sehe keinen wirklichen Vorteil für Ihren neuen Stil.

Spender
quelle
2
In mancher Hinsicht. Enumerable.Range wird genauso unlesbar, wenn Sie von min bis max iterieren möchten, da der zweite Parameter ein Bereich ist und berechnet werden muss.
Spender
Ah, guter Punkt, aber das ist ein anderer Fall (auf den ich noch nicht gestoßen bin). Vielleicht würde eine Erweiterungsmethode mit (Start, Ende) anstelle von (Start, Anzahl) in diesem Szenario helfen.
Marcel Lamothe
6

Ich denke, der foreach + Enumerable.Range ist weniger fehleranfällig (Sie haben weniger Kontrolle und weniger Möglichkeiten, es falsch zu machen, wie das Verringern des Index innerhalb des Körpers, damit die Schleife niemals endet usw.)

Das Lesbarkeitsproblem betrifft die Semantik der Bereichsfunktion, die von einer Sprache in eine andere wechseln kann (z. B. wenn nur ein Parameter angegeben wird, beginnt er bei 0 oder 1 oder ist das Ende eingeschlossen oder ausgeschlossen oder ist der zweite Parameter eine Zählung statt ein Ende Wert).

In Bezug auf die Leistung denke ich, dass der Compiler intelligent genug sein sollte, um beide Schleifen zu optimieren, damit sie auch bei großen Bereichen mit einer ähnlichen Geschwindigkeit ausgeführt werden (ich nehme an, dass Range keine Sammlung erstellt, sondern natürlich einen Iterator).

fortran
quelle
6

Ich denke, Range ist nützlich für die Arbeit mit einigen Inline-Bereichen:

var squares = Enumerable.Range(1, 7).Select(i => i * i);

Sie können jeweils vorbei. Erfordert die Konvertierung in eine Liste, hält die Dinge jedoch kompakt, wenn Sie dies wünschen.

Enumerable.Range(1, 7).ToList().ForEach(i => Console.WriteLine(i));

Aber anders als für so etwas würde ich traditionelle for-Schleife verwenden.

mcNux
quelle
Zum Beispiel habe ich dies gerade in meinem Code getan, um einen Satz dynamischer Parameter für sqlvar paramList = String.Join(",", Enumerable.Range(0, values.Length).Select(i => "@myparam" + i));
mcNux
Enumerable.Range(1, 7).ToList().ForEach(Console.WriteLine); BESSER
Yitzchak
6

Ich möchte die Syntax einiger anderer Sprachen wie Python, Haskell usw. haben.

// Write the numbers 1 thru 7
foreach (int index in [1..7])
{
    Console.WriteLine(index);
}

Zum Glück haben wir jetzt F # :)

Was C # betrifft, muss ich mich an die Enumerable.RangeMethode halten.

Thomas Danecker
quelle
In C # gibt es seit einiger Zeit
user1496062
5

@ Luke: Ich habe Ihre To()Erweiterungsmethode neu implementiert und die Enumerable.Range()Methode verwendet, um dies zu tun. Auf diese Weise wird es etwas kürzer und verwendet so viel Infrastruktur wie möglich von .NET:

public static IEnumerable<int> To(this int from, int to)
{ 
    return from < to 
            ? Enumerable.Range(from, to - from + 1) 
            : Enumerable.Range(to, from - to + 1).Reverse();
}
Thorsten Lorenz
quelle
1
Ich denke, .Reverse () wird beim Lesen des ersten Elements die gesamte Sammlung in den Speicher laden. (Es sei denn, Reverse überprüft den zugrunde liegenden Iteratortyp und sagt: "Ah, dies ist ein Enumerator.Range-Objekt. Ich werde anstelle meines normalen Verhaltens ein rückwärts zählendes Objekt ausgeben.")
billpg
4

So verwenden Sie heute eine neue Syntax

Aufgrund dieser Frage habe ich einige Dinge ausprobiert, um eine nette Syntax zu finden, ohne auf erstklassige Sprachunterstützung zu warten. Folgendes habe ich:

using static Enumerizer;

// prints: 0 1 2 3 4 5 6 7 8 9
foreach (int i in 0 <= i < 10)
    Console.Write(i + " ");

Nicht der Unterschied zwischen <=und <.

Ich habe auch ein Proof-of-Concept-Repository auf GitHub mit noch mehr Funktionen erstellt (umgekehrte Iteration, benutzerdefinierte Schrittgröße).

Eine minimale und sehr begrenzte Implementierung der obigen Schleife würde ungefähr so ​​aussehen:

public readonly struct Enumerizer
{
    public static readonly Enumerizer i = default;

    public Enumerizer(int start) =>
        Start = start;

    public readonly int Start;

    public static Enumerizer operator <(int start, Enumerizer _) =>
        new Enumerizer(start);

    public static Enumerizer operator >(int _, Enumerizer __) =>
        throw new NotImplementedException();

    public static IEnumerable<int> operator <=(Enumerizer start, int end)
    {
        for (int i = start.Start; i < end; i++)
            yield return i;
    }

    public static IEnumerable<int> operator >=(Enumerizer _, int __) =>
        throw new NotImplementedException();
}
Bruno Zell
quelle
3

Ich bin sicher, dass jeder seine persönlichen Vorlieben hat (viele würden das spätere bevorzugen, nur weil es fast allen Programmiersprachen bekannt ist), aber ich bin wie Sie und fange an, das foreach immer mehr zu mögen, besonders jetzt, wo Sie einen Bereich definieren können .

TheTXI
quelle
3

Meiner Meinung nach ist der Enumerable.Range()Weg aussagekräftiger. Neu und den Menschen unbekannt? Bestimmt. Ich denke jedoch, dass dieser deklarative Ansatz die gleichen Vorteile bietet wie die meisten anderen LINQ-bezogenen Sprachfunktionen.

MEMark
quelle
2

Ich stelle mir vor, dass es Szenarien geben könnte, in Enumerable.Range(index, count)denen der Umgang mit Ausdrücken für die Parameter klarer ist, insbesondere wenn einige der Werte in diesem Ausdruck innerhalb der Schleife geändert werden. Im Fall des forAusdrucks würde basierend auf dem Zustand nach der aktuellen Iteration Enumerable.Range()ausgewertet , während im Voraus ausgewertet wird.

Abgesehen davon forwürde ich zustimmen, dass das Festhalten an normalerweise besser ist (vertrauter / lesbarer für mehr Leute ... lesbar ist ein sehr wichtiger Wert in Code, der beibehalten werden muss).

jerryjvl
quelle
2

Ich bin damit einverstanden, dass in vielen (oder sogar den meisten Fällen) foreachviel besser lesbar ist als eine Standardschleife, forwenn einfach über eine Sammlung iteriert wird. Ihre Wahl der Verwendung Enumerable.Range(index, count)ist jedoch kein gutes Beispiel für den Wert von foreachover for.

Für einen einfachen Bereich ab 1, Enumerable.Range(index, count)sieht gut lesbar aus. Wenn der Bereich jedoch mit einem anderen Index beginnt, ist er weniger lesbar, da Sie eine ordnungsgemäße Ausführung durchführen müssen index + count - 1, um das letzte Element zu bestimmen. Zum Beispiel…

// Write the numbers 2 thru 8
foreach (var index in Enumerable.Range( 2, 7 ))
{
    Console.WriteLine(index);
}

In diesem Fall bevorzuge ich das zweite Beispiel.

// Write the numbers 2 thru 8
for (int index = 2; index <= 8; index++)
{
    Console.WriteLine(index);
}
Dustin Campbell
quelle
Ich stimme zu - ähnlich wie Spenders Kommentar zur Iteration von min nach max.
Marcel Lamothe
1

Genau genommen missbrauchen Sie die Aufzählung.

Enumerator bietet die Möglichkeit, einzeln auf alle Objekte in einem Container zuzugreifen, garantiert jedoch nicht die Reihenfolge.

Es ist in Ordnung, die Aufzählung zu verwenden, um die größte Zahl in einem Array zu finden. Wenn Sie damit beispielsweise das erste Element ungleich Null finden, verlassen Sie sich auf die Implementierungsdetails, die Sie nicht kennen sollten. In Ihrem Beispiel scheint Ihnen die Reihenfolge wichtig zu sein.

Edit : Ich liege falsch. Wie Luke betonte (siehe Kommentare), ist es sicher, sich bei der Aufzählung eines Arrays in C # auf die Reihenfolge zu verlassen. Dies unterscheidet sich beispielsweise von der Verwendung von "for in" zum Auflisten eines Arrays in Javascript.

Buti-Oxa
quelle
Nicht sicher - Ich meine, die Range-Methode ist so aufgebaut, dass sie eine inkrementelle Sequenz zurückgibt. Daher bin ich mir nicht sicher, wie ich mich auf ein Implementierungsdetail verlasse. Können Sie (oder jemand anderes) näher darauf eingehen?
Marcel Lamothe
Ich glaube nicht, dass versprochen wird, dass die zurückgegebene Sequenz inkrementell ist. Natürlich ist es immer in der Praxis. Sie können jede Sammlung aufzählen. Für viele gibt es viele sinnvolle Aufträge, von denen Sie möglicherweise einen erhalten.
Buti-Oxa
2
@Marcel, @ buti-oxa: Der Aufzählungsprozess (z. B. mit foreach) garantiert keine bestimmte Reihenfolge an sich, aber die Reihenfolge wird durch den verwendeten Enumerator bestimmt: Der integrierte Enumerator für Array, List <> etc wird immer in der Reihenfolge der Elemente iteriert; Für SortedDictionary <>, SortedList <> usw. wird immer in der Reihenfolge des Schlüsselvergleichs iteriert. und für Dictionary <>, HashSet <> usw. gibt es keine garantierte Bestellung. Ich bin mir ziemlich sicher, dass Sie sich auf Enumerable verlassen können. Range iteriert immer in aufsteigender Reihenfolge.
LukeH
Sicher, der Enumerator kennt die Reihenfolge, die er bereitstellt. Wenn ich also den Enumerator schreibe, kann ich mich auf die Reihenfolge verlassen. Wenn Compiler / Bibliothek für mich ist, woher weiß ich das? Sagt irgendwo in der Spezifikation / Dokumentation, dass der integrierte Enumerator für das Array immer von 1 nach N iteriert? Ich konnte dieses Versprechen nicht finden.
Buti-Oxa
2
@ buti-oxa, Schauen Sie sich Abschnitt 8.8.4 (Seite 240) der C # 3-Spezifikation an: "Bei eindimensionalen Arrays werden Elemente in aufsteigender Indexreihenfolge durchlaufen, beginnend mit Index 0 und endend mit Indexlänge - 1. "" ( download.microsoft.com/download/3/8/8/… )
LukeH
1

Ich mag den foreach+ Enumerable.RangeAnsatz und benutze ihn manchmal.

// does anyone object to the new style over the traditional style?
foreach (var index in Enumerable.Range(1, 7))

Ich lehne den varMissbrauch in Ihrem Vorschlag ab. Ich weiß es zu schätzen var, aber verdammt, schreibe einfach intin diesem Fall! ;-);

xyz
quelle
Heh, ich habe mich gefragt, wann ich mich dafür begeistern würde :)
Marcel Lamothe
Ich habe mein Beispiel aktualisiert, um nicht von der Hauptfrage abzuhalten.
Marcel Lamothe