¿Por qué atrapar y volver a lanzar una excepción en C #?

Estoy mirando el artículo C # – Objeto de transferencia de datos en DTO serializables.

El artículo incluye esta pieza de código:

public static string SerializeDTO(DTO dto) { try { XmlSerializer xmlSer = new XmlSerializer(dto.GetType()); StringWriter sWriter = new StringWriter(); xmlSer.Serialize(sWriter, dto); return sWriter.ToString(); } catch(Exception ex) { throw ex; } } 

El rest del artículo parece sensato y razonable (para un novato), pero ese try-catch-throw arroja una excepción WtfException … ¿No es exactamente equivalente a no manejar excepciones en absoluto?

Es decir:

 public static string SerializeDTO(DTO dto) { XmlSerializer xmlSer = new XmlSerializer(dto.GetType()); StringWriter sWriter = new StringWriter(); xmlSer.Serialize(sWriter, dto); return sWriter.ToString(); } 

¿O me estoy perdiendo algo fundamental sobre el manejo de errores en C #? Es prácticamente lo mismo que Java (menos excepciones marcadas), ¿no? … Es decir, ambos refinaron C ++.

La pregunta sobre el desbordamiento de la stack ¿ La diferencia entre volver a tirar la captura sin parámetros y no hacer nada? parece apoyar mi opinión de que try-catch-throw es-un no-op.


EDITAR:

Solo para resumir para cualquiera que encuentre este hilo en el futuro …

