it-swarm.com.de

Aus einer verschachtelten Schleife ausbrechen

Wenn ich eine for-Schleife habe, die in einer anderen verschachtelt ist, wie kann ich dann auf schnellstem Weg effizient aus beiden (inneren und äußeren) Schleifen kommen?

Ich möchte keinen Boolean verwenden und muss dann zu einer anderen Methode wechseln, sondern nur die erste Codezeile nach der äußeren Schleife ausführen.

Was ist ein schneller und schöner Weg, dies zu tun?

Vielen Dank


Ich dachte, Ausnahmen seien nicht billig/sollten nur in einem wirklich außergewöhnlichen Zustand abgeworfen werden. Daher glaube ich nicht, dass diese Lösung aus Performance-Sicht gut wäre.

Ich halte es nicht für richtig, die neueren Features von .NET (anon-Methoden) zu nutzen, um etwas zu tun, was ziemlich grundlegend ist.

Aus diesem Grund hat tvon (kann leider keinen vollständigen Benutzernamen buchstabieren!) Eine Lösung von Nizza.

Marc: Gute Verwendung von anon-Methoden, und auch das ist großartig. Da ich jedoch in einem Job arbeiten könnte, in dem wir keine .NET/C # -Version verwenden, die anon-Methoden unterstützt, muss ich auch einen traditionellen Ansatz kennen.

189
dotnetdev

Nun, goto, aber das ist hässlich und nicht immer möglich. Sie können die Schleifen auch in eine Methode (oder eine anon-Methode) einfügen und mit return zum Hauptcode zurückkehren.

    // goto
    for (int i = 0; i < 100; i++)
    {
        for (int j = 0; j < 100; j++)
        {
            goto Foo; // yeuck!
        }
    }
Foo:
    Console.WriteLine("Hi");

vs:

// anon-method
Action work = delegate
{
    for (int x = 0; x < 100; x++)
    {
        for (int y = 0; y < 100; y++)
        {
            return; // exits anon-method
        }
    }
};
work(); // execute anon-method
Console.WriteLine("Hi");

Beachten Sie, dass wir in C # 7 "lokale Funktionen" erhalten sollten, was (Syntax tbd etc) bedeutet, dass es etwa so funktionieren sollte:

// local function (declared **inside** another method)
void Work()
{
    for (int x = 0; x < 100; x++)
    {
        for (int y = 0; y < 100; y++)
        {
            return; // exits local function
        }
    }
};
Work(); // execute local function
Console.WriteLine("Hi");
184
Marc Gravell

Die C # -Anpassung des Ansatzes, die häufig im C-Set-Wert der Variablen der äußeren Schleife außerhalb der Schleifenbedingungen verwendet wird (d. H. Für die Schleife, die die int-Variable INT_MAX -1 verwendet, ist oft eine gute Wahl):

for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < 100; j++)
    {
        if (exit_condition)
        {
            // cause the outer loop to break:
            // use i = INT_MAX - 1; otherwise i++ == INT_MIN < 100 and loop will continue 
            i = int.MaxValue - 1;
            Console.WriteLine("Hi");
            // break the inner loop
            break;
        }
    }
    // if you have code in outer loop it will execute after break from inner loop    
}

Wie im Code erwähnt, springt break nicht auf magische Weise zur nächsten Iteration der äußeren Schleife. Wenn Sie also Code außerhalb der inneren Schleife haben, erfordert dieser Ansatz mehr Überprüfungen. Ziehen Sie in diesem Fall andere Lösungen in Betracht.

Dieser Ansatz funktioniert mit for- und while-Schleifen, jedoch nicht mit foreach. Im Fall von foreach haben Sie keinen Codezugriff auf den ausgeblendeten Enumerator, so dass Sie ihn nicht ändern können (und selbst wenn IEnumerator keine "MoveToEnd" -Methode hat).

Danksagungen an die Autoren der eingebetteten Kommentare:
i = INT_MAX - 1 Vorschlag von Meta
for/foreach Kommentar von ygoe .
Richtige IntMax von jmbpiano
anmerkung zum Code nach innerer Schleife durch blizpasta

93

Diese Lösung gilt nicht für C #

Für Personen, die diese Frage über andere Sprachen gefunden haben, lassen sich mit Javascript, Java und D gekennzeichnete Pausen und Weiterführungen

