Excepciones, códigos de error y sindicatos discriminados.

80

Recientemente comencé un trabajo de programación en C #, pero tengo bastante experiencia en Haskell.

Pero entiendo que C # es un lenguaje orientado a objetos, no quiero forzar una clavija redonda en un agujero cuadrado.

Leí el artículo Lanzamiento de excepciones de Microsoft que dice:

NO devuelva códigos de error.

Pero al estar acostumbrado a Haskell, he estado usando el tipo de datos C # OneOf, devolviendo el resultado como el valor "correcto" o el error (a menudo una enumeración) como el valor "izquierdo".

Esto es muy parecido a la convención de EitherHaskell.

Para mí, esto parece más seguro que las excepciones. En C #, ignorar las excepciones no produce un error de compilación, y si no se detectan, simplemente aparecen y bloquean su programa. Esto quizás sea mejor que ignorar un código de error y producir un comportamiento indefinido, pero bloquear el software de su cliente todavía no es algo bueno, especialmente cuando realiza muchas otras tareas comerciales importantes en segundo plano.

Con OneOf, uno tiene que ser bastante explícito acerca de desempacarlo y manejar el valor de retorno y los códigos de error. Y si uno no sabe cómo manejarlo en esa etapa en la pila de llamadas, debe colocarse en el valor de retorno de la función actual, para que las personas que llaman sepan que podría producirse un error.

Pero este no parece ser el enfoque que sugiere Microsoft.

¿El uso OneOfde excepciones en lugar de excepciones para manejar excepciones "ordinarias" (como Archivo no encontrado, etc.) es un enfoque razonable o es una práctica terrible?


Vale la pena señalar que he escuchado que las excepciones como flujo de control se consideran un antipatrón serio , por lo que si la "excepción" es algo que normalmente manejaría sin finalizar el programa, ¿no es ese "flujo de control" de alguna manera? Entiendo que hay un área gris aquí.

Tenga en cuenta que no estoy usando OneOfpara cosas como "Sin memoria", las condiciones de las que no espero recuperar aún arrojarán excepciones. Pero creo que los problemas bastante razonables, como la entrada del usuario que no analiza son esencialmente "flujo de control" y probablemente no deberían arrojar excepciones.


Pensamientos posteriores

De esta discusión lo que me llevo actualmente es lo siguiente:

  1. Si espera que la persona que llama inmediatamente catchmaneje la excepción la mayor parte del tiempo y continúe su trabajo, tal vez a través de otra ruta, probablemente debería ser parte del tipo de retorno. Optionalo OneOfpuede ser útil aquí.
  2. Si espera que la persona que llama de inmediato no detecte la excepción la mayor parte del tiempo, lance una excepción para evitar la tontería de pasarla manualmente por la pila.
  3. Si no está seguro de lo que va a hacer la persona que llama inmediatamente, tal vez proporcione ambos, como Parsey TryParse.
Clinton
fuente
13
Use F # Result;-)
Astrinus
8
Algo fuera de tema, pero tenga en cuenta que el enfoque que está viendo tiene la vulnerabilidad general de que no hay forma de garantizar que el desarrollador maneje el error correctamente. Claro, el sistema de mecanografía puede actuar como un recordatorio, pero pierde el valor predeterminado de volar el programa por no manejar un error, lo que hace que sea más probable que termine en un estado inesperado. Sin embargo, si el recordatorio vale la pena perder ese incumplimiento es un debate abierto. Me inclino a pensar que es mejor escribir todo el código asumiendo que una excepción lo terminará en algún momento; entonces el recordatorio tiene menos valor.
jpmc26
99
Artículo relacionado: Eric Lippert sobre los cuatro "cubos" de excepciones . El ejemplo que da sobre las concepciones exógenas muestra que hay escenarios en los que solo hay que lidiar con excepciones, es decir FileNotFoundException.
Søren D. Ptæus
77
Puedo estar solo en esto, pero creo que chocar es bueno . No hay mejor evidencia de que haya un problema en su software que un registro de fallas con el seguimiento adecuado. Si tienes un equipo que puede corregir e implementar un parche en 30 minutos o menos, dejar que aparezcan esos amigos feos puede no ser algo malo.
T. Sar - Restablece a Monica el
99
¿Cómo es que ninguna de las respuestas aclara que la Eithermónada no es un código de error , y tampoco lo es OneOf? Son fundamentalmente diferentes y, en consecuencia, la pregunta parece estar basada en un malentendido. (Aunque en forma modificada, sigue siendo una pregunta válida).
Konrad Rudolph

