¿Por qué no puede haber una declaración "continuar" dentro de un bloque "finalmente"?

107

No tengo ningún problema; Tengo curiosidad. Imagina el siguiente escenario:

foreach (var foo in list)
{
    try
    {
         //Some code
    }
    catch (Exception)
    {
        //Some more code
    }
    finally
    {
        continue;
    }
}

Esto no se compilará, ya que genera el error del compilador CS0157 :

El control no puede salir del cuerpo de una cláusula final

¿Por qué?

lpaloub
fuente
7
Entonces. Sólo curioso. Si para usted tiene sentido por qué no se compila, ¿por qué quiere que alguien le explique lo que ya tiene sentido? =)
J. Steen
14
¿Por qué necesitaría una continue;en finallybloque? ¿no es lo mismo que continue;después del try -- catchbloqueo?
bansi
6
@ J.Steen No, SÍ sé que finally/continuees una limitación del compilador de C # :-) También tengo curiosidad sobre la razón de esta limitación.
xanatos
5
@xanatos: técnicamente hablando, es una limitación del CIL subyacente. De la especificación del lenguaje : "Nunca se permite que la transferencia de control ingrese a un manejador de captura o cláusula final, excepto a través del mecanismo de manejo de excepciones". y "La transferencia de control fuera de una región protegida solo se permite mediante una instrucción de excepción (dejar, finalizar.filtro, fin.captura o fin.finalmente)". La brfamilia de instrucciones de rama no puede lograr esto.
firmar
1
@Unsigned: también es una buena limitación, permitirlo sería extraño :)
user7116

Respuestas:

150

finallylos bloques se ejecutan tanto si se lanza una excepción como si no. Si se lanza una excepción, ¿qué diablos haría continue? No puede continuar la ejecución del bucle, porque una excepción no detectada transferirá el control a otra función.

Incluso si no se lanza ninguna excepción, finallyse ejecutará cuando se ejecuten otras sentencias de transferencia de control dentro del bloque try / catch, como return, por ejemplo, que trae el mismo problema.

En resumen, con la semántica de finallyno tiene sentido permitir transferir el control desde el interior de un finallybloque al exterior de este.

Apoyar esto con alguna semántica alternativa sería más confuso que útil, ya que existen soluciones simples que hacen que el comportamiento deseado sea más claro. Entonces, obtiene un error y se ve obligado a pensar correctamente sobre su problema. Es la idea general de "arrojarte al pozo del éxito" que sigue en C #.

C #, tú y el éxito

Si desea ignorar las excepciones (la mayoría de las veces es una mala idea) y continuar ejecutando el ciclo, use un bloque catch all:

foreach ( var in list )
{
    try{
        //some code
    }catch{
        continue;
    }
}

Si desea hacerlo continuesolo cuando no se produzcan excepciones no detectadas, simplemente colóquelo continuefuera del bloque try.

R. Martinho Fernandes
fuente
12
Aceptaré esto como la respuesta, porque usted hace comprender las razones por las que Microsoft posiblemente decidió no aceptar una continuación en un final. Probablemente esas imágenes también me han convencido :)
lpaloub
20
¿Puede elaborar la idea de "arrojarlo al pozo del éxito"? No lo entendí :-D
Ant
13
La imagen fue hecha por Jon Skeet, por cierto. Obtuve
R. Martinho Fernandes
1
Por supuesto, un continueen un finallybloque estará bien si solo continúa un bucle local definido dentro del finallybloque. En la pregunta, sin embargo, intenta continuar un ciclo "externo". Declaraciones similares que no puede tener dentro de un finallybloque son return, break(al salir del bloque) y goto(al ir a una etiqueta fuera del finallybloque). Para una discusión de Java relacionada, consulte Regresar de un bloque finalmente en Java .
Jeppe Stig Nielsen
32

Aquí hay una fuente confiable:

Una instrucción continue no puede salir de un bloque final (Sección 8.10). Cuando una instrucción continue ocurre dentro de un bloque final, el destino de la instrucción continue debe estar dentro del mismo bloque final; de lo contrario, se produce un error en tiempo de compilación.

Se ha tomado de MSDN, 8.9.2 La declaración continue .

La documentación dice que:

