Welcher Weg zum Beenden der Leseschleife ist der bevorzugte Ansatz?

13

Wenn Sie einen Reader iterieren müssen, bei dem die Anzahl der zu lesenden Elemente unbekannt ist, und die einzige Möglichkeit besteht darin, weiterzulesen, bis Sie das Ende erreicht haben.

Dies ist oft der Ort, an dem Sie eine Endlosschleife benötigen.

  1. Es gibt das Immer true, das anzeigt, dass irgendwo innerhalb des Blocks ein breakoder eine returnAnweisung stehen muss .

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
  2. Es gibt die Double Read for Loop-Methode.

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
  3. Es gibt die Single Read While-Schleife. Nicht alle Sprachen unterstützen diese Methode .

    int offset = 0;
    Record r = null;
    while((r = Read(++offset)) != null)
    {
        // do work
    }

Ich frage mich, welcher Ansatz mit der geringsten Wahrscheinlichkeit einen Fehler verursacht, der am besten lesbar und am häufigsten verwendet wird.

Jedes Mal, wenn ich eines davon schreiben muss, denke ich, dass es einen besseren Weg geben muss .

Reactgular
quelle
2
Warum pflegen Sie einen Offset? Erlauben Ihnen die meisten Stream-Leser nicht, einfach "weiterzulesen"?
Robert Harvey
@RobertHarvey in meinem aktuellen Bedarf hat der Leser eine zugrunde liegende SQL-Abfrage, die den Offset verwendet, um Ergebnisse zu paginieren. Ich weiß nicht, wie lange die Abfrageergebnisse dauern, bis ein leeres Ergebnis zurückgegeben wird. Aber für die Frage ist es eigentlich keine Voraussetzung.
Reactgular
3
Sie scheinen verwirrt zu sein - im Fragentitel geht es um Endlosschleifen, aber im Fragentext dreht sich alles um das Beenden von Schleifen. Die klassische Lösung (aus der Zeit der strukturierten Programmierung) besteht darin, eine Vorlese-Schleife durchzuführen, während Sie Daten haben, und diese als letzte Aktion in der Schleife erneut zu lesen. Es ist einfach (um die Fehleranforderungen zu erfüllen), am häufigsten (seit 50 Jahren). Am lesbarsten ist Ansichtssache.
andy256
@ andy256 verwirrt ist für mich der Zustand vor dem Kaffee.
Reactgular
1
Ah, so ist das korrekte Verfahren 1) Trinken Sie Kaffee, während Sie die Tastatur meiden, 2) Beginnen Sie die Codierschleife.
andy256

Antworten:

49

Ich würde hier einen Schritt zurück machen. Sie konzentrieren sich auf die wählerischen Details des Codes, übersehen aber das größere Bild. Werfen wir einen Blick auf eine Ihrer Beispielschleifen:

int offset = 0;
while(true)
{
    Record r = Read(offset);
    if(r == null)
    {
        break;
    }
    // do work
    offset++;
}

Was bedeutet dieser Code? Die Bedeutung ist "Arbeiten Sie an jedem Datensatz in einer Datei". Aber so sieht der Code nicht aus . Der Code sieht wie folgt aus: "Versatz beibehalten. Datei öffnen. Schleife ohne Endebedingung eingeben. Datensatz lesen. Auf Nullheit prüfen." Das alles, bevor wir zur Arbeit kommen! Die Frage, die Sie stellen sollten, lautet: " Wie kann ich sicherstellen, dass das Erscheinungsbild dieses Codes mit der Semantik übereinstimmt? "

foreach(Record record in RecordsFromFile())
    DoWork(record);

Jetzt liest sich der Code wie beabsichtigt. Trennen Sie Ihre Mechanismen von Ihrer Semantik . In Ihrem ursprünglichen Code mischen Sie den Mechanismus - die Details der Schleife - mit der Semantik - der Arbeit, die für jeden Datensatz geleistet wurde.

Jetzt müssen wir umsetzen RecordsFromFile(). Wie lässt sich das am besten umsetzen? Wen interessiert das? Das ist nicht der Code, den sich irgendjemand ansehen wird. Es ist ein grundlegender Mechanismuscode und seine zehn Zeilen lang. Schreibe es wie du willst. Wie wäre es damit?

public IEnumerable<Record> RecordsFromFile()
{
    int offset = 0;
    while(true)
    {
        Record record = Read(offset);
        if (record == null) yield break;
        yield return record;
        offset += 1;
    }
}

Jetzt, da wir eine träge berechnete Folge von Datensätzen manipulieren, werden alle möglichen Szenarien möglich:

foreach(Record record in RecordsFromFile().Take(10))
    DoWork(record);

foreach(Record record in RecordsFromFile().OrderBy(r=>r.LastName))
    DoWork(record);

