¿Cuáles y por qué prefieres las excepciones o los códigos de devolución?

92

Mi pregunta es qué prefieren la mayoría de los desarrolladores para el manejo de errores, excepciones o códigos de retorno de errores. Sea específico del idioma (o familia de idiomas) y por qué prefiere uno sobre el otro.

Te pregunto esto por curiosidad. Personalmente, prefiero los códigos de retorno de error, ya que son menos explosivos y no obligan al código de usuario a pagar la penalización de rendimiento de excepción si no lo desean.

actualización: ¡gracias por todas las respuestas! Debo decir que aunque no me gusta la imprevisibilidad del flujo de código con excepciones. La respuesta sobre el código de retorno (y los identificadores de su hermano mayor) agrega mucho ruido al código.

Robert Gould
fuente
1
Un problema del huevo o la gallina del mundo del software ... eternamente discutible. :)
Gishu
Lo siento, pero espero que tener una variedad de opiniones ayude a las personas (incluido yo mismo) a elegir adecuadamente.
Robert Gould

Respuestas:

107

Para algunos lenguajes (es decir, C ++), la fuga de recursos no debería ser una razón

C ++ está basado en RAII.

Si tiene un código que podría fallar, devolver o lanzar (es decir, el código más normal), entonces debe tener su puntero envuelto dentro de un puntero inteligente (asumiendo que tiene una muy buena razón para no tener su objeto creado en la pila).

Los códigos de retorno son más detallados

Son detallados y tienden a convertirse en algo como:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

Al final, tu código es una colección de instrucciones identificadas (vi este tipo de código en el código de producción).

Este código bien podría traducirse en:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

Que separan limpiamente el código y el procesamiento de errores, lo que puede ser algo bueno .

Los códigos de retorno son más frágiles

Si no es alguna advertencia oscura de un compilador (ver el comentario de "phjr"), pueden ignorarse fácilmente.

Con los ejemplos anteriores, suponga que alguien se olvida de manejar su posible error (esto sucede ...). El error se ignora cuando se "devuelve" y posiblemente explote más tarde (es decir, un puntero NULL). El mismo problema no ocurrirá con excepción.

El error no se ignorará. Sin embargo, a veces quieres que no explote ... Así que debes elegir con cuidado.

Los códigos de devolución a veces deben traducirse