Respuestas:

131

pero bloquear el software de su cliente todavía no es algo bueno

Ciertamente es algo bueno.

Desea cualquier cosa que deje el sistema en un estado indefinido para detener el sistema porque un sistema indefinido puede hacer cosas desagradables como datos corruptos, formatear el disco duro y enviar correos electrónicos amenazadores al presidente. Si no puede recuperarse y volver a poner el sistema en un estado definido, lo más responsable es que se bloquee. Es exactamente por eso que construimos sistemas que se bloquean en lugar de desgarrarse silenciosamente. Ahora, claro, todos queremos un sistema estable que nunca se cuelgue, pero solo queremos eso cuando el sistema se mantenga en un estado seguro predecible y definido.

He oído que las excepciones como control de flujo se consideran un antipatrón serio

Eso es absolutamente cierto, pero a menudo se malinterpreta. Cuando inventaron el sistema de excepción temían estar rompiendo la programación estructurada. La programación estructurada es por eso que tenemos for, while, until, break, y continuecuando todo lo que necesitamos, para hacer todo eso, es goto.

Dijkstra nos enseñó que usar goto de manera informal (es decir, saltar donde quieras) hace que leer códigos sea una pesadilla. Cuando nos dieron el sistema de excepción temieron que estaban reinventando el goto. Entonces nos dijeron que no "lo usáramos para controlar el flujo" con la esperanza de que lo entendiéramos. Desafortunadamente, muchos de nosotros no lo hicimos.

Curiosamente, a menudo no abusamos de las excepciones para crear código de espagueti como solíamos hacerlo con goto. El consejo en sí parece haber causado más problemas.

Fundamentalmente, las excepciones son sobre rechazar una suposición. Cuando solicita que se guarde un archivo, asume que el archivo puede y será guardado. La excepción que obtienes cuando no puede ser puede ser que el nombre sea ilegal, que el HD esté lleno o que una rata haya roído tu cable de datos. Puede manejar todos esos errores de manera diferente, puede manejarlos de la misma manera, o puede dejar que detengan el sistema. Hay un camino feliz en su código donde sus suposiciones deben ser ciertas. De una forma u otra, las excepciones lo llevan por ese camino feliz. Estrictamente hablando, sí, eso es una especie de "control de flujo", pero eso no es de lo que te estaban advirtiendo. Hablaban de tonterías como esta :

ingrese la descripción de la imagen aquí

"Las excepciones deben ser excepcionales". Esta pequeña tautología nació porque los diseñadores de sistemas de excepción necesitan tiempo para construir trazas de pila. En comparación con saltar, esto es lento. Come tiempo de CPU. Pero si está a punto de iniciar sesión y detener el sistema o al menos detener el procesamiento intensivo de tiempo actual antes de comenzar el siguiente, entonces tiene algo de tiempo para matar. Si las personas comienzan a usar excepciones "para el control de flujo", esas suposiciones sobre el tiempo desaparecen. Entonces, "Las excepciones deben ser excepcionales" realmente se nos dieron como una consideración de rendimiento.

Mucho más importante que eso no nos confunde. ¿Cuánto tiempo te llevó detectar el bucle infinito en el código anterior?

NO devuelva códigos de error.

... es un buen consejo cuando estás en una base de código que normalmente no usa códigos de error. ¿Por qué? Porque nadie recordará guardar el valor de retorno y verificar sus códigos de error. Todavía es una buena convención cuando estás en C.

OneOf

Estás utilizando otra convención más. Eso está bien siempre que establezca la convención y no simplemente pelee con otra. Es confuso tener dos convenciones de error en la misma base de código. Si de alguna manera se ha deshecho de todo el código que usa la otra convención, continúe.

Me gusta la convención a mí mismo. Una de las mejores explicaciones que encontré aquí * :

ingrese la descripción de la imagen aquí

Pero por mucho que me guste, todavía no voy a mezclarlo con las otras convenciones. Elige uno y quédate con él. 1