NO HAGA

 try { // Do stuff that might throw an exception } catch (Exception e) { throw e; // This destroys the strack trace information! } 

¡La información de rastreo de la stack puede ser crucial para identificar la causa raíz del problema!

HACER

 try { // Do stuff that might throw an exception } catch (SqlException e) { // Log it if (e.ErrorCode != NO_ROW_ERROR) { // filter out NoDataFound. // Do special cleanup, like maybe closing the "dirty" database connection. throw; // This preserves the stack trace } } catch (IOException e) { // Log it throw; } catch (Exception e) { // Log it throw new DAOException("Excrement occurred", e); // wrapped & chained exceptions (just like java). } finally { // Normal clean goes here (like closing open files). } 

Capture las excepciones más específicas antes que las menos específicas (como Java).


Referencias

  • MSDN – Manejo de excepciones
  • MSDN – try-catch (referencia de C #)

Primero; la forma en que lo hace el código en el artículo es malo. throw ex reiniciará la stack de llamadas en la excepción al punto donde se encuentra esta sentencia throw; perder la información sobre dónde se creó realmente la excepción.

En segundo lugar, si solo capturas y vuelves a lanzar de esa forma, no veo valor agregado, el ejemplo de código anterior sería igual de bueno (o, dado el throw ex , un poco, incluso mejor) sin el try-catch.

Sin embargo, hay casos en los que es posible que desee atrapar y volver a lanzar una excepción. El registro podría ser uno de ellos:

 try { // code that may throw exceptions } catch(Exception ex) { // add error logging here throw; } 

No hagas esto,

 try { ... } catch(Exception ex) { throw ex; } 

Perderás la información de seguimiento de la stack …

O bien,

 try { ... } catch { throw; } 

O

 try { ... } catch (Exception ex) { throw new Exception("My Custom Error Message", ex); } 

Una de las razones por las que es posible que desee volver a lanzar es si está manejando diferentes excepciones, por ejemplo

 try { ... } catch(SQLException sex) { //Do Custom Logging //Don't throw exception - swallow it here } catch(OtherException oex) { //Do something else throw new WrappedException("Other Exception occured"); } catch { System.Diagnostics.Debug.WriteLine("Eeep! an error, not to worry, will be handled higher up the call stack"); throw; //Chuck everything else back up the stack } 

C # (antes de C # 6) no es compatible con CIL “excepciones filtradas”, lo que VB hace, por lo que en C # 1-5 una razón para volver a lanzar una excepción es que no tiene suficiente información en el momento de la captura () para determinar si realmente desea atrapar la excepción.

Por ejemplo, en VB puedes hacer

 Try .. Catch Ex As MyException When Ex.ErrorCode = 123 .. End Try 

… que no manejaría MyExceptions con diferentes valores de ErrorCode. En C # antes de v6, tendría que atrapar y volver a lanzar MyException si el ErrorCode no era 123:

 try { ... } catch(MyException ex) { if (ex.ErrorCode != 123) throw; ... } 

Desde C # 6.0 puedes filtrar al igual que con VB:

 try { // Do stuff } catch (Exception e) when (e.ErrorCode == 123456) // filter { // Handle, other exceptions will be left alone and bubble up } 

Mi razón principal para tener código como:

 try { //Some code } catch (Exception e) { throw; } 

es así que puedo tener un punto de interrupción en la captura, que tiene un objeto de excepción instanciado. Hago esto mucho mientras desarrollo / depuración. Por supuesto, el comstackdor me da una advertencia sobre todas las e no utilizadas, e idealmente deberían eliminarse antes de una versión de lanzamiento.

Sin embargo, son agradables durante la depuración.

Una razón válida para volver a lanzar excepciones puede ser que desee agregar información a la excepción, o tal vez ajustar la excepción original en una de sus propias creaciones:

 public static string SerializeDTO(DTO dto) { try { XmlSerializer xmlSer = new XmlSerializer(dto.GetType()); StringWriter sWriter = new StringWriter(); xmlSer.Serialize(sWriter, dto); return sWriter.ToString(); } catch(Exception ex) { string message = String.Format("Something went wrong serializing DTO {0}", DTO); throw new MyLibraryException(message, ex); } } 

¿No es esto exactamente equivalente a no manejar excepciones en absoluto?

No exactamente, no es lo mismo. Restablece la stacktrace de la excepción. Aunque estoy de acuerdo con que esto probablemente sea un error, y por lo tanto un ejemplo de código incorrecto.

No desea lanzar ex, ya que perderá la stack de llamadas. Consulte Manejo de excepciones (MSDN).

Y sí, el try … catch no está haciendo nada útil (aparte de perder la stack de llamadas, por lo que en realidad es peor) a menos que por alguna razón no quieras exponer esta información).

Un punto que las personas no han mencionado es que, si bien los lenguajes .NET realmente no hacen una distinción adecuada, la cuestión de si uno debe tomar medidas cuando se produce una excepción y si se resolverá o no, son en realidad preguntas distintas. Hay muchos casos en los que uno debe tomar medidas basadas en excepciones que uno no tiene la esperanza de resolver, y hay algunos casos en que todo lo que es necesario para “resolver” una excepción es desenrollar la stack hasta cierto punto; no se requieren más acciones. .

Debido a la creencia común de que uno solo debe “atrapar” cosas que uno puede “manejar”, una gran cantidad de código que debe tomar medidas cuando ocurren excepciones, no lo hace. Por ejemplo, una gran cantidad de código adquirirá un locking, colocará el objeto guardado “temporalmente” en un estado que viole sus invariantes, luego lo colocará en un estado legítimo y luego liberará el locking antes de que nadie más pueda ver el objeto. Si se produce una excepción mientras el objeto se encuentra en un estado peligrosamente inválido, la práctica común es liberar el locking con el objeto que aún se encuentra en ese estado. Un patrón mucho mejor sería tener una excepción que ocurra mientras el objeto está en una condición “peligrosa” invalidar expresamente el locking, por lo que cualquier bash futuro de adquirirlo fallará inmediatamente. El uso constante de dicho patrón mejoraría en gran medida la seguridad del llamado manejo de excepciones de “Pokemon”, que en mi humilde opinión recibe una mala reputación principalmente debido a un código que permite que las excepciones se filtren sin tomar antes las medidas apropiadas.

En la mayoría de los lenguajes .NET, la única forma de que el código actúe en función de una excepción es catch (aunque sepa que no va a resolver la excepción), realice la acción en cuestión y luego vuelva a throw ). Otro posible enfoque si el código no se preocupa por la excepción que se lanza es usar una bandera ok con un bloque try/finally ; establezca la bandera ok en false antes del bloque, y en true antes de que salga el bloque, y antes de cualquier return que esté dentro del bloque. Luego, dentro de finally , asum que si no está establecido, debe haber ocurrido una excepción. Tal enfoque es semánticamente mejor que un catch / throw , pero es feo y es menos sostenible de lo que debería ser.

Una posible razón para catch-throw es desactivar cualquier filtro de excepción que se encuentre más allá de la stack para que no se filtre ( enlace antiguo aleatorio ). Pero, por supuesto, si esa era la intención, habría un comentario allí diciendo eso.

Depende de lo que esté haciendo en el bloque catch, y si quiere pasar el error al código de llamada o no.

Puede decir Catch io.FileNotFoundExeption ex y luego usar una ruta de archivo alternativa o algo así, pero aun así arrojar el error.

También hacer Throw lugar de Throw Ex permite mantener el seguimiento completo de la stack. Throw ex reinicia el seguimiento de la stack de la statement throw (espero que tenga sentido).

Esto puede ser útil cuando su progtwigción funciona para una biblioteca o dll.

Esta estructura de replanteo se puede usar para reiniciar a propósito la stack de llamadas de modo que en lugar de ver la excepción lanzada desde una función individual dentro de la función, obtenga la excepción de la función misma.

Creo que esto solo se usa para que las excepciones lanzadas sean más limpias y no entren en las “raíces” de la biblioteca.

En el ejemplo del código que ha publicado, de hecho, no tiene sentido capturar la excepción, ya que no se hace nada en la captura, solo se vuelve a lanzar, de hecho, hace más daño que bien, ya que se pierde la stack de llamadas. .

Sin embargo, usted atraparía una excepción para hacer algo de lógica (por ejemplo, cerrar la conexión sql del locking de archivos, o simplemente un poco de registro) en el caso de una excepción, devolverlo al código de llamada para tratar. Esto sería más común en una capa de negocios que en el código de front-end, ya que es posible que desee que el codificador que implementa su capa de negocios maneje la excepción.

Para volver a iterar a través del NO hay ningún punto en atrapar la excepción en el ejemplo que publicó. ¡NO lo hagas así!

Lo sentimos, pero muchos ejemplos como “diseño mejorado” todavía huelen de forma horrible o pueden ser extremadamente engañosos. Tener try {} catch {log; throw} es completamente inútil. El registro de excepciones debe hacerse en un lugar central dentro de la aplicación. las excepciones salpican la stack de fichas de todos modos, ¿por qué no registrarlas en algún lugar cerca de los bordes del sistema?

Se debe tener precaución cuando serialice su contexto (es decir, DTO en un ejemplo dado) solo en el mensaje de registro. Puede contener fácilmente información confidencial que quizás no quiera llegar a las manos de todas las personas que pueden acceder a los archivos de registro. Y si no agrega ninguna información nueva a la excepción, realmente no veo el punto de envolvimiento de excepciones. La buena vieja Java tiene algún punto para eso, requiere que la persona que llama sepa qué tipo de excepciones debería esperar y luego llamar al código. Como no tiene esto en .NET, el ajuste no sirve para nada en al menos el 80% de los casos que he visto.

Si bien muchas de las otras respuestas brindan buenos ejemplos de por qué es posible que desee detectar una nueva excepción, nadie parece haber mencionado un escenario “finalmente”.

Un ejemplo de esto es cuando tiene un método en el que establece el cursor (por ejemplo, en un cursor de espera), el método tiene varios puntos de salida (por ejemplo, if () return;) y desea asegurarse de que el cursor se reinicie en el fin del método.

Para hacer esto puedes envolver todo el código en un try / catch / finally. Finalmente, coloque el cursor nuevamente en el cursor derecho. Para que no entierre ninguna excepción válida, vuelva a tirarla en la trampa.

 try { Cursor.Current = Cursors.WaitCursor; // Test something if (testResult) return; // Do something else } catch { throw; } finally { Cursor.Current = Cursors.Default; } 

Además de lo que han dicho los demás, vea mi respuesta a una pregunta relacionada que muestra que atrapar y volver a lanzar no es un no-op (está en VB, pero parte del código podría invocarse C # desde VB).

La mayoría de las respuestas hablan del escenario catch-log-rethrow.

En lugar de escribirlo en su código, considere usar AOP, en particular Postsharp.Diagnostic.Toolkit con OnExceptionOptions IncludeParameterValue e IncludeThisArgument