Digamos que tenemos las siguientes funciones:

  • doSomething, que puede devolver un int llamado NOT_FOUND_ERROR
  • doSomethingElse, que puede devolver un bool "falso" (por error)
  • doSomethingElseAgain, que puede devolver un objeto Error (con las variables __LINE__, __FILE__ y la mitad de la pila.
  • do TryToDoSomethingWithAllThisMess que, bueno ... Use las funciones anteriores y devuelva un código de error del tipo ...

¿Cuál es el tipo de retorno de do TryToDoSomethingWithAllThisMess si falla una de sus funciones llamadas?

Los códigos de retorno no son una solución universal

Los operadores no pueden devolver un código de error. Los constructores de C ++ tampoco pueden.

Códigos de retorno significa que no puede encadenar expresiones

El corolario del punto anterior. ¿Y si quiero escribir?

CMyType o = add(a, multiply(b, c)) ;

No puedo, porque el valor de retorno ya se usa (y, a veces, no se puede cambiar). Entonces, el valor de retorno se convierte en el primer parámetro, enviado como referencia ... O no.

Se escriben excepciones

Puede enviar diferentes clases para cada tipo de excepción. Las excepciones de recursos (es decir, sin memoria) deben ser ligeras, pero cualquier otra cosa podría ser tan pesada como sea necesario (me gusta la excepción de Java que me da toda la pila).

Luego, cada captura puede especializarse.

Nunca uses catch (...) sin volver a lanzar

Por lo general, no debe ocultar un error. Si no vuelve a lanzar, como mínimo, registre el error en un archivo, abra un cuadro de mensaje, lo que sea ...

La excepción son ... NUKE

El problema con la excepción es que usarlos en exceso producirá un código lleno de intentos / capturas. Pero el problema está en otra parte: ¿Quién intenta / captura su código usando el contenedor STL? Aún así, esos contenedores pueden enviar una excepción.

Por supuesto, en C ++, nunca deje que una excepción salga de un destructor.

La excepción es ... sincrónica

Asegúrese de atraparlos antes de que saquen su hilo de rodillas o se propaguen dentro de su bucle de mensajes de Windows.

¿La solución podría ser mezclarlos?

Entonces supongo que la solución es lanzar cuando algo no debería suceder. Y cuando algo pueda suceder, utilice un código de retorno o un parámetro para permitir que el usuario reaccione.

Entonces, la única pregunta es "¿qué es algo que no debería suceder?"

Depende del contrato de tu función. Si la función acepta un puntero, pero especifica que el puntero no debe ser NULL, entonces está bien lanzar una excepción cuando el usuario envía un puntero NULL (la pregunta es, en C ++, cuándo el autor de la función no usó referencias en su lugar de punteros, pero ...)

Otra solución sería mostrar el error.

A veces, tu problema es que no quieres errores. Usar excepciones o códigos de retorno de error es genial, pero ... Quieres saberlo.

En mi trabajo, utilizamos una especie de "Afirmar". Dependiendo de los valores de un archivo de configuración, sin importar las opciones de compilación de depuración / liberación:

  • registrar el error
  • abre un cuadro de mensaje con un "Oye, tienes un problema"
  • abre un cuadro de mensaje con un "Oye, tienes un problema, quieres depurar"

Tanto en el desarrollo como en las pruebas, esto permite al usuario identificar el problema exactamente cuando se detecta y no después (cuando algún código se preocupa por el valor de retorno o dentro de una captura).

Es fácil de agregar al código heredado. Por ejemplo:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

conduce una especie de código similar a:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(Tengo macros similares que están activas solo en la depuración).

Tenga en cuenta que en producción, el archivo de configuración no existe, por lo que el cliente nunca ve el resultado de esta macro ... Pero es fácil activarlo cuando sea necesario.

Conclusión

Cuando codifica utilizando códigos de retorno, se está preparando para fallar y espera que su fortaleza de pruebas sea lo suficientemente segura.

Cuando codifica utilizando una excepción, sabe que su código puede fallar y, por lo general, coloca la captura de contrafuego en la posición estratégica elegida en su código. Pero por lo general, su código se trata más de "lo que debe hacer" que de "lo que temo que suceda".

Pero cuando codifica, debe usar la mejor herramienta a su disposición y, a veces, es "Nunca esconda un error y muéstrelo lo antes posible". La macro que hablé anteriormente sigue esta filosofía.

paercebal
fuente
3
Sí, PERO con respecto a su primer ejemplo, podría escribirse fácilmente como: if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
bobobobo
Por qué no ... Pero luego, todavía encuentro doSomething(); doSomethingElse(); ...mejor porque si necesito agregar if / while / etc. declaraciones para fines de ejecución normal, no quiero que se mezclen con if / while / etc. declaraciones agregadas para propósitos excepcionales ... Y como la regla real sobre el uso de excepciones es lanzar , no atrapar , las declaraciones try / catch generalmente no son invasivas.
paercebal
1
Su primer punto muestra cuál es el problema con las excepciones. Su flujo de control se vuelve extraño y está separado del problema real. Está reemplazando algunos niveles de identificación por una cascada de capturas. Usaría ambos códigos de retorno (o devolver objetos con mucha información) para posibles errores y excepciones para cosas que no se esperan .
Peter
2
@Peter Weber: No es extraño. Está separado del problema real porque no forma parte del flujo de ejecución normal . Es una ejecución excepcional . Y luego, nuevamente, el punto sobre la excepción es, en caso de error excepcional , lanzar con frecuencia y atrapar rara vez , si es que alguna vez. Por lo tanto, los bloques de captura rara vez aparecen en el código.
paercebal
El ejemplo en este tipo de debate es muy simplista. Normalmente, "doSomething ()" o "doSomethingElse ()" en realidad realizan algo, como cambiar el estado de algún objeto. El código de excepción no garantiza devolver el objeto al estado anterior y menos aún cuando la captura está muy lejos del lanzamiento ... Como ejemplo, imagina que doSomething se llama dos veces e incrementa un contador antes de lanzar. ¿Cómo sabe al detectar la excepción que debe disminuir una o dos veces? En general, escribir código seguro de excepción para cualquier cosa que no sea un ejemplo de juguete es muy difícil (¿imposible?).
xryl669
36

Yo uso ambos en realidad.

Utilizo códigos de retorno si se trata de un posible error conocido. Si es un escenario que sé que puede suceder, y que sucederá, entonces hay un código que se envía de vuelta.

Las excepciones se usan únicamente para cosas que NO espero.

Stephen Wrighton
fuente
+1 para una forma muy sencilla de hacerlo. Al menos es lo suficientemente corto para que pueda leerlo muy rápidamente. :-)
Ashish Gupta
1
"Las excepciones se utilizan únicamente para cosas que NO estoy esperando " . Si no las está esperando, ¿por qué las puede hacer o cómo puede usarlas?
anar khalilov
5
Solo porque no espero que suceda, no significa que no pueda ver cómo podría suceder. Espero que mi SQL Server se encienda y responda. Pero todavía codifico mis expectativas para poder fallar con elegancia si se produce un tiempo de inactividad inesperado.
Stephen Wrighton
¿No se clasificaría fácilmente un SQL Server que no responde en "posible error conocido"?
tkburbidge
21