Las sentencias de un bloque finalmente se ejecutan siempre cuando el control deja una sentencia try. Esto es cierto si la transferencia de control se produce como resultado de una ejecución normal, como resultado de ejecutar una instrucción break, continue, goto o return, o como resultado de propagar una excepción fuera de la instrucción try. Si se lanza una excepción durante la ejecución de un bloque finalmente, la excepción se propaga a la siguiente instrucción try adjunta. Si otra excepción estaba en proceso de propagarse, esa excepción se pierde. El proceso de propagación de una excepción se analiza con más detalle en la descripción de la instrucción throw (Sección 8.9.5).

Es de aquí 8.10 La declaración de prueba .

Mortalus
fuente
31

Puede pensar que tiene sentido, pero en realidad no tiene sentido .

foreach (var v in List)
{
    try
    {
        //Some code
    }
    catch (Exception)
    {
        //Some more code
        break; or return;
    }
    finally
    {
        continue;
    }
}

¿Qué piensa hacer un descanso o continuar cuando se lanza una excepción? El equipo del compilador de C # no quiere tomar decisiones por su cuenta asumiendo breako continue. En cambio, decidieron quejarse de que la situación del desarrollador será ambigua para transferir el control finally block.

Por lo tanto, el trabajo del desarrollador es indicar claramente lo que pretende hacer en lugar de que el compilador asuma otra cosa.

¡Espero que entiendas por qué esto no se compila!

Sriram Sakthivel
fuente
En una nota relacionada, incluso si no hubiera "captura", la ruta de ejecución que sigue a una finallydeclaración se vería afectada por si se catchhabía salido a través de una excepción, y no hay ningún mecanismo para decir cómo debería interactuar con a continue. Sin embargo, me gusta tu ejemplo porque muestra un problema aún mayor.
supercat
@supercat De acuerdo con usted, mi respuesta muestra un ejemplo de situación ambigua para el compilador, aún hay muchos problemas con este enfoque.
Sriram Sakthivel
16

Como han dicho otros, pero centrados en las excepciones, en realidad se trata de un manejo ambiguo de la transferencia de control.

En su mente, probablemente esté pensando en un escenario como este:

public static object SafeMethod()
{
    foreach(var item in list)
    {
        try
        {
            try
            {
                //do something that won't transfer control outside
            }
            catch
            {
                //catch everything to not throw exceptions
            }
        }
        finally
        {
            if (someCondition)
                //no exception will be thrown, 
                //so theoretically this could work
                continue;
        }
    }

    return someValue;
}

Teóricamente, puede rastrear el flujo de control y decir, sí, esto está "bien". No se lanza ninguna excepción, no se transfiere ningún control. Pero los diseñadores del lenguaje C # tenían otros problemas en mente.

La excepción lanzada

public static void Exception()
{
    try
    {
        foreach(var item in list)
        {
            try
            {
                throw new Exception("What now?");
            }
            finally
            {
                continue;
            }
        }
    }
    catch
    {
        //do I get hit?
    }
}

El temido Goto

public static void Goto()
{
    foreach(var item in list)
    {
        try
        {
            goto pigsfly;
        }
        finally
        {
            continue;
        }
    }

    pigsfly:
}

El regreso

public static object ReturnSomething()
{
    foreach(var item in list)
    {
        try
        {
            return item;
        }
        finally
        {
            continue;
        }
    }
}

El rompimiento

public static void Break()
{
    foreach(var item in list)
    {
        try
        {
            break;
        }
        finally
        {
            continue;
        }
    }
}

Así que en conclusión, sí, mientras que no es una ligera posibilidad de utilizar una continuede las situaciones en que no se están transfiriendo el control, pero una buena oferta (la mayoría?) De los casos se trata de excepciones o returnbloques. Los diseñadores del lenguaje sintieron que esto sería demasiado ambiguo y (probablemente) imposible de garantizar en el momento de la compilación que su continuese usa solo en casos donde el flujo de control no se está transfiriendo.

Chris Sinclair
fuente
Obtuve la misma situación en java cuando proguard se bloqueaba durante la ofuscación. Tu explicación es bastante buena. C # da un error de tiempo de compilación, tiene mucho sentido.
Dev_Vikram
11

En general continue, no tiene sentido cuando se usa en finallybloque. Mira esto:

foreach (var item in list)
{
    try
    {
        throw new Exception();
    }
    finally{
        //doesn't make sense as we are after exception
        continue;
    }
}
ghord
fuente
"En general" muchas cosas no tienen sentido. Y no significa que no deba funcionar "en particular". IE: continueno tiene sentido fuera de la declaración de ciclo, pero no significa que no sea compatible.
zerkms
Creo que tiene sentido. itempuede ser archivo, lectura fallida -> finallycierra el archivo. continueevita el resto del procesamiento.
jnovacho
5

"Esto no se compilará y creo que tiene mucho sentido"

Bueno, creo que no.

Cuando literalmente lo catch(Exception)tienes, no necesitas el finalmente (y probablemente ni siquiera el continue).

Cuando tenga el más realista catch(SomeException), ¿qué debería suceder cuando no se detecta una excepción? Tu continuequieres ir en una dirección, la excepción manejando otra.

Henk Holterman
fuente
2
Creo que podrías necesitar finallycuando lo tengas catch. Se usa comúnmente para cerrar recursos de manera confiable.
jnovacho
Sin embargo, no son solo excepciones. Fácilmente podría tener una returndeclaración dentro de sus bloques tryo catch. ¿Que hacemos ahora? ¿Volver o continuar el bucle? (y si continuamos, ¿qué pasa si es el último elemento en iterar? ¿Entonces simplemente continuamos fuera del foreachy el retorno nunca ocurre?) EDITAR: O incluso gotoa una etiqueta fuera del ciclo, prácticamente cualquier acción que transfiera el control fuera del foreachciclo. .
Chris Sinclair
2
No estoy de acuerdo con "Cuando literalmente tienes catch (Exception), no necesitas el finalmente (y probablemente ni siquiera el continuar)". ¿Qué sucede si necesito hacer una operación independientemente de la excepción que haya generado o no?
lpaloub
De acuerdo, finalmente se ocupa de los correos electrónicos adicionales returndentro del bloque. Pero normalmente la ejecución continúa después del catch-all.
Henk Holterman
'finalmente' no es 'captura'. Debe usarse para el código de limpieza. un bloque finalmente se ejecuta independientemente de si se lanza una excepción o no . Cosas como cerrar archivos o liberar memoria (si está utilizando código no administrado), etc., estas son las cosas que coloca en un bloque
final
3

No se puede dejar el cuerpo de un bloque definitivo. Esto incluye palabras clave de descanso, retorno y, en su caso, continuar.

Joseph Devlin
fuente
3

El finallybloque se puede ejecutar con una excepción a la espera de ser relanzado. Realmente no tendría sentido poder salir del bloque (mediante a continueo cualquier otra cosa) sin volver a lanzar la excepción.

Si desea continuar con su ciclo pase lo que pase, no necesita la declaración finalmente: simplemente capture la excepción y no vuelva a lanzar.

Falanwe
fuente
1

finallyse ejecuta tanto si se lanza una excepción no detectada como si no. Otros ya han explicado por qué esto lo vuelve continueilógico, pero aquí hay una alternativa que sigue el espíritu de lo que parece estar pidiendo este código. Básicamente, finally { continue; }está diciendo:

  1. Cuando haya excepciones detectadas, continúe
  2. Cuando haya excepciones no detectadas, permita que se lancen, pero continúe

(1) podría satisfacerse colocando continueal final de cada uno catch, y (2) podría satisfacerse almacenando excepciones no detectadas para lanzarlas más tarde. Podrías escribirlo así:

var exceptions = new List<Exception>();
foreach (var foo in list) {
    try {
        // some code
    } catch (InvalidOperationException ex) {
        // handle specific exception
        continue;
    } catch (Exception ex) {
        exceptions.Add(ex);
        continue;
    }
    // some more code
}
if (exceptions.Any()) {
    throw new AggregateException(exceptions);
}

En realidad, finallytambién se habría ejecutado en el tercer caso, donde no hubo excepciones lanzadas en absoluto, capturadas o no capturadas. Si lo desea, por supuesto, puede colocar un solo continuedespués del bloque try-catch en lugar de dentro de cada uno catch.

nmclean
fuente
1

Técnicamente hablando, es una limitación del CIL subyacente. De la especificación del idioma :

Nunca se permite que la transferencia de control ingrese a un manejador de captura o cláusula final, excepto a través del mecanismo de manejo de excepciones.