1: Con lo cual quiero decir, no me hagas pensar en más de una convención al mismo tiempo.


Pensamientos posteriores

De esta discusión lo que me llevo actualmente es lo siguiente:

  1. Si espera que la persona que llama inmediatamente detecte y maneje la excepción la mayor parte del tiempo y continúe su trabajo, tal vez a través de otra ruta, probablemente debería ser parte del tipo de retorno. Opcional o OneOf puede ser útil aquí.
  2. Si espera que la persona que llama de inmediato no detecte la excepción la mayor parte del tiempo, lance una excepción para evitar la tontería de pasarla manualmente por la pila.
  3. Si no está seguro de lo que va a hacer la persona que llama de inmediato, quizás proporcione ambos, como Parse y TryParse.

Realmente no es tan simple. Una de las cosas fundamentales que debes entender es qué es un cero.

¿Cuántos días quedan en mayo? 0 (porque no es mayo. Ya es junio).

Las excepciones son una forma de rechazar una suposición, pero no son la única forma. Si usa excepciones para rechazar la suposición, deja el camino feliz. Pero si elige valores para enviar el camino feliz que indica que las cosas no son tan simples como se suponía, entonces puede permanecer en ese camino siempre que pueda lidiar con esos valores. A veces, 0 ya se usa para significar algo, por lo que debe encontrar otro valor para mapear su supuesto rechazando la idea. Puede reconocer esta idea por su uso en álgebra antigua . Las mónadas pueden ayudar con eso, pero no siempre tiene que ser una mónada.

Por ejemplo 2 :

IList<int> ParseAllTheInts(String s) { ... }

¿Puedes pensar en alguna buena razón para que esto deba diseñarse de manera que arroje algo deliberadamente? ¿Adivina lo que obtienes cuando no se puede analizar int? Ni siquiera necesito decírtelo.

Esa es una señal de un buen nombre. Lo siento, pero TryParse no es mi idea de un buen nombre.

A menudo evitamos lanzar una excepción al no obtener nada cuando la respuesta podría ser más de una cosa al mismo tiempo, pero por alguna razón si la respuesta es una cosa o nada, nos obsesionamos con insistir en que nos dé una cosa o lanzar:

IList<Point> Intersection(Line a, Line b) { ... }

¿Las líneas paralelas realmente necesitan causar una excepción aquí? ¿Es realmente tan malo si esta lista nunca contendrá más de un punto?

Quizás semánticamente no puedes soportar eso. Si es así, es una pena. Pero tal vez las mónadas, que no tienen un tamaño arbitrario como las Listtiene, te harán sentir mejor al respecto.

Maybe<Point> Intersection(Line a, Line b) { ... }

Las Mónadas son pequeñas colecciones de propósito especial que están destinadas a ser utilizadas de manera específica para evitar tener que probarlas. Se supone que debemos encontrar formas de lidiar con ellos independientemente de lo que contengan. De esa manera, el camino feliz sigue siendo simple. Si se abre y prueba cada mónada que toca, las está usando mal.

Lo sé, es raro. Pero es una herramienta nueva (bueno, para nosotros). Así que dale algo de tiempo. Los martillos tienen más sentido cuando dejas de usarlos en los tornillos.


Si me permites, me gustaría abordar este comentario:

¿Cómo es que ninguna de las respuestas aclara que O mónada no es un código de error, y tampoco lo es OneOf? Son fundamentalmente diferentes y, en consecuencia, la pregunta parece estar basada en un malentendido. (Aunque en forma modificada, sigue siendo una pregunta válida). - Konrad Rudolph 4 de junio de 18 a 14:08

Esto es absolutamente cierto. Las mónadas están mucho más cerca de las colecciones que las excepciones, las banderas o los códigos de error. Ellos hacen contenedores finos para tales cosas cuando se usan sabiamente.

