Instrucciones e continue etiquetadas break

Note

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos y se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de idioma (LDM) pertinentes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones.

Resumen

Permitir break e continue instrucciones para especificar opcionalmente una etiqueta que identifique qué bucle o switch instrucción de destino, habilitando el flujo de control más limpio en construcciones anidadas sin necesidad goto de instrucciones u otras contorciones como funciones anidadas, devoluciones de tupla, etc.

Motivación

Al trabajar con bucles anidados o bucles que contienen switch instrucciones, los desarrolladores a menudo necesitan interrumpir o continuar un bucle externo desde dentro de un contexto interno. Actualmente, hay dos enfoques principales para lograrlo, ambos con desventajas significativas:

Uso de goto instrucciones

string foundValue = null;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            goto FOUND;
    }
}
FOUND:
ProcessValue(foundValue);

Aunque goto funciona, requiere colocar etiquetas después de la construcción del bucle y no comunica claramente la intención de interrumpir un bucle específico. Para continuar con un bucle externo, el enfoque se vuelve aún más incómodo:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
    }
    CONTINUE_OUTER: ;
}

Este patrón resulta confuso porque la etiqueta debe colocarse al final del cuerpo del bucle, justo antes de la llave de cierre, para que se produzca la comprobación del incremento y la condición. Cuando se necesitan y breakcontinue para el mismo bucle externo, se requieren dos etiquetas independientes:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
        
        if (ShouldExitAll(x, y))
            goto BREAK_OUTER;
    }
    CONTINUE_OUTER: ;
}
BREAK_OUTER:
// Subsequent statements

Uso de variables de marca

string foundValue = null;
bool shouldBreak = false;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
        {
            shouldBreak = true;
            break;
        }
    }
    if (shouldBreak)
        break;
}
ProcessValue(foundValue);

Este enfoque requiere administración de estado adicional, aumenta el nivel de detalle del código y oculta la intención del flujo de control.

Solución propuesta

Con etiquetada break y continue, el código se vuelve más claro y fácil de mantener:

string foundValue = null;
outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            break outer;
    }
}
ProcessValue(foundValue);

La etiqueta se coloca directamente en el bucle que identifica y la instrucción break/continue denomina explícitamente su destino. Para continuar:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
    }
}

Esto expresa naturalmente "continuar el bucle externo", sin la confusión de la colocación de etiqueta asociada a goto. Se puede usar una sola etiqueta para ambas operaciones:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
        
        if (ShouldExitAll(x, y))
            break outer;
    }
}

Esta característica se ha solicitado ampliamente en la comunidad de C#, con discusiones que datan décadas y el tema que se vuelve a solicitar y se vuelve a solicitar continuamente. Existen características similares en otros lenguajes modernos:

En todos estos casos, los idiomas funcionan de la misma manera que en esta especificación. Es decir, algunas construcciones pueden tener una etiqueta y es posible hacer referencia a esa etiqueta de sus respectivas continue instrucciones o break .

Diseño detallado

Las siguientes actualizaciones se presentan como diferencias en las secciones correspondientes del estándar de C# 7 (statements.md). En esta sección, tachado indica que se quita texto de la especificación existente y negrita indica que se agrega texto. El prose sin cambios se cita textualmente para el contexto.

§13.5 Instrucciones etiquetadas

Inserte el párrafo siguiente inmediatamente después del párrafo existente "Se puede hacer referencia a una etiqueta desde goto instrucciones (§13.10.4) dentro del ámbito de la etiqueta".":

Si la instrucción anidada inmediatamente dentro de un labeled_statement es una switch_statement (§13.8.3) o una iteration_statement (§13.9), se dice que la instrucción anidada se etiquetará con el identificador del labeled_statement. Un break_statement (§13.10.2) o continue_statement (§13.10.3) puede especificar este identificador para hacer referencia a la instrucción con etiqueta contenedora.

Nota: Solo la instrucción que está anidada inmediatamente dentro de un labeled_statement está etiquetada con ese identificador. Por ejemplo, dado a: b: while (…) …, solo b etiqueta el iteration_statement; a etiqueta el labeled_statementb: while (…) … interno, que no es en sí mismo un switch_statement o iteration_statement. Por lo tanto, break a; o continue a; aparecer dentro del cuerpo del bucle no tiene como destino la while instrucción . nota final

§13.10.2 La instrucción break

break_statement
    : 'break' identifier? ';'
    ;

La break instrucción sale de la instrucción más cercana que incluye switch, while, do, foro foreach .

