Salir de un bucle anidado


Si tengo un bucle for que está anidado dentro de otro, ¿cómo puedo salir eficientemente de ambos bucles (interno y externo) de la manera más rápida posible?

No quiero tener que usar un booleano y luego tener que decir ir a otro método, sino simplemente ejecutar la primera línea de código después del bucle externo.

¿Cuál es una manera rápida y agradable de hacer esto?

Gracias


Estaba pensando que las excepciones no son baratas / solo deben lanzarse en un verdadero condición excepcional, etc. Por lo tanto, no creo que esta solución sea buena desde una perspectiva de rendimiento.

No creo que sea correcto aprovechar las características más nuevas en.NET (métodos anon) para hacer algo que es bastante fundamental.

Debido a eso, tvon (lo siento no puede deletrear nombre de usuario completo!) tiene una buena solución.

Marc: Buen uso de los métodos anon, y esto también es genial, pero porque podría estar en un trabajo donde no usamos una versión de. NET/C# que soporte anon métodos, necesito saber un enfoque tradicional también.

Author: Bill the Lizard, 0000-00-00

9 answers

Bueno, goto, pero eso es feo, y no siempre es posible. También puede colocar los bucles en un método (o un método anon) y usar return para volver al código principal.

    // 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");

Tenga en cuenta que en C # 7 deberíamos obtener "funciones locales", lo que (sintaxis por determinar, etc.) significa que debería funcionar algo como:

// 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");
 179
Author: Marc Gravell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-21 10:20:07

No sé si funciona en C#, pero en C a menudo hago esto:

    for (int i = 0; i < 100; i++)
    {
        for (int j = 0; j < 100; j++)
        {
            if (exit_condition)
            {
                // cause the outer loop to break:
                i = INT_MAX;
                Console.WriteLine("Hi");
                // break the inner loop
                break;
            }
        }
    }
 90
Author: Nils Pipenbrinck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-28 02:21:38

Esta solución no se aplica a C #

Para las personas que encontraron esta pregunta a través de otros idiomas, Javascript, Java y D permite saltos etiquetados y continúa :

outer: while(fn1())
{
   while(fn2())
   {
     if(fn3()) continue outer;
     if(fn4()) break outer;
   }
}
 35
Author: BCS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-06 18:50:30

Use un protector adecuado en el bucle exterior. Coloque el protector en el bucle interior antes de romperse.

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
    }
}

O mejor aún, abstrae el bucle interno en un método y sale del bucle externo cuando devuelve false.

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

    .... some outer loop stuff

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

    ... more outer loop stuff
}
 25
Author: tvanfosson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-28 00:07:24

No me cite en esto, pero podría usar goto como se sugiere en el MSDN. Hay otras soluciones, como incluir un indicador que se comprueba en cada iteración de ambos bucles. Finalmente, podría usar una excepción como una solución realmente pesada a su problema.

GOTO:

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

Condición:

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
   }
}

Excepción:

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 ) {}
 19
Author: David Rodríguez - dribeas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-16 08:25:21

¿Es posible refactorizar el bucle for anidado en un método privado? De esa manera usted podría simplemente 'volver' fuera del método para salir del bucle.

 14
Author: NoizWaves,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-28 00:04:43

Factorice en una función/método y use early return, o reorganice sus bucles en una cláusula while. goto / excepciones / lo que sea ciertamente no es apropiado aquí.

def do_until_equal():
  foreach a:
    foreach b:
      if a==b: return
 11
Author: Dustin Getz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-28 00:10:42

Usted pidió una combinación de rápido, agradable, sin uso de un booleano, sin uso de goto, y C#. Has descartado todas las formas posibles de hacer lo que quieres.

La forma más rápida y menos fea es usar un goto.

 8
Author: Windows programmer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-28 00:47:14

Me parece que a la gente no le gusta mucho una declaración goto, así que sentí la necesidad de enderezar esto un poco.

Creo que las 'emociones' que la gente tiene sobre goto eventualmente se reducen a la comprensión del código y (conceptos erróneos) sobre las posibles implicaciones de rendimiento. Antes de responder a la pregunta, por lo tanto, primero entraré en algunos de los detalles sobre cómo se compila.

Como todos sabemos, C# se compila a IL, que luego se compila al ensamblador usando un SSA compilación. Voy a dar un poco de información sobre cómo funciona todo esto, y luego tratar de responder a la pregunta en sí.

De C # a IL

Primero necesitamos un trozo de código C#. Vamos a empezar simple:

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

Voy a hacer esto paso a paso para darle una buena idea de lo que sucede bajo el capó.

Primera traducción: de foreach al bucle equivalente for (Nota: Estoy usando una matriz aquí, porque no quiero entrar en detalles de IDesposable -- en cuyo caso me gustaría también se tiene que utilizar un IEnumerable):

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

Segunda traducción: el for y break se traduce en un equivalente más fácil:

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

Y tercera traducción (este es el equivalente del código IL): cambiamos break y while a una rama:

    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; 

