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 Either
Haskell.
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 OneOf
de 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 OneOf
para 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:
- Si espera que la persona que llama inmediatamente
catch
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.Optional
oOneOf
puede ser útil aquí. - 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.
- Si no está seguro de lo que va a hacer la persona que llama inmediatamente, tal vez proporcione ambos, como
Parse
yTryParse
.
fuente
Result
;-)FileNotFoundException
.Either
mónada no es un código de error , y tampoco lo esOneOf
? Son fundamentalmente diferentes y, en consecuencia, la pregunta parece estar basada en un malentendido. (Aunque en forma modificada, sigue siendo una pregunta válida).Respuestas:
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.
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
, ycontinue
cuando todo lo que necesitamos, para hacer todo eso, esgoto
.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 :
"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?
... 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.
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í * :
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.
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 :
¿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:
¿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
List
tiene, te harán sentir mejor al respecto.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:
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.
fuente
throw
que en realidad no sabe / no dice a dónde iremos;)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.
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).
fuente
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
TryXXX
métodos, que devolverían unboolean
resultado exitoso y proporcionarían un segundo resultado de valor a través de unout
pará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 unMaybe<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.
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 #.
fuente
java.util.Stream
(un IEnumerable similar a medio asno) y lambdas ahora, ¿verdad? :)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.
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.
fuente
OneOf
se trata de enriquecer el valor de retorno, como devolver un Nullable. Reemplaza las excepciones para situaciones que no son excepcionales para empezar.OneOf
es casi equivalente a las excepciones marcadas en Java. Hay algunas diferencias:OneOf
no 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.OneOf
no 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 deOneOf
sí 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:Esto evita tu miedo
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.
fuente
OneOf<SomeSuccess, SomeErrorInfo>
dondeSomeErrorInfo
proporciona detalles del error.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 regresonull
. La sintaxis de lostry/catch
bloques 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 .
fuente
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 unOptional
, 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:
... y luego crean diferentes respuestas HTTP para cada resultado, lo cual es mucho mejor que usar excepciones. Pero si llamas al método así.
entonces usted no tiene un problema y las excepciones no es la mejor opción. Comparación de Rail
save
vssave!
.fuente
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.TryParse
Está 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 esfalse
, el parámetro de salida se establecerá en0
. 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.
fuente
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).
fuente
catch
palabra 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 ... yint.Parse
lanza una excepción.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 deOneOf<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.
fuente
Exception
s.