La break instrucción sale de la switch_statement más cercana (§13.8.3) o iteration_statement (§13.9), o, si se especifica un identificador, la switch_statement más cercana o iteration_statement etiquetada con ese identificador (consulte §13.5).

El destino de una break instrucción es el punto final de la instrucción más cercana que incluye switch, while, do, foro instrucción foreachdeterminada como se ha indicado anteriormente. Si una break instrucción no está incluida en una switchinstrucción , while, do, o forforeach , se produce un error en tiempo de compilación.Si no existe dicha instrucción envolvente, se produce un error en tiempo de compilación.

Cuando varias instrucciones switch, while, do, for o foreach se anidan entre sí, la instrucción break solo se aplica a la instrucción más interna. Para transferir el control entre varios niveles de anidamiento, se usará una goto instrucción (§13.10.4).

Una instrucción break no puede salir de un bloque finally (§13.11). Cuando se produce una break instrucción dentro de un finally bloque, el destino de la break instrucción estará dentro del mismo finally bloque; de lo contrario, se produce un error en tiempo de compilación.

Se ejecuta una break instrucción como se indica a continuación:

  • Si la instrucción break sale de uno o varios bloques try con bloques finally asociados, el control se transfiere inicialmente al bloque finally de la instrucción try más interna. Si y cuando llegue al punto final de un bloque finally, el control se transfiere al bloque finally de la instrucción try siguiente que lo envuelve. Este proceso se repite hasta que se han ejecutado los finally bloques de todas las instrucciones try intermedias.
  • El control se transfiere al destino de la instrucción break.

Dado que una break instrucción transfiere incondicionalmente el control en otro lugar, el punto final de una break instrucción nunca es accesible.

Ejemplo: una etiqueta se break resuelve en el switch_statement oiteration_statement más cercano con la etiqueta coincidente:

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (i * j > 20)
            break outer; // exits the outer for-loop
    }
}

ejemplo final

§13.10.3 La instrucción continue

continue_statement
    : 'continue' identifier? ';'
    ;

La continue instrucción inicia una nueva iteración de la instrucción envolvente whilemás cercana, , do, foro foreach .

La continue instrucción inicia una nueva iteración de la iteration_statement más cercana (§13.9) o, si se especifica un identificador, la iteration_statement más cercana etiquetada con ese identificador (consulte §13.5).

El destino de una continue instrucción es el punto final de la instrucción incrustada de la instrucción insertada más cercana que incluye whilela instrucción , do, for, o foreachiteration_statement determinada como se ha indicado anteriormente. Si una continue instrucción no está incluida en una whileinstrucción , do, foro foreach , se produce un error en tiempo de compilación.Si no existe dicha instrucción envolvente, se produce un error en tiempo de compilación.

Cuando varias while, do, for o foreach instrucciones se anidan entre sí, una continue instrucción solo se aplica a la instrucción más interna. Para transferir el control entre varios niveles de anidamiento, se usará una goto instrucción (§13.10.4).

Una instrucción continue no puede salir de un bloque finally (§13.11). Cuando se produce una continue instrucción dentro de un finally bloque, el destino de la continue instrucción estará dentro del mismo finally bloque; de lo contrario, se produce un error en tiempo de compilación.

Se ejecuta una continue instrucción como se indica a continuación:

  • Si la instrucción continue sale de uno o varios bloques try con bloques finally asociados, el control se transfiere inicialmente al bloque finally de la instrucción try más interna. Si y cuando llegue al punto final de un bloque finally, el control se transfiere al bloque finally de la instrucción try siguiente que lo envuelve. Este proceso se repite hasta que se han ejecutado los finally bloques de todas las instrucciones try intermedias.
  • El control se transfiere al destino de la instrucción continue.

Dado que una continue instrucción transfiere incondicionalmente el control en otro lugar, el punto final de una continue instrucción nunca es accesible.

Ejemplo: una etiqueta se continue resuelve en el iteration_statement más cercano con la etiqueta coincidente:

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (ShouldSkip(i, j))
            continue outer; // continues the outer for-loop
    }
}

ejemplo final

Desventajas y alternativas

Mantener las instrucciones using goto