De acuerdo con el Capítulo 7 titulado "Excepciones" en Pautas de diseño de marcos: convenciones, expresiones idiomáticas y patrones para bibliotecas .NET reutilizables , se dan numerosos fundamentos de por qué el uso de excepciones sobre los valores de retorno es necesario para marcos de OO como C #.

Quizás esta sea la razón más convincente (página 179):

"Las excepciones se integran bien con los lenguajes orientados a objetos. Los lenguajes orientados a objetos tienden a imponer restricciones en las firmas de miembros que no son impuestas por funciones en lenguajes que no son OO. Por ejemplo, en el caso de constructores, sobrecargas de operadores y propiedades, el desarrollador no tiene opción en el valor de retorno. Por esta razón, no es posible estandarizar el reporte de errores basado en el valor de retorno para marcos orientados a objetos. Un método de reporte de errores, como excepciones, que está fuera de la banda de la firma del método es la única opción " .

prisa
fuente
10

Mi preferencia (en C ++ y Python) es usar excepciones. Las facilidades proporcionadas por el lenguaje lo convierten en un proceso bien definido para generar, capturar y (si es necesario) volver a lanzar excepciones, lo que hace que el modelo sea fácil de ver y usar. Conceptualmente, es más limpio que los códigos de retorno, ya que las excepciones específicas pueden definirse por sus nombres y tener información adicional que las acompañe. Con un código de retorno, está limitado solo al valor de error (a menos que desee definir un objeto ReturnStatus o algo así).

A menos que el código que está escribiendo sea crítico en cuanto al tiempo, la sobrecarga asociada con el desenrollado de la pila no es lo suficientemente significativa como para preocuparse.

Jason Etheridge
fuente
2
Recuerde que el uso de excepciones dificulta el análisis de programas.
Paweł Hajdan
7

Las excepciones solo deben devolverse cuando sucede algo que no esperaba.

El otro punto de las excepciones, históricamente, es que los códigos de retorno son intrínsecamente propietarios, a veces se puede devolver un 0 de una función C para indicar éxito, a veces -1, o cualquiera de ellos para un error con 1 para un éxito. Incluso cuando se enumeran, las enumeraciones pueden ser ambiguas.

Las excepciones también pueden proporcionar mucha más información, y específicamente deletrear bien 'Algo salió mal, esto es lo que, un seguimiento de pila y alguna información de apoyo para el contexto'

