2008-12-06 8 views
78

El siguiente es aceptable:¿Por qué no puede arrojar retorno aparecer dentro de un bloque de prueba con un truco?

try 
{ 
    Console.WriteLine("Before"); 

    yield return 1; 

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

Los finally carreras de bloque cuando todo ha terminado de ejecutarse (IEnumerator<T> apoya IDisposable para proporcionar una manera de asegurar esto, incluso cuando se abandona la enumeración antes de que termine).

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 que (por el bien del argumento) que se lanza una excepción por uno u otro de los WriteLine llamadas dentro del bloque try. ¿Cuál es el problema con continuar la ejecución en el bloque catch?

Por supuesto, la parte de retorno rendimiento es (actualmente) no puede tirar nada, pero ¿por qué debería impedirnos tener un encerrando try/catch para hacer frente a las excepciones producidas antes o después de un yield return?

Actualización: ¡Hay un interesting comment from Eric Lippert here - parece que ya tienen suficientes problemas para implementar correctamente el comportamiento try/finally!

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é.

+2

Enlace directo al comentario de Eric Lippert: http://blogs.msdn.com/oldnewthing/archive/2008/08/14/8862242.aspx# 8867246 –

+0

nota: no se puede ceder en el bloque catch tampoco :-( –

Respuesta

42

Sospecho que esto es una cuestión de practicidad en lugar de factibilidad. Sospecho que hay muy, muy pocas veces donde esta restricción es en realidad, un problema que no se puede evitar, pero la complejidad añadida en el compilador sería muy significativa.

Hay algunas cosas como esta que yo ya he encontrado:

  • atributos no pudiendo ser genéricos
  • Incapacidad para X que se derivan de X.Y (una clase anidada en X)
  • bloques Iterator utilizando 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 hizo la elección pragmática, por lo que los aplaudo. Prefiero tener un lenguaje un poco más restrictivo con un compilador preciso del 99.9% (sí, hay errores, encontré uno en SO el otro día) que un lenguaje flexible que no pudo compilar correctamente.

EDITAR: Aquí hay una pseudo-prueba de cómo es posible.

considerar que:

  • Puede asegurarse de que la propia parte de retorno de rendimiento no produce una excepción (precalcular el valor, y entonces estás sólo la creación de un campo y devolver la "verdadera")
  • Está permitido try/catch que no utiliza rendimiento en un bloque iterador.
  • Todas las variables locales en el bloque iterador son variables de instancia de tipo generado, para que pueda moverse libremente código para nuevos métodos

Ahora transformar:

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

en (una especie de pseudo-có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 es en la configuración de bloques de prueba/captura, pero eso es algo que el compilador puede hacer.

Puede que me haya perdido algo aquí - si es así, ¡por favor hágamelo saber!

+8

Una buena prueba de concepto, pero esta estrategia se vuelve dolorosa (probablemente más para un programador de C# que para un escritor de compiladores de C#) una vez que comienzas a crear ámbitos con cosas como 'using' y' foreach'. Por ejemplo: 'try {foreach (string s en c) {return return s;}} catch (Exception) {}' – Brian

+0

La semántica normal de "try/catch" implica que si se salta alguna parte de un bloque try/catch debido a una excepción, el control se transferirá a un bloque de "captura" adecuado, si existe. Desafortunadamente, si se produce una excepción "durante" un retorno de rendimiento, no hay forma de que el iterador distinga los casos en que se está Dispuesto debido a una excepción de aquellos en los que se está Disolviendo porque el propietario recuperó todos los datos de interés. – supercat

+4

"Sospecho que hay muy pocas veces en las que esta restricción es realmente un problema que no se puede solucionar". Es como decir que no necesita excepciones porque puede usar la estrategia de devolución del código de error comúnmente utilizada en C tantos años atrás Admito que las dificultades técnicas pueden ser significativas, pero esto todavía limita severamente la utilidad del "rendimiento", en mi opinión, debido al código de espagueti que tienes que escribir para solucionarlo. – jpmc26

2

Me gustaría especular que debido a la forma en que la pila de llamadas se activa/desenrolla cuando se produce el retorno de un enumerador, resulta imposible que un bloque try/catch realmente "atrape" la excepción. (porque el bloque de retorno de rendimiento no está en la pila, aunque originó el bloque de iteración)

Para obtener una idea de lo que estoy hablando, configure un bloque de iteradores y un foreach utilizando ese iterador. Comprueba cómo se ve la pila de llamadas dentro del bloque foreach y luego márcala dentro del bloque try/finally del iterador.

+0

Estoy familiarizado con el desenrollado de la pila en C++, donde se invocan los destructores sobre los objetos locales que salen del ámbito. Lo correspondiente en C# sería try/finally. Pero ese desenrollamiento no ocurre cuando ocurre un retorno de rendimiento. Y para try/catch no hay necesidad de que interactúe con yield return. –

+0

Comprueba qué ocurre con la pila de llamadas cuando bucles sobre un iterador y entenderás lo que quiero decir – Radu094

+0

@ Radu094: No, estoy seguro de que sería posible. No olvides que ya se maneja finalmente, que es al menos algo similar. –

5

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

+2

No creo que compre eso. Creo que sería completamente factible, pero muy complicado. –

+2

Los bloques de prueba/captura en C# no están destinados a ser reentrantes. Si los divide, entonces es posible llamar a MoveNext() después de una excepción y continuar el bloque try con un estado posiblemente no válido. –

2

He aceptado la respuesta de THE INVINCIBLE SKEET hasta que alguien de Microsoft viene a verter agua fría sobre la idea. Pero no estoy de acuerdo con la parte de la materia de opinión; por supuesto, un compilador correcto es más importante que uno completo, pero el compilador de C# ya es muy astuto al clasificar esta transformación para nosotros en la medida de lo posible. Un poco más de exhaustividad en este caso facilitaría el uso del lenguaje, enseñarlo, explicarlo, con menos casos extremos o errores. Entonces creo que valdría la pena el esfuerzo extra. Algunos chicos en Redmond se rascan la cabeza durante una quincena y, como resultado, millones de codificadores durante la próxima década pueden relajarse un poco más.

(También albergo un sórdido deseo de que haya una forma de hacer yield return lanzar una excepción que ha sido introducida en la máquina de estado "desde afuera", por el código que dirige la iteración. Pero mis razones para querer esto son bastante oscuros.)

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

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

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

Así que no tendría más sentido para evaluar este dentro del bloque precedente try/catch:

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 podría estar anidados bloques try/catch y excepciones: relanza

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

+1

Sí, pondría la evaluación en el try/catch. En realidad, no importa dónde coloque la configuración de la variable. El punto principal es que puede efectivamente romper un único intento/captura con un retorno de rendimiento en dos intentos/capturas con un rendimiento entre ellos. –

-5

para aquellos que utilizan la ONU dad:

yield return new WaitForSeconds(startWait); 
while (numWaves < 4 && _myPauseState) 
{ 
for (int i = 0; i < hazardCount;) 
{ 
//spawn code 
} 
yield return new WaitForSeconds(waveWait); 
numWaves++; 
} 

es realmente posible en el interior de un IEnumerator

+7

¿Dónde está el 'try' /' catch'? –

Cuestiones relacionadas