¿Por qué no puede aparecer el rendimiento de rendimiento dentro de un bloque de prueba con una captura?

95

Lo siguiente está bien:

try
{
    Console.WriteLine("Before");

    yield return 1;

    Console.WriteLine("After");
}
finally
{
    Console.WriteLine("Done");
}

El finallybloque se ejecuta cuando todo ha terminado de ejecutarse ( IEnumerator<T>admite IDisposableproporcionar una forma de garantizar esto incluso cuando la enumeración se abandona antes de que finalice).

Pero esto no está bien:

try
{
    Console.WriteLine("Before");

    yield return 1;  // error CS1626: Cannot yield a value in the body of a try block with a catch clause

    Console.WriteLine("After");
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Supongamos (por el bien del argumento) que una u otra de las WriteLinellamadas dentro del bloque try lanza una excepción . ¿Cuál es el problema de continuar la ejecución en catchbloque?

Por supuesto, la parte de retorno de rendimiento (actualmente) no puede arrojar nada, pero ¿por qué eso debería evitar que tengamos un cierre try/ catchpara lidiar con las excepciones lanzadas antes o después de un yield return?

Actualización: Hay un comentario interesante de Eric Lippert aquí : ¡parece que ya tienen suficientes problemas para implementar correctamente el comportamiento de prueba / finalmente!

EDITAR: La página de MSDN en este error es: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Sin embargo, no explica por qué.

Daniel Earwicker
fuente
2
Enlace directo al comentario de Eric Lippert: blogs.msdn.com/oldnewthing/archive/2008/08/14/…
Roman Starkov
nota: tampoco puedes ceder en el bloque de captura en sí :-(
Simon_Weaver
2
El enlace Oldnewthing ya no funciona.
Sebastian Redl

Respuestas:

50

Sospecho que se trata de una cuestión de practicidad más que de viabilidad. Sospecho que hay muy, muy pocas ocasiones en las que esta restricción es en realidad un problema que no se puede solucionar, pero la complejidad adicional en el compilador sería muy significativa.

Hay algunas cosas como esta que ya he encontrado:

  • Los atributos no pueden ser genéricos
  • Incapacidad de que X se derive de XY (una clase anidada en X)
  • Bloques de iteradores usando campos públicos en las clases generadas

En cada uno de estos casos, sería posible ganar un poco más de libertad, a costa de una complejidad adicional en el compilador. El equipo tomó la decisión pragmática, por lo que los aplaudo: prefiero tener un lenguaje un poco más restrictivo con un compilador 99.9% preciso (sí, hay errores; me encontré con uno en SO el otro día) que uno más lenguaje flexible que no se pudo compilar correctamente.

EDITAR: Aquí hay una pseudoprueba de cómo es factible.

Considere eso:

  • Puede asegurarse de que la parte de retorno de rendimiento en sí no arroje una excepción (calcule previamente el valor, y luego solo está configurando un campo y devolviendo "verdadero")
  • Se le permite try / catch que no usa rendimiento de retorno en un bloque iterador.
  • Todas las variables locales en el bloque del iterador son variables de instancia en el tipo generado, por lo que puede mover el código libremente a nuevos métodos

Ahora transforma:

try
{
    Console.WriteLine("a");
    yield return 10;
    Console.WriteLine("b");
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

en (especie de pseudocódigo):

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    __current = 10;
    return true;

case just_after_yield_return:
    try
    {
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        CatchBlock();
    }
    goto case post;

case post;
    Console.WriteLine("Post");


void CatchBlock()
{
    Console.WriteLine("Catch block");
}

La única duplicación está en la configuración de bloques try / catch, pero eso es algo que el compilador ciertamente puede hacer.

Es posible que me haya perdido algo aquí; si es así, ¡házmelo saber!

Jon Skeet
fuente
11
Una buena prueba de concepto, pero esta estrategia se vuelve dolorosa (probablemente más para un programador de C # que para un compilador de C #) una vez que comienza a crear ámbitos con cosas como usingy foreach. Por ejemplo:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian
La semántica normal de "try / catch" implica que si se omite cualquier parte de un bloque try / catch debido a una excepción, el control se transferirá a un bloque "catch" adecuado, si existe. Desafortunadamente, si ocurre una excepción "durante" una devolución de rendimiento, el iterador no puede distinguir los casos en los que se elimina debido a una excepción de aquellos en los que se elimina porque el propietario recuperó todos los datos de interés.
supercat
7
"Sospecho que hay muy, muy pocas ocasiones en las que esta restricción es en realidad un problema que no se puede solucionar". Es como decir que no necesita excepciones porque puede usar la estrategia de devolución de código de error que se usa comúnmente en C, por lo que Hace muchos años. Admito que las dificultades técnicas pueden ser significativas, pero esto aún limita severamente la utilidad de yield, en mi opinión, debido al código espagueti que tienes que escribir para solucionarlo.
jpmc26
@ jpmc26: No, realmente no es como decir eso en absoluto. No puedo recordar que esto me haya mordido nunca , y he usado bloques de iteradores muchas veces. Limita ligeramente la utilidad de la yieldOMI, está muy lejos de ser grave .
Jon Skeet
2
Esta 'característica' en realidad requiere un código bastante feo en algunos casos para solucionarlo, consulte stackoverflow.com/questions/5067188/…
namey
5

Todas las yielddeclaraciones en una definición de iterador se convierten a un estado en una máquina de estado que utiliza efectivamente una switchdeclaración para avanzar estados. Si se hizo generar código para yielddeclaraciones en un try / catch tendría que duplicar todo en el trybloque para cada yield declaración, excluyendo todos los demás yielddeclaración para ese bloque. Esto no siempre es posible, especialmente si una yielddeclaración depende de una anterior.

Mark Cidade
fuente
2
No creo que me lo traiga. Creo que sería completamente factible, pero muy complicado.
Jon Skeet
2
Los bloques Try / catch en C # no están destinados a volver a entrar. Si los divide, es posible llamar a MoveNext () después de una excepción y continuar el bloque try con un estado posiblemente no válido.
Mark Cidade
2

Yo especularía que debido a la forma en que la pila de llamadas se enrolla / desenrolla cuando se obtiene el retorno de un enumerador, se vuelve imposible que un bloque try / catch "capture" la excepción. (porque el bloque de retorno de rendimiento no está en la pila, aunque él originó el bloque de iteración)

Para tener una idea de lo que estoy hablando, configure un bloque de iterador y un foreach usando ese iterador. Verifique cómo se ve la pila de llamadas dentro del bloque foreach y luego verifíquelo dentro del bloque try / finalmente del iterador.

Radu094
fuente
Estoy familiarizado con el desenrollado de pilas en C ++, donde los destructores se llaman en objetos locales que salen del alcance. Lo correspondiente en C # sería probar / finalmente. Pero ese desenvolvimiento no ocurre cuando ocurre el retorno del rendimiento. Y para try / catch no es necesario que interactúe con el rendimiento.
Daniel Earwicker
Compruebe lo que le sucede a la pila de llamadas al recorrer un iterador y comprenderá lo que quiero decir
Radu094
@ Radu094: No, estoy seguro de que sería posible. No olvides que ya se maneja finalmente, que es al menos algo similar.
Jon Skeet
2

He aceptado la respuesta de THE INVINCIBLE SKEET hasta que alguien de Microsoft viene a echar agua fría sobre la idea. Pero no estoy de acuerdo con la parte de la cuestión de la opinión; por supuesto, un compilador correcto es más importante que uno completo, pero el compilador de C # ya es muy inteligente al resolver esta transformación para nosotros en la medida en que lo hace. Un poco más de integridad en este caso haría que el lenguaje sea más fácil de usar, enseñar, explicar, con menos casos extremos o errores. Así que creo que valdría la pena el esfuerzo extra. Algunos tipos en Redmond se rascan la cabeza durante quince días y, como resultado, millones de programadores durante la próxima década pueden relajarse un poco más.

(También albergo un sórdido deseo de que haya una manera de hacer yield returnlanzar una excepción que ha sido metida en la máquina de estado "desde el exterior", por el código que impulsa la iteración. Pero mis razones para querer esto son bastante oscuras).

En realidad, una consulta que tengo sobre la respuesta de Jon tiene que ver con el lanzamiento de la expresión de retorno de rendimiento.

Obviamente, el rendimiento de rendimiento 10 no es tan malo. Pero esto sería malo:

yield return File.ReadAllText("c:\\missing.txt").Length;

Entonces, ¿no tendría más sentido evaluar esto dentro del bloque try / catch anterior?

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
        __current = File.ReadAllText("c:\\missing.txt").Length;
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    return true;

El siguiente problema serían los bloques try / catch anidados y las excepciones relanzadas:

try
{
    Console.WriteLine("x");

    try
    {
        Console.WriteLine("a");
        yield return 10;
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        Console.WriteLine("y");

        if ((DateTime.Now.Second % 2) == 0)
            throw;
    }
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

Pero estoy seguro de que es posible ...

Daniel Earwicker
fuente
1
Sí, pondría la evaluación en el try / catch. Realmente no importa dónde coloque la configuración de la variable. El punto principal es que puede dividir efectivamente un solo intento / captura con un rendimiento de rendimiento en dos intentos / capturas con un rendimiento de rendimiento entre ellos.
Jon Skeet