y

Transferir el control de una región protegida sólo se permite a través de una instrucción excepción ( leave, end.filter, end.catch, o end.finally)

En la página del documento para la brinstrucción :

Esta instrucción no puede realizar transferencias de control dentro y fuera de prueba, captura, filtro y finalmente bloques.

Este último es válido para todas las instrucciones de bifurcación, incluyendo beq, brfalse, etc.

No firmado
fuente
-1

Los diseñadores del lenguaje simplemente no querían (o no podían) razonar acerca de que la semántica de un bloque final fuera terminado por una transferencia de control.

Un problema, o quizás el problema clave, es que el finallybloque se ejecuta como parte de alguna transferencia de control no local (procesamiento de excepción). El objetivo de esa transferencia de control no es el ciclo circundante; el procesamiento de excepciones aborta el ciclo y continúa desenrollando más.

Si tenemos una transferencia de control fuera del finallybloque de limpieza, entonces la transferencia de control original está siendo "secuestrada". Se cancela y el control va a otra parte.

La semántica se puede resolver. Otros idiomas lo tienen.

Los diseñadores de C # decidieron simplemente no permitir las transferencias de control estáticas, "tipo goto", simplificando un poco las cosas.

Sin embargo, incluso si hace eso, no resuelve la pregunta de qué sucede si se inicia una transferencia dinámica desde un finally: ¿y si el bloque finalmente llama a una función y esa función lanza? El procesamiento de la excepción original se "secuestra".

Si trabaja con la semántica de esta segunda forma de secuestro, no hay razón para desterrar el primer tipo. En realidad, son lo mismo: una transferencia de control es una transferencia de control, tenga el mismo alcance léxico o no.

Kaz
fuente
La diferencia es que, finallypor definición, debe seguir inmediatamente la transferencia que la desencadenó (una excepción no detectada return, etc.). Sería fácil permitir transferencias "goto-like" dentro finally(¿qué lenguajes hacen esto, por cierto?), Pero hacerlo significaría que es try { return } finally { ... } posible que no regrese , lo cual es completamente inesperado. Si finallyllama a una función que lanza, eso no es realmente lo mismo, porque ya esperamos que puedan ocurrir excepciones en cualquier momento e interrumpir el flujo normal.
nmclean
@nmclean: En realidad, una excepción que se escapa finallyes algo parecido, ya que puede resultar en un flujo de programa inesperado e ilógico. La única diferencia es que los diseñadores del lenguaje, al no poder rechazar todos los programas en los que un bloque finalmente podría lanzar una excepción no controlada, en cambio permiten que dichos programas se compilen y esperan que el programa pueda aceptar cualquier consecuencia que pueda seguir al abandono de la anterior excepción: desenrollar secuencia.
supercat
@supercat Estoy de acuerdo en que interrumpe el flujo esperado, pero mi punto es que ese es siempre el caso de las excepciones no controladas, ya sea que el flujo actual sea ao finallyno. Según la lógica del último párrafo de Kaz, las funciones también deberían tener la libertad de afectar el flujo en el alcance de su llamador a través de continueetc., porque se les permite hacerlo mediante excepciones. Pero esto, por supuesto, no está permitido; las excepciones son la única excepción a esta regla, de ahí el nombre "excepción" (o "interrupción").
nmclean
@nmclean: las excepciones no anidadas representan un flujo de control que difiere de la ejecución normal, pero aún está estructurado. La declaración que sigue a un bloque solo debe ejecutarse si todas las excepciones que ocurrieron dentro de ese bloque fueron capturadas dentro de él. Eso puede parecer una expectativa razonable, pero una excepción que ocurre dentro de un finallybloque puede violarla. Realmente una situación desagradable, que en mi humilde opinión debería haber sido resuelta al mínimo informando a los finallybloques sobre cualquier excepción pendiente para que puedan construir objetos de excepción compuestos.
supercat
@mmclean Lo siento, no estoy de acuerdo con que el nombre "excepción" provenga de alguna "excepción a la regla" (con respecto al control) específica de C #, LOL. El aspecto de alteración del flujo de control de una excepción es simplemente una transferencia de control dinámico no local. Una transferencia de control regular dentro del mismo ámbito léxico (sin que se produzca un desenrollamiento) puede considerarse como un caso especial de eso.
Kaz