foreach(Record record in RecordsFromFile().Where(r=>r.City == "London")
    DoWork(record);

Und so weiter.

Fragen Sie sich, wann immer Sie eine Schleife schreiben: "Liest diese Schleife wie ein Mechanismus oder wie die Bedeutung des Codes?" Wenn die Antwort "wie ein Mechanismus" ist, versuchen Sie, diesen Mechanismus auf seine eigene Methode zu verschieben, und schreiben Sie den Code, um die Bedeutung sichtbarer zu machen.

Eric Lippert
quelle
3
+1 endlich eine vernünftige Antwort. Genau das werde ich tun. Vielen Dank.
Reactgular
1
"Versuchen Sie, diesen Mechanismus auf eine eigene Methode zu verschieben" - das klingt nach einem Refactoring der Extraktionsmethode, nicht wahr? "Verwandeln Sie das Fragment in eine Methode, deren Name den Zweck der Methode erklärt."
gnat
2
@gnat: Was ich vorschlage, ist etwas komplizierter als die "Extraktionsmethode", die ich als bloßes Verschieben eines Codestücks an einen anderen Ort betrachte. Das Extrahieren von Methoden ist definitiv ein guter Schritt, um den Code so lesbar wie seine Semantik zu machen. Ich schlage vor, dass die Methodenextraktion sorgfältig durchgeführt wird, wobei Richtlinien und Mechanismen getrennt zu halten sind.
Eric Lippert
1
@gnat: Genau! In diesem Fall ist das Detail, das ich extrahieren möchte, der Mechanismus, mit dem alle Datensätze aus der Datei gelesen werden, während die Richtlinie beibehalten wird. Die Politik lautet: "Wir müssen an jedem Datensatz etwas arbeiten".
Eric Lippert
1
Aha. Auf diese Weise ist es einfacher zu lesen und zu warten. Studieren Sie diesen Code, kann ich separat auf die Politik und Mechanismus konzentrieren, macht es mich nicht zwingen , auf geteilte Aufmerksamkeit
gnat
19

Sie brauchen keine Endlosschleife. In C # -Leseszenarios sollten Sie niemals eines benötigen. Dies ist mein bevorzugter Ansatz, vorausgesetzt, Sie müssen wirklich einen Offset beibehalten:

Record r = Read(0);
offset=1;
while(r != null)
{
    // Do work
    r = Read(offset);
    offset++
}

Dieser Ansatz erkennt die Tatsache an, dass es einen Einrichtungsschritt für den Leser gibt, sodass zwei Lesemethodenaufrufe vorhanden sind. Die whileBedingung befindet sich ganz oben in der Schleife, nur für den Fall, dass der Reader überhaupt keine Daten enthält.

Robert Harvey
quelle
6

Nun, es hängt von Ihrer Situation ab. Aber eine der "C # -ish" -Lösungen, die ich mir vorstellen kann, ist die Verwendung der integrierten IEnumerable-Schnittstelle und einer foreach-Schleife. Die Schnittstelle für IEnumerator ruft MoveNext nur mit true oder false auf, sodass die Größe unbekannt sein kann. Dann wird Ihre Terminierungslogik einmal im Enumerator geschrieben und Sie müssen nicht mehr als an einer Stelle wiederholen.

MSDN enthält ein Beispiel für IEnumerator <T> . Sie müssen auch ein IEnumerable <T> erstellen, um den IEnumerator <T> zurückzugeben.

J Trana
quelle
Können Sie ein Codebeispiel geben?
Reactgular
danke, dafür habe ich mich entschieden. Ich denke zwar nicht, dass Sie jedes Mal, wenn Sie eine Endlosschleife haben, neue Klassen erstellen müssen, um die Frage zu lösen.
Reactgular
Ja, ich weiß was du meinst - viel Overhead. Das eigentliche Genie / Problem von Iteratoren besteht darin, dass sie auf jeden Fall so eingerichtet sind, dass zwei Dinge gleichzeitig über eine Sammlung iteriert werden können. So viele Male Sie brauchen nicht die Funktionalität , so dass Sie könnte einfach den Wrapper um die Objekte sowohl IEnumerable und IEnumerator implementieren. Eine andere Betrachtungsweise ist, dass die zugrunde liegende Sammlung von Inhalten, die Sie durchlaufen, nicht unter Berücksichtigung des akzeptierten C # -Paradigmas entworfen wurde. Und das ist in Ordnung. Ein zusätzlicher Bonus von Iteratoren ist, dass Sie alle parallelen LINQ-Inhalte kostenlos erhalten können!
J Trana
2

Wenn ich Initialisierungs-, Bedingungs- und Inkrementierungsoperationen habe, verwende ich gerne die for-Schleifen von Sprachen wie C, C ++ und C #. So was:

for (int offset = 0, Record r = Read(offset); r != null; r = Read(++offset)){
    // loop here!
}

Oder dies, wenn Sie denken, dass dies besser lesbar ist. Ich persönlich bevorzuge den ersten.

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
Trylks
quelle