Mientras el compilador hace estas cosas en un solo paso, le da una idea del proceso. El código IL que evoluciona del programa C # es la traducción literal del último código C#. Usted puede ver para usted mismo aquí: https://dotnetfiddle.net/QaiLRz (haga clic en 'ver IL')

Ahora, una cosa que has observado aquí es que durante el proceso, el código se vuelve más complejo. La forma más fácil de observar esto es por el hecho de que necesitábamos más y más código para completar lo mismo. También podría argumentar que foreach, for, while y break son en realidad manos cortas para goto, lo cual es parcialmente cierto.

De IL a Ensamblador

El compilador JIT. NET es un compilador de SSA. No voy a entrar en todos los detalles del formulario SSA aquí y cómo crear un compilador de optimización, es demasiado, pero puede dar una comprensión básica sobre lo que sucederá. Para una comprensión más profunda, es mejor comenzar a leer sobre la optimización de compiladores (me gusta este libro para una breve introducción: http://ssabook.gforge.inria.fr/latest/book.pdf ) y LLVM (llvm.org).

Cada compilador de optimización se basa en el hecho de que el código es fácil y sigue patrones predecibles. En el caso de los bucles FOR, usamos la teoría de grafos para analizar ramas, y luego optimizamos cosas como cycli en nuestras ramas (por ejemplo, ramas hacia atrás).

Sin embargo, ahora tenemos ramas forward para implementar nuestros bucles. Como habrás adivinado, este es en realidad uno de los primeros pasos que el JIT va a arreglar, como este:

    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:
    // ...

Como puedes ver, ahora tenemos una rama hacia atrás, que es nuestro pequeño bucle. Lo único que sigue siendo desagradable aquí es la rama con la que terminamos debido a nuestra declaración break. En algunos casos, podemos mover esto de la misma manera, pero en otros está ahí para quedarse.

Entonces, ¿por qué el compilador hace esto? Bueno, si podemos desenrollar el bucle, podríamos ser capaces de vectorizarlo. Incluso podríamos ser capaces de probar que sólo hay constantes que se añaden, lo que significa que todo nuestro bucle podría desaparecer en el aire. Para resumir: haciendo que los patrones sean predecibles (haciendo que las ramas sean predecibles), podemos probar que ciertas condiciones se mantienen en nuestro bucle, lo que significa que podemos hacer magia durante la optimización JIT.

Sin embargo, las ramas tienden a romper esos patrones predecibles agradables, que es algo optimizadores por lo tanto amable-una aversión. Break, continue, goto-todos tienen la intención de romper estos patrones predecibles-y por lo tanto no son realmente 'agradables'.

También deberías darte cuenta en este punto que un simple foreach es más predecible que un montón de goto declaraciones que van por todas partes. En términos de (1) legibilidad y (2) desde la perspectiva del optimizador, es la mejor solución.

Otra cosa que vale la pena mencionar es que es muy relevante para la optimización de compiladores asignar registros a variables (un proceso llamado register allocation). Como puede saber, solo hay un número finito de registros en su CPU y son, con mucho, las piezas de memoria más rápidas en su hardware. Las variables utilizadas en el código que está en el bucle más interno, tienen más probabilidades de obtener un registro asignado, mientras que las variables fuera de su bucle son menos importantes (porque este código es probablemente menos golpeado).

Ayuda, demasiada complejidad... ¿qué debo hacer?

La conclusión es que siempre debe usar las construcciones de lenguaje que tiene a su disposición, que generalmente (implícitamente) construirán patrones predecibles para su compilador. Trate de evitar ramas extrañas si es posible (específicamente: break, continue, goto o un return en medio de la nada).

La buena noticia aquí es que estos patrones predecibles son fáciles de leer (para los humanos) y fáciles de detectar (para los compiladores).

Uno de esos patrones se llama SESE, que significa Single Entry Single Exit.

Y ahora llegamos a la verdadera pregunta.

Imagina que tienes algo como esto:

// a is a variable.

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

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

La forma más fácil de hacer de este un patrón predecible es simplemente eliminar el if completamente:

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

En otros casos también puede dividir el método en 2 métodos:

// 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;
}

Variables Temporales? Bueno, malo o feo?

Incluso podría decidir devolver un booleano desde dentro del bucle (pero personalmente prefiero la forma SESE porque así es como el compilador lo verá y creo que es más limpio de leer).

Algunas personas piensan que es más limpio usar una variable temporal, y proponen una solución como esta:

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.
  // ...
}
// ...

Personalmente me opongo a este enfoque. Mira de nuevo sobre cómo se compila el código. Ahora piensa en lo que esto hará con estos patrones agradables y predecibles. ¿Entiendes?

Bien, déjame explicarlo. Lo que sucederá es que:

  • El compilador escribirá todo como ramas.
  • Como un paso de optimización, el compilador hará un flujo de datos
 8
Author: ,
Warning: date() expects parameter 2 to be long, string given in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61