c# - make - Rompiendo de un bucle anidado
how to make a loop in c# (19)
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 tengo que decir ir a otro método, sino simplemente ejecutar la primera línea de código después del ciclo externo.
¿Cuál es una manera rápida y agradable de hacer esto?
Gracias
Estaba pensando que las excepciones no son baratas / solo deberían arrojarse en condiciones verdaderamente excepcionales, etc. Por lo tanto, no creo que esta solución sea buena desde la perspectiva del rendimiento.
No creo que sea correcto aprovechar las características más nuevas en .NET (métodos anónimos) para hacer algo que es bastante fundamental.
Por eso, tvon (lo siento, no puedo deletrear el nombre de usuario completo!) Tiene una buena solución.
Marc: Buen uso de los métodos anon, y esto también es genial, pero como podría estar en un trabajo en el que no usamos una versión de .NET / C # que admita métodos nuevos, también necesito conocer un enfoque tradicional.
¿Es posible refactorizar el bucle for anidado en un método privado? De esa forma, simplemente podría ''regresar'' fuera del método para salir del ciclo.
¿Incluso miraste la palabra clave break
? Oo
Esto es solo un pseudocódigo, pero deberían poder ver lo que quiero decir:
<?php
for(...) {
while(...) {
foreach(...) {
break 3;
}
}
}
Si piensas que break
es una función como break()
, entonces su parámetro sería la cantidad de bucles de los que se saldrá. Como estamos en el tercer ciclo del código aquí, podemos salir de los tres.
Manual: http://php.net/break
A veces es bueno abstraer el código en su propia función y luego usar un retorno anticipado; sin embargo, los primeros retornos son malos:)
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;
}
}
}
}
Como veo, aceptaste la respuesta en la que la persona te remite a tu declaración, donde en la programación moderna y en opinión de expertos, Goto es un asesino, lo llamamos un asesino en programación que tiene ciertas razones, que no voy a discutir aquí en este punto, pero la solución de su pregunta es muy simple, puede usar una bandera booleana en este tipo de escenario, como lo demostraré en mi ejemplo:
for (; j < 10; j++)
{
//solution
bool breakme = false;
for (int k = 1; k < 10; k++)
{
//place the condition where you want to stop it
if ()
{
breakme = true;
break;
}
}
if(breakme)
break;
}
simple y simple. :)
Creo que a menos que quieras hacer lo "booleano", la única solución es tirar. ¡Lo cual obviamente no deberías hacer ...!
Dependiendo de su situación, es posible que pueda hacer esto, pero solo si no está ejecutando el código DESPUÉS del ciclo interno.
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 100; j++)
{
i = 100;
break;
}
}
No es elegent, pero puede ser la solución más fácil dependiendo de su problema.
Desde que vi por primera vez la break
en C hace un par de décadas, este problema me ha molestado. Esperaba que alguna mejora del lenguaje tuviera una extensión para romper, que funcionaría así:
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.
He visto muchos ejemplos que usan "break" pero ninguno que usa "continue".
Todavía requeriría una bandera de algún tipo en el bucle interno:
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
...
}
Lanza una excepción personalizada que sale del bucle outter.
Funciona para for
, foreach
o while
o cualquier clase de bucle y cualquier lenguaje que use try catch exception
block
try
{
foreach (object o in list)
{
foreach (object another in otherList)
{
// ... some stuff here
if (condition)
{
throw new CustomExcpetion();
}
}
}
}
catch (CustomException)
{
// log
}
Me parece que a la gente no le gusta mucho una declaración de goto
, así que sentí la necesidad de arreglar esto un poco.
Creo que las ''emociones'' que la gente tiene sobre goto
finalmente se reducen a la comprensión del código y (conceptos erróneos) sobre las posibles implicaciones de rendimiento. Antes de responder la pregunta, primero entraré en algunos detalles sobre cómo se compila.
Como todos sabemos, C # se compila en IL, que luego se compila en ensamblador utilizando un compilador de SSA. Daré un poco de información sobre cómo funciona todo esto, y luego intentaré responder la pregunta en sí.
De C # a IL
Primero necesitamos una pieza de código C #. Comencemos simple:
foreach (var item in array)
{
// ...
break;
// ...
}
Haré esto paso a paso para darte una buena idea de lo que sucede debajo del capó.
Primera traducción: de foreach
al bucle equivalente for
(Nota: estoy usando un array aquí, porque no quiero entrar en detalles de IDisposable, en cuyo caso también tendría que usar un IEnumerable):
for (int i=0; i<array.Length; ++i)
{
var item = array[i];
// ...
break;
// ...
}
Segunda traducción: el for
y el break
se traduce a un equivalente más fácil:
int i=0;
while (i < array.Length)
{
var item = array[i];
// ...
break;
// ...
++i;
}
Y tercera traducción (esto es el equivalente del código IL): cambiamos el break
y while
en 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;
Si bien 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 #. Puede verlo usted mismo aquí: https://dotnetfiddle.net/QaiLRz (haga clic en ''ver IL'')
Ahora, una cosa que ha 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 necesitamos más y más código para obtener lo mismo. También podría argumentar que foreach
, for
, while
y break
son en realidad short-hands para goto
, lo cual es parcialmente cierto.
De IL a Ensamblador
El compilador .NET JIT es un compilador de SSA. No voy a entrar en todos los detalles del formulario de SSA aquí y cómo crear un compilador de optimización, es demasiado, pero puede dar una comprensión básica de 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 avanzadas para implementar nuestros bucles. Como habrás adivinado, este es realmente uno de los primeros pasos que el JIT va a solucionar, 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 puede ver, ahora tenemos una rama hacia atrás, que es nuestro pequeño bucle. Lo único que sigue siendo desagradable aquí es la sucursal con la que terminamos debido a nuestra declaración de 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 probar que solo se están agregando constantes, lo que significa que todo nuestro ciclo podría desaparecer en el aire. Para resumir: al hacer que los patrones sean predecibles (haciendo las ramas predecibles), podemos probar que ciertas condiciones se mantienen en nuestro ciclo, lo que significa que podemos hacer magia durante la optimización JIT.
Sin embargo, las ramas tienden a romper esos buenos patrones predecibles, lo cual es algo optimizador, por lo tanto amable, una antipatía. Break, continue, goto, todos intentan romper estos patrones predecibles, y por lo tanto, no son realmente "agradables".
También debe darse cuenta en este punto de que un foreach
simple es más predecible que un montón de declaraciones goto
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 optimizar los compiladores para asignar registros a las variables (un proceso llamado asignación de registro ). Como sabrá, 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 probablemente sea menor).
Ayuda, demasiada complejidad ... ¿Qué debería hacer?
La conclusión es que siempre debe usar las construcciones de lenguaje que tiene a su disposición, que generalmente (implícitamente) generarán patrones predecibles para su compilador. Trate de evitar las ramas extrañas si es posible (específicamente: break
, continue
, goto
o return
en el 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 puede decidir devolver un booleano desde el bucle (pero personalmente prefiero el formulario SESE porque así es como lo verá el compilador y creo que es más fácil leerlo).
Algunas personas piensan que es más limpio usar una variable temporal y proponer 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. Mire nuevamente cómo se compila el código. Ahora piense en lo que esto hará con estos patrones agradables y predecibles. ¿Obtener la imagen?
Bien, déjame deletrearlo. Lo que sucederá es que:
- El compilador escribirá todo como ramas.
- Como paso de optimización, el compilador realizará análisis de flujo de datos en un intento de eliminar la extraña variable
more
que solo se usa en el flujo de control. - Si tiene éxito, la variable
more
será eliminada del programa y solo quedarán ramas. Estas ramas se optimizarán, por lo que obtendrá una sola ramificación del lazo interno. - Si no es exitoso, la variable
more
definitivamente se usa en el ciclo más interno, por lo que si el compilador no lo optimiza, tiene una alta probabilidad de asignarse a un registro (que consume una valiosa memoria de registro).
Entonces, para resumir: el optimizador en su compilador se va a meter en un infierno de muchos problemas para darse cuenta de que more
solo se usa para el flujo de control, y en el mejor de los casos, lo traducirá a una sola rama fuera del exterior en bucle.
En otras palabras, el mejor escenario posible es que termine con el equivalente a esto:
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:
// ...
Mi opinión personal sobre esto es bastante simple: si esto es lo que pretendíamos desde el principio, hagamos el mundo más fácil tanto para el compilador como para la legibilidad, y escribamos eso de inmediato.
tl; dr:
Línea de fondo:
- Use una condición simple en su bucle for si es posible. Siga las construcciones lingüísticas de alto nivel que tenga a su alcance tanto como sea posible.
- Si todo falla y te quedas con
goto
obool more
, prefieres el anterior.
No me cites sobre esto, pero podrías usar goto como se sugiere en 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 para su problema.
IR:
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 ) {}
No sé si funciona en C #, pero en CI a menudo hace 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;
}
}
}
Pediste una combinación de rápido, agradable, sin uso de 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.
Recuerdo que, en mis días de estudiante, se decía que es matemáticamente demostrable que puedes hacer cualquier cosa en el código sin un goto (es decir, no hay ninguna situación en la que goto sea la única respuesta). Por lo tanto, nunca uso goto''s (solo mi preferencia personal, sin sugerir que estoy en lo cierto o equivocado)
De todos modos, para salir de los bucles anidados hago algo como esto:
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;
}
... espero que ayude a aquellos que me quieren son anti-goto "fanboys" :)
Use una protección adecuada en el bucle externo. Coloque el protector en el lazo interno antes de romper.
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, abstraiga el ciclo interno en un método y salga del ciclo externo cuando devuelva falso.
for (int i = 0; i < N; ++i) {
.... some outer loop stuff
if (!doInner(i, N, M)) {
break;
}
... more outer loop stuff
}
factorizar en una función / método y usar el retorno anticipado, o reorganizar los bucles en una cláusula while. goto / exceptions / lo que sea, ciertamente no es apropiado aquí.
def do_until_equal():
foreach a:
foreach b:
if a==b: return
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 permiten roturas etiquetadas y continúan :
outer: while(fn1())
{
while(fn2())
{
if(fn3()) continue outer;
if(fn4()) break outer;
}
}
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 anónimo) y usar return
para salir 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", que (sintaxis tbd, etc.) significa que debería funcionar algo así 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");
bool breakInnerLoop=false
for(int i=0;i<=10;i++)
{
for(int J=0;i<=10;i++)
{
if(i<=j)
{
breakInnerLoop=true;
break;
}
}
if(breakInnerLoop)
{
continue
}
}