Dicho esto, un código de retorno bien enumerado puede ser útil para un conjunto conocido de resultados, un simple 'aquí hay n resultados de la función, y simplemente se ejecutó de esta manera'

johnc
fuente
6

En Java, uso (en el siguiente orden):

  1. Diseño por contrato (garantizar que se cumplan las condiciones previas antes de intentar cualquier cosa que pueda fallar). Esto detecta la mayoría de las cosas y devuelvo un código de error para esto.

  2. Devolver códigos de error mientras se procesa el trabajo (y realizar una reversión si es necesario).

  3. Excepciones, pero se usan solo para cosas inesperadas.

paxdiablo
fuente
1
¿No sería un poco más correcto usar afirmaciones para contratos? Si el contrato se rompe, no hay nada que pueda salvarlo.
Paweł Hajdan
@ PawełHajdan, las afirmaciones están deshabilitadas de forma predeterminada, creo. Esto tiene el mismo problema que las assertcosas de C, ya que no detectará problemas en el código de producción a menos que ejecute con aserciones todo el tiempo. Tiendo a ver las afirmaciones como una forma de detectar problemas durante el desarrollo, pero solo para cosas que afirmarán o no afirmarán consistentemente (como cosas con constantes, no cosas con variables o cualquier otra cosa que pueda cambiar en tiempo de ejecución).
paxdiablo
Y doce años para responder a tu pregunta. Debería tener una mesa de ayuda :-)
paxdiablo
6

Los códigos de retorno fallan casi siempre en la prueba del " pozo del éxito ".

  • Es demasiado fácil olvidarse de verificar un código de retorno y luego tener un error de pista falsa más adelante.
  • Los códigos de retorno no tienen la gran información de depuración como pila de llamadas, excepciones internas.
  • Los códigos de retorno no se propagan, lo que, junto con el punto anterior, tiende a generar un registro de diagnóstico excesivo e interrelacionado en lugar de registrarlo en un lugar centralizado (controladores de excepciones a nivel de aplicación y de subprocesos).
  • Los códigos de retorno tienden a generar código desordenado en forma de bloques 'if' anidados
  • El tiempo del desarrollador dedicado a depurar un problema desconocido que de otro modo habría sido una excepción obvia (pozo de éxito) ES caro.
  • Si el equipo detrás de C # no tenía la intención de que las excepciones gobernaran el flujo de control, las excepciones no se escribirían, no habría un filtro "when" en las declaraciones catch y no habría necesidad de la declaración 'throw' sin parámetros .

En cuanto al rendimiento:

  • Las excepciones pueden ser computacionalmente costosas RELATIVAS a no lanzar nada, pero se llaman EXCEPCIONES por una razón. Las comparaciones de velocidad siempre logran asumir una tasa de excepción del 100%, lo que nunca debería ser el caso. Incluso si una excepción es 100 veces más lenta, ¿cuánto importa eso si solo ocurre el 1% del tiempo?
  • A menos que estemos hablando de aritmética de punto flotante para aplicaciones gráficas o algo similar, los ciclos de CPU son baratos en comparación con el tiempo del desarrollador.
  • El costo desde la perspectiva del tiempo conlleva el mismo argumento. En relación con las consultas a la base de datos o las llamadas a servicios web o las cargas de archivos, el tiempo normal de aplicación empequeñecerá el tiempo de excepción. Las excepciones fueron casi sub-MICROsegundo en 2006
    • Desafío a cualquiera que trabaje en .net a configurar su depurador para que se interrumpa en todas las excepciones y deshabilite solo mi código y vea cuántas excepciones ya están sucediendo y que ni siquiera conoce.
b_levitt
fuente
4

Un gran consejo que recibí de The Pragmatic Programmer fue algo así como "su programa debería poder realizar todas sus funciones principales sin utilizar excepciones".