naranja confitada
fuente
99
¿No sería más apropiado si la pista roja estuviera debajo de las cajas y no las atravesara?
Flater
44
Lógicamente, tienes razón. Sin embargo, cada cuadro en este diagrama particular representa una operación de enlace monádico. bind incluye (¡quizás crea!) la pista roja. Recomiendo ver la presentación completa. Es interesante y Scott es un gran orador.
Gusdor
77
Pienso en las excepciones más como una instrucción COMEFROM que como un GOTO, ya throwque en realidad no sabe / no dice a dónde iremos;)
Warbo
3
No hay nada de malo en mezclar convenciones si puedes establecer límites, de eso se trata la encapsulación.
Robert Harvey
44
La primera sección completa de esta respuesta se lee con fuerza como si dejaras de leer la pregunta después de la oración citada. La pregunta reconoce que bloquear el programa es superior a pasar a un comportamiento indefinido: son choques contrastantes en tiempo de ejecución, no con una ejecución continua e indefinida, sino con errores en tiempo de compilación que le impiden incluso construir el programa sin considerar el posible error y lidiar con eso (que puede terminar siendo un bloqueo si no hay nada más que hacer, pero será un bloqueo porque quieres que lo sea, no porque lo hayas olvidado).
KRyan
35

C # no es Haskell, y debe seguir el consenso de expertos de la comunidad de C #. Si, en cambio, intentas seguir las prácticas de Haskell en un proyecto de C #, alienarás a todos los demás en tu equipo, y eventualmente descubrirás las razones por las cuales la comunidad de C # hace las cosas de manera diferente. Una gran razón es que C # no apoya convenientemente a los sindicatos discriminados.

He oído que las excepciones como control de flujo se consideran un antipatrón serio,

Esta no es una verdad universalmente aceptada. La opción en cada idioma que admite excepciones es lanzar una excepción (que la persona que llama no puede manejar) o devolver algún valor compuesto (que la persona que llama DEBE manejar).

Propagar las condiciones de error hacia arriba a través de la pila de llamadas requiere un condicional en cada nivel, duplicando la complejidad ciclomática de esos métodos y, en consecuencia, duplicando el número de casos de prueba unitaria. En las aplicaciones comerciales típicas, muchas excepciones van más allá de la recuperación y pueden propagarse al más alto nivel (por ejemplo, el punto de entrada de servicio de una aplicación web).

Kevin Cline
fuente
99
Propagar mónadas no requiere nada.
Basilevs
23
@Basilevs Requiere soporte para propagar mónadas mientras se mantiene el código legible, que C # no tiene. O recibes instrucciones repetidas de if-return o cadenas de lambdas.
Sebastian Redl
44
@SebastianRedl lambdas se ven bien en C #.
Basilevs
@Basilevs Desde el punto de vista de la sintaxis, sí, pero sospecho que desde el punto de vista del rendimiento podrían ser menos atractivos.
Pharap
55
En C # moderno, probablemente pueda usar parcialmente las funciones locales para recuperar algo de velocidad. En cualquier caso, la mayoría de los softwares comerciales se ralentiza mucho más significativamente por IO que por otra cosa.
Turksarama
26

Has venido a C # en un momento interesante. Hasta hace relativamente poco, el lenguaje se asentaba firmemente en el imperativo espacio de programación. Usar excepciones para comunicar errores era definitivamente la norma. El uso de códigos de retorno se vio afectado por la falta de soporte lingüístico (por ejemplo, alrededor de uniones discriminadas y coincidencia de patrones). De manera bastante razonable, la orientación oficial de Microsoft fue evitar los códigos de retorno y utilizar excepciones.

Sin embargo, siempre ha habido excepciones a esto, en forma de TryXXXmétodos, que devolverían un booleanresultado exitoso y proporcionarían un segundo resultado de valor a través de un outparámetro. Estos son muy similares a los patrones de "prueba" en la programación funcional, excepto que el resultado proviene de un parámetro de salida, en lugar de un Maybe<T>valor de retorno.

Pero las cosas están cambiando. La programación funcional se está volviendo aún más popular y los lenguajes como C # están respondiendo. C # 7.0 introdujo algunas características básicas de coincidencia de patrones en el lenguaje; C # 8 introducirá mucho más, incluyendo una expresión de cambio, patrones recursivos, etc. Junto con esto, ha habido un crecimiento en "bibliotecas funcionales" para C #, como mi propia biblioteca Succinc <T> , que también brinda soporte para uniones discriminadas .

Estas dos cosas combinadas significan que el código como el siguiente está creciendo lentamente en popularidad.

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

