c# exception-handling system.reflection

¿En qué se diferencian las cláusulas de "falla" de CIL de las cláusulas de "captura" en C#?



exception-handling system.reflection (5)

De acuerdo con la norma CLI (Partition IIA, capítulo 19) y la página de referencia de MSDN para la enumeración System.Reflection.ExceptionHandlingClauseOptions , existen cuatro tipos diferentes de bloques de controladores de excepciones:

  • Cláusulas catch : "Capturar todos los objetos del tipo especificado".
  • Cláusulas de filtro : "Introduzca el controlador solo si el filtro tiene éxito".
  • Finalmente, cláusulas: "Manejar todas las excepciones y la salida normal".
  • cláusulas de error : "Manejar todas las excepciones pero no la salida normal".

Dadas estas breves explicaciones (citadas de la Norma CLI, por cierto), estas deben asignarse a C # de la siguiente manera:

  • captura - catch (FooException) { … }
  • filtro : no está disponible en C # (pero en VB.NET como Catch FooException When booleanExpression )
  • finalmente - finally { … }
  • falla - catch { … }

Experimentar:

Un experimento simple muestra que esta asignación no es lo que realmente hace el compilador de C # de .NET:

// using System.Linq; // using System.Reflection; static bool IsCatchWithoutTypeSpecificationEmittedAsFaultClause() { try { return MethodBase .GetCurrentMethod() .GetMethodBody() .ExceptionHandlingClauses .Any(clause => clause.Flags == ExceptionHandlingClauseOptions.Fault); } catch // <-- this is what the above code is inspecting { throw; } }

Este método devuelve false . Es decir, la catch { … } no se ha emitido como una cláusula de error.

Un experimento similar muestra que, de hecho, se emitió una cláusula catch ( clause.Flags == ExceptionHandlingClauseOptions.Clause ), aunque no se haya especificado ningún tipo de excepción.

Preguntas:

  1. Si catch { … } realmente es una cláusula catch, entonces, ¿en qué se diferencian las cláusulas de fallo de las cláusulas catch?
  2. ¿El compilador de C # alguna vez emite cláusulas de falla?

Hay cuatro tipos diferentes de bloques de control de excepciones:

  • Cláusulas catch : "Capturar todos los objetos del tipo especificado".
  • Cláusulas de filtro : "Introduzca el controlador solo si el filtro tiene éxito".
  • Finalmente, cláusulas: "Manejar todas las excepciones y la salida normal".
  • cláusulas de error : "Manejar todas las excepciones pero no la salida normal".

Dadas estas breves explicaciones (citadas de la Norma CLI, por cierto), estas deben asignarse a C # de la siguiente manera:

  • captura - catch (FooException) { … }
  • filtro : no está disponible en C # (pero en VB.NET como Catch FooException When booleanExpression )
  • finalmente - finally { … }
  • falla - catch { … }

Es esa última línea donde te equivocaste. Lea las descripciones de nuevo. fault y, finally se describen de manera prácticamente idéntica. La diferencia entre ellos es que finally siempre se ingresa, mientras que la fault solo se ingresa si el control abandona el try por una excepción. Tenga en cuenta que esto significa que un bloque catch ya puede haber actuado.

Si escribes esto en C #:

try { ... } catch (SpecificException ex) { ... } catch { ... }

Entonces no hay forma de que se ingrese al tercer bloque si el control abandona el try través de una SpecificException . Es por eso que catch {} no es un mapeo por fault .


1. Si catch { … } realmente es una cláusula catch, entonces ¿en qué se diferencian las cláusulas de fallo de las cláusulas catch?

El compilador de C # (al menos el que se envía con .NET) en realidad parece compilar catch { … } como si realmente fuera catch (object) { … } . Esto se puede mostrar con el siguiente código.

// using System; // using System.Linq; // using System.Reflection; static Type GetCaughtTypeOfCatchClauseWithoutTypeSpecification() { try { return MethodBase .GetCurrentMethod() .GetMethodBody() .ExceptionHandlingClauses .Where(clause => clause.Flags == ExceptionHandlingClauseOptions.Clause) .Select(clause => clause.CatchType) .Single(); } catch // <-- this is what the above code is inspecting { throw; } }

Ese método devuelve typeof(object) .

Entonces, conceptualmente, un manejador de fallas es similar a una catch { … } ; sin embargo, el compilador de C # nunca genera código para esa construcción exacta, sino que pretende que es un catch (object) { … } , que es conceptualmente una cláusula catch. Así se emite una cláusula catch.

Nota al margen: el libro de Jeffrey Richter "CLR a través de C #" contiene información relacionada (en las páginas 472–474): es decir, que el CLR permite lanzar cualquier valor, no solo objetos Exception . Sin embargo, a partir de la versión 2 de CLR, los valores que no son de Exception se envuelven automáticamente en un objeto RuntimeWrappedException . Así que parece algo sorprendente que C # transformaría la catch en catch (object) lugar de catch (Exception) . Sin embargo, hay una razón para esto: se puede decir a CLR que no envuelva valores que no sean de Exception aplicando un [assembly: RuntimeCompatibility(WrapNonExceptionThrows = false)] .

Por cierto, el compilador VB.NET, a diferencia del compilador C #, traduce Catch to Catch anonymousVariable As Exception .

2. ¿Alguna vez el compilador de C # genera cláusulas de falla?