Smashery
fuente
4
Lo estás malinterpretando. Lo que querían decir era "si su programa arroja excepciones en sus flujos normales, está mal". En otras palabras, "solo use excepciones para cosas excepcionales".
Paweł Hajdan
4

Escribí una publicación de blog sobre esto hace un tiempo.

La sobrecarga de rendimiento de lanzar una excepción no debería influir en su decisión. Si lo está haciendo bien, después de todo, una excepción es excepcional .

Thomas
fuente
La publicación de blog vinculada tiene más que ver con mirar antes de saltar (cheques) versus más fácil pedir perdón que permiso (excepciones o códigos de retorno). Respondí allí con mis pensamientos sobre ese tema (pista: TOCTTOU). Pero esta pregunta se trata de un tema diferente, a saber, en qué condiciones utilizar el mecanismo de excepción de un lenguaje en lugar de devolver un valor con propiedades especiales.
Damian Yerrick
Estoy completamente de acuerdo. Parece que he aprendido una o dos cosas en los últimos nueve años;)
Thomas
4

No me gustan los códigos de retorno porque hacen que el siguiente patrón se propague a lo largo de su código

CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
  // bail out
  goto FunctionExit;
}

Pronto, una llamada a método que consta de 4 llamadas a funciones se llena con 12 líneas de manejo de errores. Algunas de las cuales nunca sucederán. Abundan los casos y los interruptores.

Las excepciones son más limpias si las usa bien ... para señalar eventos excepcionales ... después de los cuales la ruta de ejecución no puede continuar. A menudo son más descriptivos e informativos que los códigos de error.

Si tiene varios estados después de una llamada a un método que deben manejarse de manera diferente (y no son casos excepcionales), use códigos de error o params. Aunque Personaly, he encontrado que esto es raro ...

He buscado un poco sobre el contraargumento de la "penalización de rendimiento" ... más en el mundo C ++ / COM, pero en los lenguajes más nuevos, creo que la diferencia no es tanta. En cualquier caso, cuando algo explota, las preocupaciones sobre el rendimiento quedan relegadas a un segundo plano :)

Gishu
fuente
3

Con cualquier compilador decente o entorno de ejecución, las excepciones no conllevan una penalización significativa. Es más o menos como una declaración GOTO que salta al controlador de excepciones. Además, tener excepciones detectadas por un entorno de ejecución (como la JVM) ayuda a aislar y corregir un error mucho más fácilmente. Tomaré una NullPointerException en Java sobre una segfault en C cualquier día.

Kyle Cronin
fuente
2
Las excepciones son extremadamente caras. Tienen que recorrer la pila para encontrar posibles controladores de excepciones. Este paseo en pila no es barato. Si se crea un seguimiento de pila, es incluso más caro, porque entonces se debe analizar toda la pila .
Derek Park
Me sorprende que los compiladores no puedan determinar dónde se detectará la excepción al menos parte del tiempo. Además, el hecho de que una excepción altere el flujo del código hace que sea más fácil identificar exactamente dónde ocurre un error, en mi opinión, compensa una penalización en el rendimiento.
Kyle Cronin
Las pilas de llamadas pueden volverse extremadamente complejas en tiempo de ejecución y los compiladores generalmente no realizan ese tipo de análisis. Incluso si lo hicieran, todavía tendrías que caminar por la pila para obtener un rastro. También tendría que desenrollar la pila para lidiar con finallybloques y destructores para objetos asignados a la pila.
Derek Park
Sin embargo, estoy de acuerdo en que los beneficios de depuración de las excepciones a menudo compensan los costos de rendimiento.
Derek Park
1
Derak Park, las excepciones son caras cuando ocurren. Ésta es la razón por la que no deben usarse en exceso. Pero cuando no suceden, no cuestan prácticamente nada.
paercebal
3

Tengo un conjunto simple de reglas:

1) Use códigos de retorno para las cosas a las que espera que reaccione su interlocutor inmediato.