Todavía es bastante nicho en este momento, aunque incluso en los últimos dos años he notado un marcado cambio de hostilidad general entre los desarrolladores de C # a dicho código a un creciente interés en el uso de tales técnicas.

Todavía no hemos llegado a decir " NO devuelva códigos de error". Es anticuado y necesita retirarse. Pero la marcha por el camino hacia ese objetivo está en marcha. Así que elija: quédese con las viejas formas de lanzar excepciones con el abandono gay si esa es la forma en que le gusta hacer las cosas; o comience a explorar un mundo nuevo donde las convenciones de lenguajes funcionales se están volviendo más populares en C #.

David Arno
fuente
22
Es por eso que odio C # :) Siguen agregando formas de desollar a un gato y, por supuesto, las viejas formas nunca desaparecen. Al final no hay convenciones para nada, un programador perfeccionista puede sentarse durante horas decidiendo qué método es "más agradable", y un nuevo programador tiene que aprender todo antes de poder leer el código de otros.
Aleksandr Dubinsky
24
@AleksandrDubinsky, te encuentras con un problema central con los lenguajes de programación en general, no solo con C #. Se crean nuevos lenguajes, esbeltos, frescos y llenos de ideas modernas. Y muy pocas personas los usan. Con el tiempo, obtienen usuarios y nuevas funciones, agregadas a las funciones antiguas que no se pueden eliminar porque rompería el código existente. La hinchazón crece. Entonces la gente crea nuevos lenguajes que son delgados, frescos y llenos de ideas modernas ...
David Arno
77
@AleksandrDubinsky, no lo odies cuando agregan ahora características de la década de 1960.
ctrl-alt-delor
66
@AleksandrDubinsky Sí. Debido a que Java intentó agregar una cosa una vez (genéricos), fallaron, ahora tenemos que vivir con el horrible desastre que resultó, por lo que aprendieron la lección y dejaron de agregar cualquier cosa
:)
3
@Agent_L: Sabes que Java ha agregado java.util.Stream(un IEnumerable similar a medio asno) y lambdas ahora, ¿verdad? :)
cHao
5

Creo que es perfectamente razonable usar un DU para devolver errores, pero luego diría eso mientras escribía OneOf :) Pero lo diría de todos modos, ya que es una práctica común en F #, etc. (por ejemplo, el tipo de resultado, como lo mencionaron otros )

No pienso en los errores que generalmente devuelvo como situaciones excepcionales, sino como normales, pero espero que rara vez se encuentren situaciones que puedan o no necesiten ser manejadas, pero que deben modelarse.

Aquí está el ejemplo de la página del proyecto.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

Lanzar excepciones para estos errores parece una exageración: tienen una sobrecarga de rendimiento, aparte de las desventajas de no ser explícitos en la firma del método o proporcionar una coincidencia exhaustiva.

Hasta qué punto OneOf es idiomático en C #, esa es otra pregunta. La sintaxis es válida y razonablemente intuitiva. Los DU y la programación orientada al ferrocarril son conceptos bien conocidos.

Guardaría Excepciones para cosas que indiquen que algo está roto cuando sea posible.

mcintyre321
fuente
Lo que está diciendo es que OneOfse trata de enriquecer el valor de retorno, como devolver un Nullable. Reemplaza las excepciones para situaciones que no son excepcionales para empezar.
Aleksandr Dubinsky
Sí, y proporciona una manera fácil de hacer una comparación exhaustiva en la persona que llama, lo que no es posible utilizando una jerarquía de resultados basada en herencia.
mcintyre321
4