outer: while(fn1())
{
   while(fn2())
   {
     if(fn3()) continue outer;
     if(fn4()) break outer;
   }
}
37
BCS

Verwenden Sie eine geeignete Schutzvorrichtung in der äußeren Schleife. Setzen Sie den Schutz in die innere Schleife, bevor Sie brechen.

bool exitedInner = false;

for (int i = 0; i < N && !exitedInner; ++i) {

    .... some outer loop stuff

    for (int j = 0; j < M; ++j) {

        if (sometest) {
            exitedInner = true;
            break;
        }
    }
    if (!exitedInner) {
       ... more outer loop stuff
    }
}

Oder besser noch, die innere Schleife in eine Methode abstrahieren und die äußere Schleife verlassen, wenn sie false zurückgibt.

for (int i = 0; i < N; ++i) {

    .... some outer loop stuff

    if (!doInner(i, N, M)) {
       break;
    }

    ... more outer loop stuff
}
25
tvanfosson

Zitieren Sie mich nicht dazu, aber Sie könnten goto verwenden, wie in der MSDN vorgeschlagen. Es gibt andere Lösungen, beispielsweise ein Flag, das in jeder Iteration beider Schleifen geprüft wird. Schließlich können Sie eine Ausnahme als wirklich schwergewichtige Lösung für Ihr Problem verwenden.

GEHE ZU:

for ( int i = 0; i < 10; ++i ) {
   for ( int j = 0; j < 10; ++j ) {
      // code
      if ( break_condition ) goto End;
      // more code
   }
}
End: ;

Bedingung:

bool exit = false;
for ( int i = 0; i < 10 && !exit; ++i ) {
   for ( int j = 0; j < 10 && !exit; ++j ) {
      // code
      if ( break_condition ) {
         exit = true;
         break; // or continue
      }
      // more code
   }
}

Ausnahme:

try {
    for ( int i = 0; i < 10 && !exit; ++i ) {
       for ( int j = 0; j < 10 && !exit; ++j ) {
          // code
          if ( break_condition ) {
             throw new Exception()
          }
          // more code
       }
    }
catch ( Exception e ) {}

Ist es möglich, die verschachtelte for-Schleife in eine private Methode umzuwandeln? Auf diese Weise können Sie einfach aus der Methode zurückkehren, um die Schleife zu beenden.

14
NoizWaves

Mir kommt es so vor, als würden Leute eine goto -Anweisung sehr ablehnen, deshalb hatte ich das Bedürfnis, dies ein wenig zu korrigieren.

Ich glaube, die Emotionen, die die Leute über goto haben, laufen letztendlich darauf hinaus, den Code zu verstehen und (falsche Vorstellungen) über mögliche Auswirkungen auf die Leistung. Bevor ich die Frage beantworte, gehe ich daher zunächst auf einige Details der Kompilierung ein.

Wie wir alle wissen, wird C # in IL kompiliert, das dann mithilfe eines SSA-Compilers in Assembler kompiliert wird. Ich werde ein paar Einblicke geben, wie das alles funktioniert, und dann versuchen, die Frage selbst zu beantworten.

Von C # nach IL

Zuerst brauchen wir ein Stück C # -Code. Fangen wir einfach an:

foreach (var item in array)
{
    // ... 
    break;
    // ...
}

Ich werde dies Schritt für Schritt tun, um Ihnen eine gute Vorstellung davon zu geben, was unter der Haube passiert.

Erste Übersetzung: von foreach zur äquivalenten for-Schleife (Anmerkung: Ich verwende hier ein Array, weil ich nicht auf Details von IDisposable eingehen möchte - in diesem Fall habe ich d muss auch einen IEnumerable verwenden):

for (int i=0; i<array.Length; ++i)
{
    var item = array[i];
    // ...
    break;
    // ...
}

Zweite Übersetzung: Die for und break werden in ein einfacheres Äquivalent übersetzt:

int i=0;
while (i < array.Length)
{
    var item = array[i];
    // ...
    break;
    // ...
    ++i;
}

Und die dritte Übersetzung (das entspricht dem IL-Code): Wir ändern break und while in einen Zweig:

    int i=0; // for initialization

startLoop:
    if (i >= array.Length) // for condition
    {
        goto exitLoop;
    }
    var item = array[i];
    // ...
    goto exitLoop; // break
    // ...
    ++i;           // for post-expression
    goto startLoop; 

Während der Compiler diese Dinge in einem einzigen Schritt erledigt, gibt er Ihnen einen Einblick in den Prozess. Der aus dem C # -Programm resultierende IL-Code ist die wörtliche Übersetzung des letzten C # -Codes. Sie können sich hier selbst davon überzeugen: https://dotnetfiddle.net/QaiLRz (klicken Sie auf "IL anzeigen")

Sie haben hier festgestellt, dass der Code während des Vorgangs komplexer wird. Der einfachste Weg, dies zu beobachten, ist die Tatsache, dass wir immer mehr Code brauchten, um das Gleiche zu erreichen. Sie könnten auch argumentieren, dass foreach, for, while und break tatsächlich Short-Hands für goto sind, was teilweise zutrifft.

Von IL zu Assembler

Der .NET JIT-Compiler ist ein SSA-Compiler. Ich werde hier nicht auf alle Details des SSA-Formulars eingehen und erläutern, wie ein optimierender Compiler erstellt wird. Es ist einfach zu viel, kann aber ein grundlegendes Verständnis darüber vermitteln, was passieren wird. Für ein tieferes Verständnis ist es am besten, sich mit der Optimierung von Compilern vertraut zu machen (ich mag dieses Buch für eine kurze Einführung: http://ssabook.gforge.inria.fr/latest/book.pdf ) und LLVM (llvm.org).

Jeder optimierende Compiler verlässt sich auf die Tatsache, dass Code einfach ist und vorhersagbare Muster folgt. Im Fall von FOR-Schleifen verwenden wir die Graphentheorie, um Zweige zu analysieren und dann Dinge wie Cycli in unseren Zweigen zu optimieren (z. B. Zweige rückwärts).

Wir haben jedoch jetzt Forward-Zweige, um unsere Schleifen zu implementieren. Wie Sie vielleicht erraten haben, ist dies einer der ersten Schritte, die das JIT beheben wird:

    int i=0; // for initialization

    if (i >= array.Length) // for condition
    {
        goto endOfLoop;
    }

startLoop:
    var item = array[i];
    // ...
    goto endOfLoop; // break
    // ...
    ++i;           // for post-expression

    if (i >= array.Length) // for condition
    {
        goto startLoop;
    }

endOfLoop:
    // ...

Wie Sie sehen, haben wir jetzt einen Rückwärtszweig, der unsere kleine Schleife ist. Das einzige, was hier immer noch unangenehm ist, ist der Zweig, mit dem wir aufgrund unserer break -Anweisung endeten. In einigen Fällen können wir dies auf die gleiche Weise verschieben, in anderen Fällen bleibt es bestehen.

Warum macht der Compiler das? Nun, wenn wir die Schleife abrollen können, können wir sie möglicherweise vektorisieren. Vielleicht können wir sogar beweisen, dass nur Konstanten hinzugefügt werden, was bedeutet, dass unsere gesamte Schleife in Luft aufgehen könnte. Zusammenfassend lässt sich sagen: Indem wir die Muster vorhersehbar machen (indem wir die Verzweigungen vorhersehbar machen), können wir beweisen, dass bestimmte Bedingungen in unserer Schleife gelten, was bedeutet, dass wir während der JIT-Optimierung Magie anwenden können.

Zweige neigen jedoch dazu, diese vorhersehbaren Muster zu brechen, was Optimierungsfaktoren daher irgendwie ablehnen. Pause, mach weiter, mach weiter - sie alle haben vor, diese vorhersehbaren Muster zu brechen - und sind daher nicht wirklich „nett“.

Sie sollten an dieser Stelle auch erkennen, dass ein einfaches foreach vorhersehbarer ist als eine Reihe von goto Anweisungen, die überall vorkommen. In Bezug auf (1) Lesbarkeit und (2) aus Sicht des Optimierers ist dies die bessere Lösung.

Eine andere erwähnenswerte Sache ist, dass es für die Optimierung von Compilern sehr wichtig ist, Register Variablen zuzuweisen (ein Prozess, der Registerzuweisung). Wie Sie vielleicht wissen, gibt es in Ihrer CPU nur eine begrenzte Anzahl von Registern, und diese sind bei weitem die schnellsten Speicherbereiche in Ihrer Hardware. Variablen, die in Code verwendet werden, der sich in der innersten Schleife befindet, erhalten mit größerer Wahrscheinlichkeit ein Register, während Variablen außerhalb Ihrer Schleife weniger wichtig sind (da dieser Code wahrscheinlich weniger betroffen ist).

Hilfe, zu viel Komplexität ... was soll ich tun?

Im Endeffekt sollten Sie immer die Ihnen zur Verfügung stehenden Sprachkonstrukte verwenden, die normalerweise (implizit) vorhersagbare Muster für Ihren Compiler erstellen. Vermeiden Sie nach Möglichkeit seltsame Verzweigungen (insbesondere: break, continue, goto oder return mitten im Nichts).

Die gute Nachricht dabei ist, dass diese vorhersehbaren Muster sowohl leicht zu lesen (für Menschen) als auch leicht zu erkennen (für Compiler) sind.

Eines dieser Muster heißt SESE und steht für Single Entry Single Exit.

Und jetzt kommen wir zur eigentlichen Frage.

Stellen Sie sich vor, Sie haben so etwas:

// a is a variable.

for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j)
  {
     // ...

     if (i*j > a) 
     {
        // break everything
     }
  }
}