2) Utilice excepciones para errores que tengan un alcance más amplio y que se pueda esperar razonablemente que sean manejados por algo que esté a muchos niveles por encima de la persona que llama, de modo que el conocimiento del error no tenga que filtrarse a través de muchas capas, haciendo que el código sea más complejo.

En Java, solo usé excepciones no verificadas, las excepciones verificadas terminan siendo solo otra forma de código de retorno y, en mi experiencia, la dualidad de lo que podría ser "devuelto" por una llamada a un método fue generalmente más un impedimento que una ayuda.

Kendall Helmstetter Gelner
fuente
3

Utilizo Excepciones en Python tanto en circunstancias excepcionales como en circunstancias no excepcionales.

A menudo es bueno poder usar una excepción para indicar que "no se pudo realizar la solicitud", en lugar de devolver un valor de error. Significa que usted / siempre / sabe que el valor de retorno es del tipo correcto, en lugar de arbitrariamente None o NotFoundSingleton o algo así. Aquí hay un buen ejemplo de dónde prefiero usar un controlador de excepciones en lugar de un condicional en el valor de retorno.

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

El efecto secundario es que cuando se ejecuta un datastore.fetch (obj_id), nunca tienes que verificar si su valor de retorno es Ninguno, obtienes ese error inmediatamente de forma gratuita. Esto es contrario al argumento, "su programa debería poder realizar toda su funcionalidad principal sin usar excepciones en absoluto".

Aquí hay otro ejemplo de dónde las excepciones son 'excepcionalmente' útiles, para escribir código para tratar con el sistema de archivos que no está sujeto a condiciones de carrera.

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

Una llamada al sistema en lugar de dos, sin condición de carrera. Este es un mal ejemplo porque obviamente esto fallará con un OSError en más circunstancias de las que el directorio no existe, pero es una solución 'suficientemente buena' para muchas situaciones estrictamente controladas.

Jerub
fuente
El segundo ejemplo es engañoso. La forma supuestamente incorrecta es incorrecta porque el código os.path.rmdir está diseñado para lanzar una excepción. La implementación correcta en el flujo del código de retorno sería 'if rmdir (...) == FAILED: pass'
MaR
3

Creo que los códigos de retorno aumentan el ruido del código. Por ejemplo, siempre odié el aspecto del código COM / ATL debido a los códigos de retorno. Tenía que haber una verificación HRESULT para cada línea de código. Considero que el código de retorno de error es una de las malas decisiones tomadas por los arquitectos de COM. Esto dificulta la agrupación lógica del código, por lo que la revisión del código se vuelve difícil.

No estoy seguro de la comparación de rendimiento cuando hay una verificación explícita del código de retorno en cada línea.

rpattabi
fuente
1
COM Wad diseñado para ser utilizado por lenguajes que no admiten excepciones.
Kevin
Ese es un buen punto. Tiene sentido lidiar con códigos de error para lenguajes de secuencias de comandos. Al menos VB6 oculta bien los detalles del código de error al encapsularlos en el objeto Err, lo que de alguna manera ayuda a limpiar el código.
rpattabi
No estoy de acuerdo: VB6 solo registra el último error. Combinado con el infame "en caso de error, reanude el próximo", se perderá la fuente de su problema por completo, cuando lo vea. Tenga en cuenta que esta es la base del manejo de errores en Win32 APII (consulte la función GetLastError)
paercebal
2

Prefiero usar excepciones para el manejo de errores y devolver valores (o parámetros) como el resultado normal de una función. Esto proporciona un esquema de manejo de errores fácil y consistente y, si se hace correctamente, hace que el código se vea mucho más limpio.

Trento
fuente
2

Una de las grandes diferencias es que las excepciones lo obligan a manejar un error, mientras que los códigos de retorno de error pueden quedar sin marcar.

Los códigos de retorno de error, si se usan mucho, también pueden causar un código muy feo con muchas pruebas if similares a esta forma:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

Personalmente, prefiero usar excepciones para errores que DEBEN o DEBEN ser atendidos por el código de llamada, y solo uso códigos de error para "fallas esperadas" donde devolver algo es realmente válido y posible.