Obviamente no emite cláusulas de fallo para la catch { … } . Sin embargo, la publicación del blog de Bart de Smet "Desafío del lector: manejadores de fallas en C #" sugiere que el compilador de C # produce cláusulas de falla en ciertas circunstancias.


Como lo ha señalado la gente, en general el compilador de C # no genera manejadores de fallas. Sin embargo, stakx enlazó con la publicación del blog de Bart de Smet sobre cómo hacer que el compilador de C # genere un controlador de fallas.

C # usa controladores de fallas para implementar el uso de sentencias que están dentro de los bloques de iteradores. Por ejemplo, el siguiente código de C # hará que el compilador utilice una cláusula de error:

public IEnumerable<string> GetSomeEnumerable() { using (Disposable.Empty) { yield return DoSomeWork(); } }

Al descompilar el ensamblaje generado con dotPeek y la opción "Mostrar código generado por el compilador", puede ver la cláusula de falla:

bool IEnumerator.MoveNext() { try { switch (this.<>1__state) { case 0: this.<>1__state = -1; this.<>7__wrap1 = Disposable.Empty; this.<>1__state = 1; this.<>2__current = this.<>4__this.DoSomeWork(); this.<>1__state = 2; return true; case 2: this.<>1__state = 1; this.<>m__Finally2(); break; } return false; } __fault { this.System.IDisposable.Dispose(); } }

Donde normalmente una declaración de uso se asignaría a un bloque try / finally, esto no tiene sentido para los bloques de iteradores: un try / finally Dispose después de que se genere el primer valor.

Pero si DoSomeWork lanza una excepción, usted quiere Desechar. Así que un controlador de fallas es útil aquí. Llamará a Dispose solo en el caso de que ocurra una excepción, y permitirá que la excepción crezca. Conceptualmente, esto es similar a un bloque de captura que se dispone y luego se vuelve a lanzar.


Las excepciones de .NET se incorporan al soporte del sistema operativo para excepciones. Llamado manejo estructurado de excepciones en Windows. Los sistemas operativos Unix tienen algo similar, señales.

Una excepción administrada es un caso muy específico de una excepción SEH. El código de excepción es 0xe0434f53. Los últimos tres pares de hechizos deletrean "COM", te dicen algo sobre la forma en que se inició .NET.

Un programa en general puede tener interés en saber cuándo se genera y maneja una excepción, no solo las excepciones administradas. También puedes ver esto en el compilador MSVC C ++. Una cláusula catch (...) solo detecta las excepciones de C ++. Pero si compila con la opción / EHa, entonces detecta cualquier excepción. Incluyendo las cosas realmente desagradables, las excepciones del procesador como las violaciones de acceso.

La cláusula de falla es la versión de CLR de eso, su bloque asociado se ejecutará para cualquier excepción del sistema operativo, no solo para las administradas. Los lenguajes C # y VB.NET no son compatibles con esto, solo admiten el manejo de excepciones para las excepciones administradas. Pero otros idiomas pueden, solo conozco el compilador de C ++ / CLI que los emite. Hecho, por ejemplo, en su versión de la declaración de uso , llamada "semántica de pila".

Tiene sentido que C ++ / CLI lo admita, después de todo, es un lenguaje que soporta fuertemente llamar directamente código nativo desde código administrado. Pero no para C # y VB.NET, solo ejecutan código no administrado a través del contador de referencias Pinvoke o la capa de interoperabilidad COM en el CLR. Lo que ya configura un controlador ''catch-them-all'' que traduce las excepciones no administradas en administradas. Cuál es el mecanismo mediante el cual obtiene una excepción System.AccessViolationException.


Un bloque de falla sería equivalente a decir:

bool success; try { success = false; ... do stuff success = true; // Also include this immediately before any ''return'' } finally { if (!success) { ... do "fault" stuff here } }

Tenga en cuenta que esto es algo diferente semánticamente de la captura y el rebrote. Entre otras cosas, con la implementación anterior, si se produce una excepción y el seguimiento de la pila está informando los números de línea, incluirá el número de la línea en ...do stuff donde ocurrió la excepción. Por el contrario, cuando se utiliza la captura y reencendido, la traza de la pila informará el número de línea del reenvío. Si ...do stuff incluye dos o más llamadas a foo , y una de esas llamadas lanza una excepción, saber el número de línea de la llamada que falló podría ser útil, pero la captura y la repetición perderían esa información.

Los problemas más grandes con la implementación anterior son que uno debe agregar manualmente success = true; a cada lugar en el código que podría salir del bloque try , y que no hay forma de que el bloque finally sepa qué excepción puede estar pendiente. Si tuviera mis druthers, habría una sentencia finally (Exception ex) que establecería Ex en la excepción que causó la salida del bloque try (o null si el bloque salía normalmente). Esto no solo eliminaría la necesidad de establecer manualmente el indicador de "éxito", sino que también permitiría un manejo razonable del caso en el que se produce una excepción en el código de limpieza. En tal situación, uno no debe ocultar la excepción de limpieza (incluso si la excepción original normalmente representara una condición que el código de llamada estaba esperando, la falla de limpieza probablemente representa una condición que no fue) pero probablemente no quiera perder la excepción original (ya que probablemente contiene pistas sobre por qué falló la limpieza). Permitir que un bloque IDisposable sepa por qué se ingresó, e incluir una versión extendida de IDisposable través de la cual un bloque en using podría hacer que dicha información esté disponible para el código de limpieza, permitiría resolver tales situaciones de manera limpia.