Der einfachste Weg, dies zu einem vorhersehbaren Muster zu machen, besteht darin, if vollständig zu entfernen:

int i, j;
for (i=0; i<100 && i*j <= a; ++i) 
{
  for (j=0; j<100 && i*j <= a; ++j)
  {
     // ...
  }
}

In anderen Fällen können Sie die Methode auch in zwei Methoden aufteilen:

// Outer loop in method 1:

for (i=0; i<100 && processInner(i); ++i) 
{
}

private bool processInner(int i)
{
  int j;
  for (j=0; j<100 && i*j <= a; ++j)
  {
     // ...
  }
  return i*j<=a;
}

Temporäre Variablen? Gut, böse oder hässlich?

Vielleicht möchten Sie sogar einen Booleschen Wert aus der Schleife zurückgeben (aber ich persönlich bevorzuge das SESE-Formular, da der Compiler dies so sieht und ich denke, es ist sauberer zu lesen).

Einige Leute halten es für sauberer, eine temporäre Variable zu verwenden, und schlagen eine Lösung wie die folgende vor:

bool more = true;
for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j) 
  {
     // ...
     if (i*j > a) { more = false; break; } // yuck.
     // ...
  }
  if (!more) { break; } // yuck.
  // ...
}
// ...

Ich persönlich bin gegen diesen Ansatz. Sehen Sie sich noch einmal an, wie der Code kompiliert wird. Überlegen Sie nun, was dies mit diesen schönen, vorhersehbaren Mustern bewirken wird. Verstehe?

Richtig, lassen Sie mich es formulieren. Was passieren wird ist, dass:

  • Der Compiler schreibt alles als Branches aus.
  • Als Optimierungsschritt führt der Compiler eine Datenflussanalyse durch, um die seltsame Variable more zu entfernen, die zufällig nur im Steuerungsfluss verwendet wird.
  • Bei Erfolg wird die Variable more aus dem Programm entfernt und es bleiben nur Zweige übrig. Diese Zweige werden optimiert, sodass Sie nur einen einzigen Zweig aus der inneren Schleife erhalten.
  • Wenn dies nicht erfolgreich ist, wird die Variable more definitiv in der innersten Schleife verwendet. Wenn der Compiler sie also nicht entfernt, besteht eine hohe Wahrscheinlichkeit, dass sie einem Register zugewiesen wird (das wertvollen Registerspeicher aufnimmt) ).

Zusammenfassend lässt sich sagen, dass der Optimierer in Ihrem Compiler große Schwierigkeiten haben wird, herauszufinden, dass more nur für den Kontrollfluss verwendet wird, und im Das Best-Case-Szenario übersetzt es in einen einzelnen Zweig außerhalb der äußeren for-Schleife.

Mit anderen Worten, das beste Szenario ist, dass es das Äquivalent von diesem ergibt:

for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j)
  {
     // ...
     if (i*j > a) { goto exitLoop; } // perhaps add a comment
     // ...
  }
  // ...
}
exitLoop:

// ...

Meine persönliche Meinung dazu ist ganz einfach: Wenn wir dies die ganze Zeit beabsichtigt haben, wollen wir die Welt sowohl für den Compiler als auch für die Lesbarkeit einfacher machen und dies sofort schreiben.