Daniel Bruce
fuente
Al menos en C / C ++ y gcc puedes darle a una función un atributo que generará una advertencia cuando se ignore su valor de retorno.
Paweł Hajdan,
phjr: Aunque no estoy de acuerdo con el patrón de "código de error de retorno", su comentario tal vez debería convertirse en una respuesta completa. Lo encuentro bastante interesante. Por lo menos, me dio una información útil.
paercebal
2

Hay muchas razones para preferir las excepciones al código de retorno:

  • Por lo general, para facilitar la lectura, las personas intentan minimizar el número de declaraciones de devolución en un método. Al hacerlo, las excepciones evitan hacer un trabajo adicional mientras se encuentra en un estado incorrecto y, por lo tanto, evitan dañar potencialmente más datos.
  • Las excepciones son generalmente más detalladas y más fácilmente extensibles que el valor de retorno. Suponga que un método devuelve un número natural y que usa números negativos como código de retorno cuando ocurre un error, si el alcance de su método cambia y ahora devuelve enteros, tendrá que modificar todas las llamadas al método en lugar de simplemente ajustar un poco La excepción.
  • Excepciones permite separar más fácilmente el manejo de errores del comportamiento normal. Permiten garantizar que algunas operaciones se realicen de alguna manera como una operación atómica.
artilugio
fuente
2

Las excepciones no son para el manejo de errores, en mi opinión. Las excepciones son solo eso; eventos excepcionales que no esperabas. Use con precaución digo.

Los códigos de error pueden estar bien, pero devolver 404 o 200 de un método es malo, en mi opinión. Use enums (.Net) en su lugar, eso hace que el código sea más legible y más fácil de usar para otros desarrolladores. Además, no es necesario mantener una tabla sobre números y descripciones.

También; el patrón de intentar-atrapar-finalmente es un anti-patrón en mi libro. Intentar-finalmente puede ser bueno, probar-atrapar también puede ser bueno, pero probar-atrapar-finalmente nunca es bueno. try-finalmente a menudo puede ser reemplazado por una declaración "using" (patrón IDispose), que es mejor en mi opinión. Y Try-catch donde realmente detecta una excepción que puede manejar es bueno, o si hace esto:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Entonces, siempre que deje que la excepción continúe burbujeando, está bien. Otro ejemplo es este:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

Aquí realmente manejo la excepción; lo que hago fuera del try-catch cuando falla el método de actualización es otra historia, pero creo que mi punto ya está claro. :)

¿Por qué entonces try-catch-finalmente es un anti-patrón? Este es el por qué:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

¿Qué sucede si el objeto db ya se ha cerrado? ¡Se lanza una nueva excepción y debe manejarse! Esta es mejor:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

O, si el objeto db no implementa IDisposable, haga esto:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

¡De todos modos, esos son mis 2 centavos! :)

noocito
fuente
Será extraño si un objeto tiene .Close () pero no tiene .Dispose ()
abatishchev
Solo estoy usando Close () como ejemplo. Siéntete libre de pensar en ello como otra cosa. Como digo; el patrón de uso debe usarse (¡doh!) si está disponible. Esto, por supuesto, implica que la clase implementa IDisposable y, como tal, podría llamar a Dispose.
noocito
1

Solo uso excepciones, no códigos de retorno. Estoy hablando de Java aquí.

La regla general que sigo es que si tengo un método llamado doFoo(), se deduce que si no "hace foo", por así decirlo, entonces ha sucedido algo excepcional y se debe lanzar una excepción.

SCdF
fuente
1

Una cosa que temo sobre las excepciones es que lanzar una excepción arruinará el flujo de código. Por ejemplo si lo haces

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

O peor aún, ¿qué pasa si eliminé algo que no debería haber hecho, pero me lanzaron a atrapar antes de hacer el resto de la limpieza? Lanzar puso mucho peso en el pobre usuario en mi humilde opinión.