C# ya admite goto, lo que puede lograr el mismo flujo de control. Sin embargo, goto tiene varias desventajas en comparación con la etiqueta break/continue:

  • Requiere etiquetas independientes para escenarios de interrupción frente a continuar (las etiquetas de interrupción van después del bucle, continúan las etiquetas antes de la llave de cierre).
  • La colocación de etiquetas es menos intuitiva y difiere en función de si se está rompiendo o continuando
  • Menos explícito acerca de la intención (saltar a una ubicación frente a interrumpir o continuar un bucle específico)
  • Frágil y propenso a errores: los desarrolladores deben asegurarse de que no haya instrucciones colocadas accidentalmente entre las etiquetas y sus construcciones de destino. Por ejemplo, con goto END_LOOP; seguido de END_LOOP:, es fácil insertar accidentalmente una instrucción entre ellas durante el mantenimiento, lo que rompe el flujo de control previsto. Los bucles etiquetados impiden este problema enlazando la etiqueta directamente a la construcción.
  • Lleva el estigma histórico que etiqueta la interrupción o la continuación evita

Usar variables de marca

Como se muestra en la sección motivación, las variables de marca funcionan, pero agregan una caldera significativa y ocultan la lógica de flujo de control.

Uso break N o continue N con niveles numéricos

  • Frágil durante la refactorización (agregar o quitar un nivel de bucle requiere actualizar todas las referencias numéricas)
  • Más difícil de leer (debe contar niveles para comprender el destino)
  • Menos explícito que las etiquetas con nombre
  • Falta de claridad (basado en 1? basado en 0?)

Refactorización en métodos independientes

Aunque esto suele ser una buena práctica, no siempre es factible o adecuado, y a veces introduce una complejidad innecesaria para lo que debe ser un flujo de control simple.

Esta propuesta consolida y aborda los siguientes debates de la comunidad:

Preguntas abiertas

semántica de etiquetas

La especificación actual define la semántica para como break identifier/continue identifier buscar la construcción de bucle o conmutador aplicable más interna etiquetada con ese identificador y, a continuación, se envía a ella con semántica estándar.break/continue Una formalización alternativa es decir, en su lugar, que identifica una etiqueta con las mismas reglas que break identifier/continue identifier "goto". Y si la etiqueta contiene directamente una construcción de bucle o conmutador que incluye la interrupción/continuar, ese es el bucle o conmutador al que se aplica la interrupción o la continuación.

Ambas formalizaciones son eficazmente idénticas, lo que permite y no permite el mismo conjunto de programas. El enfoque elegido en este speclet se realizó tanto para la simplicidad conceptual como literal. No tiene que cubrir el ámbito de las etiquetas, ni el enlace de identificadores tal como goto lo hace, o tener que definir la lógica de enlace de entrada externa para resolver el bucle o conmutador y la instrucción continue/break. En su lugar, simplemente expande el lenguaje de especificación simple que encuentra el bucle o conmutador envolvente adecuado según la interrupción/continuación, lo que le permite extender más allá del interior, a algo superior a eso.

Si LDM se siente más ajustado a la semántica de goto+label, no sería difícil ajustar la especificación a esa. Mantener abierta esta pregunta si el grupo siente que la última forma es más natural que la que se toma aquí.

void M()
{
  label:
  Console.WriteLine();
  
  foreach (var x in ...)
  {
    break label;
    // should this scenario fail because:
    // 1. the identifier lookup fails, or
    // 2. the label is rejected (not a valid label for a `break` since not attached to a loop construct) after being found?
  }
}

Especificación sobre cómo se declaran las etiquetas:

Cada bloque o switch_block crea un espacio de declaración independiente para las etiquetas. Los nombres se introducen en este espacio de declaración a través de labeled_statements y se hace referencia a los nombres a través de goto_statements. El espacio de declaración de etiqueta de un bloque incluye los bloques anidados. Por lo tanto, dentro de un bloque anidado no es posible declarar una etiqueta con el mismo nombre que una etiqueta en un bloque envolvente.

Especificación en instrucciones etiquetadas:

El ámbito de una etiqueta es el bloque completo en el que se declara la etiqueta, incluidos los bloques anidados. Es un error en tiempo de compilación que dos etiquetas con el mismo nombre tengan alcances superpuestos.
Se puede hacer referencia a una etiqueta desde instrucciones goto (§13.10.4) dentro del ámbito de la etiqueta.

Etiquetas anidadas

¿Se debe a: b: while (true) continue a; admitir?

Recomendación: no. No se ha pedido esto a ningún usuario. No se han presentado casos de uso atractivos. La mayoría de los idiomas estándar no lo permiten, sin quejas de sus comunidades. La lang (e impl) es más sencilla y más clara aporta estrictamente que solo la instrucción que contiene directamente etiqueta las etiquetas del bucle o modificador.

Reuniones de diseño

TBD