tl; dr:

Endeffekt:

  • Verwenden Sie nach Möglichkeit eine einfache Bedingung in Ihrer for-Schleife. Halten Sie sich so weit wie möglich an die übergeordneten Sprachkonstrukte, die Ihnen zur Verfügung stehen.
  • Wenn alles fehlschlägt und Sie entweder goto oder bool more, lieber den ersteren.
13
atlaste

verwenden Sie eine Funktion/Methode und verwenden Sie die frühzeitige Rückgabe, oder ordnen Sie Ihre Schleifen in eine while-Klausel um. goto/ausnahmen/was auch immer sind hier sicherlich nicht angebracht.

def do_until_equal():
  foreach a:
    foreach b:
      if a==b: return
11
Dustin Getz

Sie haben nach einer Kombination aus schnellem, nettem, keinem Booleschen, keinem goto und C # gefragt. Sie haben alle Möglichkeiten ausgeschlossen, das zu tun, was Sie wollen.

Der schnellste und am wenigsten hässliche Weg ist die Verwendung eines goto.

8

Manchmal ist es schön, den Code in seine eigene Funktion zu abstrahieren und dann eine frühe Rückkehr zu verwenden - frühe Rückkehr ist jedoch böse:)

public void GetIndexOf(Transform transform, out int outX, out int outY)
{
    outX = -1;
    outY = -1;

    for (int x = 0; x < Columns.Length; x++)
    {
        var column = Columns[x];

        for (int y = 0; y < column.Transforms.Length; y++)
        {
            if(column.Transforms[y] == transform)
            {
                outX = x;
                outY = y;

                return;
            }
        }
    }
}
5
Sky

Abhängig von Ihrer Situation können Sie dies möglicherweise tun, jedoch nur, wenn Sie nach der inneren Schleife keinen Code ausführen.

for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < 100; j++)
    {
        i = 100;
        break;
    }
}

Es ist nicht elegant, aber es kann die einfachste Lösung sein, abhängig von Ihrem Problem.

2
Chris Bartow

Ich habe viele Beispiele gesehen, die "break" verwenden, aber keines, das "weiter" verwendet.

Es würde immer noch eine Art Flagge in der inneren Schleife erfordern:

while( some_condition )
{
    // outer loop stuff
    ...

    bool get_out = false;
    for(...)
    {
        // inner loop stuff
        ...

        get_out = true;
        break;
    }

    if( get_out )
    {
        some_condition=false;
        continue;
    }

    // more out loop stuff
    ...

}
2
dviljoen

Seit ich vor einigen Jahrzehnten break in C gesehen habe, hat mich dieses Problem geärgert. Ich hatte gehofft, dass eine Spracherweiterung eine Erweiterung hat, die zu brechen ist, was folgendermaßen funktionieren würde:

break; // our trusty friend, breaks out of current looping construct.
break 2; // breaks out of the current and it's parent looping construct.
break 3; // breaks out of 3 looping constructs.
break all; // totally decimates any looping constructs in force.
1
Jesse C. Slicer

Ich erinnere mich an meine Studententage, dass es gesagt wurde, es sei mathematisch beweisbar, dass Sie alles ohne Code tun können (d. H. Also benutze ich nie gotos (nur meine persönliche Vorliebe, nicht die Annahme, dass ich richtig oder falsch liege)

Um aus verschachtelten Schleifen auszubrechen, mache ich sowas wie folgt:

var isDone = false;
for (var x in collectionX) {
    for (var y in collectionY) {
        for (var z in collectionZ) {
            if (conditionMet) {
                // some code
                isDone = true;
            }
            if (isDone)
                break;
        }
        if (isDone) 
            break;
    }
    if (isDone)
        break;
}

... ich hoffe, das hilft denen, die ich mag, sind Anti-Gotik "Fanboys" :)

0
MG123

So habe ich es gemacht. Immer noch ein Workaround.

foreach (var substring in substrings) {
  //To be used to break from 1st loop.
  int breaker=1;
  foreach (char c in substring) {
    if (char.IsLetter(c)) {
      Console.WriteLine(line.IndexOf(c));
      \\setting condition to break from 1st loop.
      breaker=9;
      break;
    }
  }
  if (breaker==9) {
    break;
  }
}

0
Garvit Arora