OneOfes casi equivalente a las excepciones marcadas en Java. Hay algunas diferencias:

  • OneOfno funciona con métodos que producen métodos llamados por sus efectos secundarios (como pueden llamarse significativamente y el resultado simplemente ignorado). Obviamente, todos deberíamos intentar usar funciones puras, pero esto no siempre es posible.

  • OneOfno proporciona información sobre dónde ocurrió el problema. Esto es bueno, ya que ahorra rendimiento (las excepciones lanzadas en Java son costosas debido a que llena el seguimiento de la pila, y en C # será lo mismo) y lo obliga a proporcionar información suficiente en el miembro de error de OneOfsí mismo. También es malo, ya que esta información puede ser insuficiente y puede ser difícil encontrar el origen del problema.

OneOf y la excepción marcada tienen una "característica" importante en común:

  • ambos te obligan a manejarlos en todas partes en la pila de llamadas

Esto evita tu miedo

En C #, ignorar las excepciones no produce un error de compilación, y si no se detectan, simplemente aparecen y bloquean su programa.

pero como ya se dijo, este miedo es irracional. Básicamente, generalmente hay un solo lugar en su programa, donde necesita atrapar todo (es probable que ya use un marco para hacerlo). Como usted y su equipo suelen pasar semanas o años trabajando en el programa, no lo olvidarán, ¿verdad?

La gran ventaja de las excepciones ignorables es que se pueden manejar donde quieras manejarlas, en lugar de hacerlo en cualquier parte del seguimiento de la pila. Esto elimina toneladas de repeticiones (solo mire algunos códigos Java que declaran "lanzamientos ..." o excepciones de ajuste) y también hace que su código sea menos defectuoso: como cualquier método puede arrojar, debe ser consciente de ello. Afortunadamente, la acción correcta generalmente no hace nada , es decir, deja que burbujee hasta un lugar donde pueda manejarse de manera razonable.

maaartinus
fuente
+1 pero ¿por qué OneOf no funciona con efectos secundarios? (Supongo que es porque va a pasar una verificación de si no se asigna el valor de retorno, pero como no sé Oneof, ni mucho C # No estoy seguro -. Aclaración podrían mejorar su respuesta)
dcorking
1
Puede devolver información de error con OneOf haciendo que el objeto de error devuelto contenga información útil, por ejemplo, OneOf<SomeSuccess, SomeErrorInfo>donde SomeErrorInfoproporciona detalles del error.
mcintyre321
@dcorking Editado. Claro, si ignoró el valor de retorno, entonces no sabe nada sobre qué problema sucedió. Si haces esto con una función pura, está bien (acabas de perder el tiempo).
maaartinus
2
@ mcintyre321 Claro, puede agregar información, pero debe hacerlo manualmente y está sujeta a la pereza y al olvido. Supongo que, en casos más complicados, un seguimiento de la pila es más útil.
maaartinus
4

¿Usar OneOf en lugar de excepciones para manejar excepciones "ordinarias" (como File Not Found, etc.) es un enfoque razonable o es una práctica terrible?

Vale la pena señalar que he oído que las excepciones como flujo de control se consideran un antipatrón serio, por lo que si la "excepción" es algo que normalmente manejaría sin finalizar el programa, ¿no es ese "flujo de control" de alguna manera?

Los errores tienen varias dimensiones. Identifico tres dimensiones: ¿pueden prevenirse, con qué frecuencia ocurren y si pueden recuperarse? Mientras tanto, la señalización de error tiene principalmente una dimensión: decidir en qué medida golpear a la persona que llama por encima de la cabeza para obligarla a manejar el error o dejar que el error "silenciosamente" brote como una excepción.

Los errores no prevenibles, frecuentes y recuperables son los que realmente deben abordarse en el sitio de la llamada. Es mejor justificar obligar a la persona que llama a confrontarlos. En Java, hago que sean excepciones marcadas, y se logra un efecto similar al hacerlos Try*métodos o al devolver una unión discriminada. Las uniones discriminadas son especialmente agradables cuando una función tiene un valor de retorno. Son una versión mucho más refinada del regreso null. La sintaxis de los try/catchbloques no es excelente (demasiados corchetes), lo que hace que las alternativas se vean mejor. Además, las excepciones son un poco lentas porque registran rastros de pila.

Los errores evitables (que no se evitaron debido a un error / negligencia del programador) y los errores no recuperables funcionan realmente bien como excepciones comunes. Los errores poco frecuentes también funcionan mejor como excepciones comunes, ya que un programador a menudo puede considerar que no vale la pena anticipar el esfuerzo (depende del propósito del programa).

Un punto importante es que a menudo es el sitio de uso el que determina cómo se ajustan los modos de error de un método a lo largo de estas dimensiones, lo que debe tenerse en cuenta al considerar lo siguiente.

En mi experiencia, obligar a la persona que llama a enfrentar las condiciones de error está bien cuando los errores no son evitables, frecuentes y recuperables, pero se vuelve muy molesto cuando la persona que llama se ve obligada a saltar a través de aros cuando no lo necesitan o no quieren . Esto puede deberse a que la persona que llama sabe que el error no ocurrirá o porque (todavía) no quiere que el código sea resistente. En Java, esto explica la angustia contra el uso frecuente de excepciones marcadas y la devolución de Opcional (que es similar a Quizás y se introdujo recientemente en medio de mucha controversia). En caso de duda, arroje excepciones y deje que la persona que llama decida cómo quiere manejarlas.

Por último, tenga en cuenta que lo más importante sobre las condiciones de error, muy por encima de cualquier otra consideración, como la forma en que se señalan, es documentarlas a fondo .

Aleksandr Dubinsky
fuente
2

Las otras respuestas han discutido las excepciones frente a los códigos de error con suficiente detalle, por lo que me gustaría agregar otra perspectiva específica a la pregunta:

OneOf no es un código de error, es más como una mónada

La forma en que se usa OneOf<Value, Error1, Error2>es un contenedor que representa el resultado real o algún estado de error. Es como un Optional, excepto que cuando no hay valor presente, puede proporcionar más detalles por qué es así.

El principal problema con los códigos de error es que se olvida de verificarlos. Pero aquí, literalmente, no puede acceder al resultado sin verificar errores.

El único problema es cuando no te importa el resultado. El ejemplo de la documentación de OneOf da:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

... y luego crean diferentes respuestas HTTP para cada resultado, lo cual es mucho mejor que usar excepciones. Pero si llamas al método así.

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

entonces usted no tiene un problema y las excepciones no es la mejor opción. Comparación de Rail savevs save!.

Cefalópodo
fuente
1

Una cosa que debe tener en cuenta es que el código en C # generalmente no es puro. En una base de código llena de efectos secundarios, y con un compilador que no le importa lo que haga con el valor de retorno de alguna función, su enfoque puede causarle una pena considerable. Por ejemplo, si tiene un método que elimina un archivo, desea asegurarse de que la aplicación se da cuenta cuando falla el método, aunque no haya ninguna razón para verificar ningún código de error "en la ruta feliz". Esta es una diferencia considerable con respecto a los lenguajes funcionales que requieren que ignore explícitamente los valores de retorno que no está utilizando. En C #, los valores de retorno siempre se ignoran implícitamente (la única excepción son los captadores de propiedades IIRC).

La razón por la que MSDN le dice "no devolver códigos de error" es exactamente esto: nadie lo obliga a leer el código de error. El compilador no te ayuda en absoluto. Sin embargo, si su función no tiene efectos secundarios, puede usar algo como Either: el punto es que incluso si ignora el resultado del error (si lo hay), solo puede hacerlo si no usa el "resultado adecuado" ya sea. Hay algunas maneras de lograr esto: por ejemplo, solo puede permitir "leer" el valor pasando a un delegado para manejar los casos de éxito y error, y puede combinarlo fácilmente con enfoques como la programación orientada al ferrocarril (lo hace funciona bien en C #).

int.TryParseEstá en algún lugar en el medio. Es puro Define cuáles son los valores de los dos resultados en todo momento, por lo que sabe que si el valor de retorno es false, el parámetro de salida se establecerá en 0. Todavía no le impide usar el parámetro de salida sin verificar el valor de retorno, pero al menos tiene garantizado cuál es el resultado, incluso si la función falla.

Pero una cosa absolutamente crucial aquí es la consistencia . El compilador no te va a salvar si alguien cambia esa función para tener efectos secundarios. O para lanzar excepciones. Entonces, si bien este enfoque está perfectamente bien en C # (y lo uso), debe asegurarse de que todos en su equipo lo entiendan y lo usen . Muchos de los invariantes necesarios para que la programación funcional funcione de maravilla no son aplicados por el compilador de C #, y debe asegurarse de que los siga usted mismo. Debe mantenerse al tanto de las cosas que se rompen si no sigue las reglas que Haskell aplica de manera predeterminada, y esto es algo que debe tener en cuenta para cualquier paradigma funcional que introduzca en su código.

Luaan
fuente
0

La afirmación correcta sería No usar códigos de error para excepciones. Y no use excepciones para no excepciones.

Si sucede algo de lo que su código no puede recuperarse (es decir, hacerlo funcionar), esa es una excepción. No hay nada que su código inmediato haría con un código de error, excepto entregarlo hacia arriba para iniciar sesión o tal vez proporcionar el código al usuario de alguna manera. Sin embargo, esto es exactamente para lo que son las excepciones: se ocupan de todas las entregas y ayudan a analizar lo que realmente sucedió.

Sin embargo, si sucede algo, como una entrada no válida que puede manejar, ya sea reformateándola automáticamente o pidiéndole al usuario que corrija los datos (y usted pensó en ese caso), eso no es realmente una excepción en primer lugar, como usted Esperar algo. Puede manejar esos casos con códigos de error o cualquier otra arquitectura. Simplemente no hay excepciones.

(Tenga en cuenta que no tengo mucha experiencia en C #, pero mi respuesta debería ser válida en el caso general basado en el nivel conceptual de las excepciones).

Frank Hopkins
fuente
77
Estoy básicamente en desacuerdo con la premisa de esta respuesta. Si los diseñadores de idiomas no tuvieran la intención de utilizar excepciones para errores recuperables, la catchpalabra clave no existiría. Y dado que estamos hablando en un contexto C #, vale la pena señalar que la filosofía expuesta aquí no es seguida por el marco .NET; quizás el ejemplo más simple imaginable de "entrada no válida que puede manejar" es que el usuario escribe un no número en un campo donde se espera un número ... y int.Parselanza una excepción.
Mark Amery
@MarkAmery Para int.Parse no es nada de lo que pueda recuperarse ni nada de lo que esperaría. La cláusula catch generalmente se usa para evitar una falla completa desde una vista externa, no le pedirá al usuario nuevas credenciales de base de datos o similares, evitará que el programa se bloquee. Es una falla técnica, no un flujo de control de la aplicación.
Frank Hopkins
3
La cuestión de si es mejor que una función arroje una excepción cuando se produce una condición depende de si el llamador inmediato de la función puede manejar esa condición de manera útil. En los casos en que puede, lanzar una excepción que la persona que llama inmediatamente debe detectar representa un trabajo adicional para el autor del código de llamada y un trabajo adicional para la máquina que lo procesa. Sin embargo, si una persona que llama no va a estar preparada para manejar una condición, las excepciones alivian la necesidad de que la persona que llama codifique explícitamente.
supercat
@Darkwing "Puede manejar esos casos con códigos de error o cualquier otra arquitectura". Sí, eres libre de hacer eso. Sin embargo, no obtiene ningún soporte de .NET para hacerlo, ya que .NET CL usa excepciones en lugar de códigos de error. Entonces, no solo en muchos casos se ve obligado a detectar las excepciones .NET y devolver el código de error correspondiente en lugar de dejar que la excepción brote, sino que también está obligando a otros miembros de su equipo a otro concepto de manejo de errores que tienen que usar Paralelamente a las excepciones, el concepto estándar de manejo de errores.
Aleksander
-2

Es una 'idea terrible'.

Es terrible porque, al menos en .net, no tienes una lista exhaustiva de posibles excepciones. Cualquier código posiblemente puede arrojar cualquier excepción. Especialmente si está haciendo OOP y podría estar llamando a métodos anulados en un subtipo en lugar del tipo declarado

Por lo tanto, tendría que cambiar todos los métodos y funciones OneOf<Exception, ReturnType>, entonces, si desea manejar diferentes tipos de excepción, deberá examinar el tipo, If(exception is FileNotFoundException)etc.

try catch es el equivalente de OneOf<Exception, ReturnType>

Editar ----

Soy consciente de que propones regresar, OneOf<ErrorEnum, ReturnType>pero creo que esto es algo así como una distinción sin diferencia.

Está combinando códigos de retorno, excepciones esperadas y ramificación por excepción. Pero el efecto general es el mismo.

Ewan
fuente
2
Las excepciones 'normales' también son una idea terrible. la pista está en el nombre
Ewan
44
No estoy proponiendo regresar Exceptions.
Clinton
¿Está proponiendo que la alternativa a uno de ellos es el flujo de control a través de excepciones
Ewan