Robert Gould
fuente
Para esto es la instrucción <code> finalmente </code>. Pero, por desgracia, no está en el estándar C ++ ...
Thomas
En C ++ debes ceñirte a la regla práctica "Adquirir recursos en construcotor y liberarlos en destrucotor. Para este caso particular, auto_ptr funcionará perfectamente.
Serge
Thomas, estás equivocado. C ++ finalmente no lo ha hecho porque no lo necesita. En su lugar, tiene RAII. La solución de Serge es una solución que usa RAII.
paercebal
Robert, usa la solución de Serge y encontrarás que tu problema desaparece. Ahora, si escribe más intentos / capturas que lanzamientos, entonces (al juzgar su comentario) quizás tenga un problema en su código. Por supuesto, usar catch (...) sin un re-lanzamiento suele ser malo, ya que oculta el error para ignorarlo mejor.
paercebal
1

Mi regla general en el argumento de excepción frente a código de retorno:

  • Use códigos de error cuando necesite localización / internacionalización: en .NET, puede usar estos códigos de error para hacer referencia a un archivo de recursos que luego mostrará el error en el idioma apropiado. De lo contrario, use excepciones
  • Utilice excepciones solo para errores que sean realmente excepcionales. Si es algo que sucede con bastante frecuencia, use un código de error booleano o enumerado.
Jon Limjap
fuente
No hay ninguna razón por la que no pueda utilizar una excepción al hacer l10n / i18n. Las excepciones también pueden contener información localizada.
gizmo
1

No encuentro que los códigos de retorno sean menos feos que las excepciones. Con la excepción, tiene el try{} catch() {} finally {}dónde como con los códigos de retorno que tiene if(){}. Solía ​​temer las excepciones por las razones dadas en el post; no sabes si el puntero necesita ser borrado, ¿qué tienes? Pero creo que tienes los mismos problemas cuando se trata de códigos de retorno. No conoce el estado de los parámetros a menos que conozca algunos detalles sobre la función / método en cuestión.

Independientemente, debe manejar el error si es posible. Puede permitir que una excepción se propague al nivel superior con la misma facilidad que ignorar un código de retorno y dejar que el programa siga por defecto.

Me gusta la idea de devolver un valor (¿enumeración?) Para los resultados y una excepción para un caso excepcional.

Jonathan Adelson
fuente
0

Para un lenguaje como Java, iría con Exception porque el compilador da un error de tiempo de compilación si no se manejan las excepciones. Esto obliga a la función de llamada a manejar / lanzar las excepciones.

Para Python, estoy más en conflicto. No hay un compilador, por lo que es posible que la persona que llama no maneje la excepción generada por la función que genera excepciones en tiempo de ejecución. Si usa códigos de retorno, es posible que tenga un comportamiento inesperado si no se maneja correctamente y si usa excepciones, es posible que obtenga excepciones en tiempo de ejecución.

2ank3th
fuente
0

Generalmente prefiero los códigos de retorno porque permiten que la persona que llama decida si la falla es excepcional .

Este enfoque es típico en el lenguaje Elixir.

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

La gente mencionó que los códigos de retorno pueden hacer que tenga muchas ifdeclaraciones anidadas , pero eso se puede manejar con una mejor sintaxis. En Elixir, la withdeclaración nos permite separar fácilmente una serie de valor de retorno de ruta feliz de cualquier falla.

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

Elixir todavía tiene funciones que generan excepciones. Volviendo a mi primer ejemplo, podría hacer cualquiera de estos para generar una excepción si el archivo no se puede escribir.

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

Si yo, como persona que llama, sé que quiero generar un error si falla la escritura, puedo elegir llamar en File.write!lugar de File.write. O puedo elegir llamar File.writey manejar cada una de las posibles razones de falla de manera diferente.

Por supuesto, siempre es posible hacer rescueuna excepción si queremos. Pero en comparación con el manejo de un valor de retorno informativo, me parece incómodo. Si sé que una llamada de función puede fallar o incluso debería fallar, su falla no es un caso excepcional.

Nathan Long
fuente