A veces termino teniendo que escribir un método o propiedad para una biblioteca de clases para la cual no es excepcional no tener una respuesta real, sino un error. Algo no se puede determinar, no está disponible, no se encuentra, no es posible actualmente o no hay más datos disponibles.
Creo que hay tres posibles soluciones para una situación relativamente no excepcional para indicar un fallo en C # 4:
- devolver un valor mágico que no tiene sentido de otra manera (como
null
y-1
); - lanzar una excepción (por ejemplo
KeyNotFoundException
); - devolver
false
y proporcionar el valor de retorno real en unout
parámetro, (comoDictionary<,>.TryGetValue
).
Entonces las preguntas son: ¿ en qué situación no excepcional debo lanzar una excepción? Y si no debo lanzar: ¿ cuándo se devuelve un valor mágico perferido arriba implementando un Try*
método con un out
parámetro ? (Para mí, el out
parámetro parece sucio, y es más difícil usarlo correctamente).
Estoy buscando respuestas fácticas, como respuestas que involucren pautas de diseño (no conozco ninguno sobre los Try*
métodos), usabilidad (como pido esto para una biblioteca de clase), coherencia con el BCL y legibilidad.
En .NET Framework Base Class Library, se utilizan los tres métodos:
- devolver un valor mágico que no tiene sentido de otra manera:
Collection<T>.IndexOf
devuelve -1,StreamReader.Read
devuelve -1,Math.Sqrt
devuelve NaN,Hashtable.Item
devuelve nulo;
- lanzar una excepción:
Dictionary<,>.Item
lanza KeyNotFoundException,Double.Parse
lanza FormatException; o
- devolver
false
y proporcionar el valor de retorno real en unout
parámetro:
Tenga en cuenta que, como Hashtable
se creó en el momento en que no había genéricos en C #, se usa object
y, por lo tanto, puede regresar null
como un valor mágico. Pero con los genéricos, se usan excepciones Dictionary<,>
, y al principio no TryGetValue
. Al parecer, las ideas cambian.
Obviamente, el Item
- TryGetValue
y Parse
- TryParse
la dualidad está ahí por una razón, por lo que suponen que lanzar excepciones de fallos no es excepcional en C # 4 no se realiza . Sin embargo, los Try*
métodos no siempre existieron, incluso cuando Dictionary<,>.Item
existían.
fuente
Respuestas:
No creo que tus ejemplos sean realmente equivalentes. Hay tres grupos distintos, cada uno con su propia justificación de su comportamiento.
StreamReader.Read
o cuando hay un valor fácil de usar que nunca será una respuesta válida (-1 paraIndexOf
).Los ejemplos que proporciona son perfectamente claros para los casos 2 y 3. Para los valores mágicos, se puede argumentar si esta es una buena decisión de diseño o no en todos los casos.
El
NaN
devuelto porMath.Sqrt
es un caso especial: sigue el estándar de coma flotante.fuente
Either<TLeft, TRight>
mónada?Estás intentando comunicar al usuario de la API lo que tienen que hacer. Si lanzas una excepción, no hay nada que los obligue a atraparla, y solo leer la documentación les hará saber cuáles son todas las posibilidades. Personalmente, considero que es lento y tedioso profundizar en la documentación para encontrar todas las excepciones que un determinado método podría arrojar (incluso si está en modo inteligente, todavía tengo que copiarlas manualmente).
Un valor mágico aún requiere que lea la documentación y posiblemente haga referencia a alguna
const
tabla para decodificar el valor. Al menos no tiene la sobrecarga de una excepción para lo que llama una ocurrencia no excepcional.Es por eso que a pesar de que los
out
parámetros a veces están mal vistos, prefiero ese método, con laTry...
sintaxis. Es sintaxis canónica .NET y C #. Le está comunicando al usuario de la API que tiene que verificar el valor de retorno antes de usar el resultado. También puede incluir un segundoout
parámetro con un útil mensaje de error, que nuevamente ayuda con la depuración. Es por eso que voto por la soluciónTry...
con elout
parámetro.Otra opción es devolver un objeto "resultado" especial, aunque esto me parece mucho más tedioso:
Entonces su función se ve bien porque solo tiene parámetros de entrada y solo devuelve una cosa. Es solo que lo único que devuelve es una especie de tupla.
De hecho, si te gusta ese tipo de cosas, puedes usar las nuevas
Tuple<>
clases que se introdujeron en .NET 4. Personalmente, no me gusta el hecho de que el significado de cada campo es menos explícito porque no puedo darItem1
yItem2
nombres útiles.fuente
IMyResult
causa, es posible comunicar un resultado más complejo que la simpletrue
ofalse
o el valor del resultado.Try*()
solo es útil para cosas simples como la conversión de cadenas a int.Como sus ejemplos ya muestran, cada uno de estos casos debe evaluarse por separado y hay un espectro considerable de gris entre "circunstancias excepcionales" y "control de flujo", especialmente si su método está destinado a ser reutilizable, y podría usarse en patrones bastante diferentes de lo que fue originalmente diseñado. No espere que todos acordamos lo que significa "no excepcional", especialmente si discute de inmediato la posibilidad de utilizar "excepciones" para implementar eso.
Es posible que tampoco estemos de acuerdo sobre qué diseño hace que el código sea más fácil de leer y mantener, pero supondré que el diseñador de la biblioteca tiene una visión personal clara de eso y solo necesita equilibrarlo con las otras consideraciones involucradas.
Respuesta corta
Siga sus instintos, excepto cuando diseñe métodos bastante rápidos y espere una posibilidad de reutilización imprevista.
Respuesta larga
Cada futuro llamante puede traducir libremente entre códigos de error y excepciones como lo desee en ambas direcciones; Esto hace que los dos enfoques de diseño sean casi equivalentes, excepto por el rendimiento, la facilidad de depuración y algunos contextos de interoperabilidad restringidos. Esto generalmente se reduce al rendimiento, así que concentrémonos en eso.
Como regla general, espere que lanzar una excepción sea 200 veces más lento que un rendimiento regular (en realidad, hay una variación significativa en eso).
Como otra regla general, lanzar una excepción a menudo puede permitir un código mucho más limpio en comparación con los valores mágicos más crudos, porque no confía en que el programador traduzca el código de error a otro código de error, ya que viaja a través de múltiples capas de código de cliente hacia un punto donde hay suficiente contexto para manejarlo de manera consistente y adecuada. (Caso especial:
null
tiende a tener mejores resultados aquí que otros valores mágicos debido a su tendencia a traducirse automáticamente a unoNullReferenceException
en el caso de algunos, pero no todos los tipos de defectos; generalmente, pero no siempre, bastante cerca de la fuente del defecto. )Entonces, ¿cuál es la lección?
Para una función que se llama solo algunas veces durante la vida útil de una aplicación (como la inicialización de la aplicación), use cualquier cosa que le brinde un código más limpio y fácil de entender. El rendimiento no puede ser una preocupación.
Para una función desechable, use cualquier cosa que le proporcione un código más limpio. Luego, realice algunos perfiles (si es necesario) y cambie las excepciones a los códigos de retorno si se encuentran entre los principales cuellos de botella sospechosos según las mediciones o la estructura general del programa.
Para una función reutilizable costosa, use cualquier cosa que le proporcione un código más limpio. Si básicamente tiene que someterse a un viaje de ida y vuelta a la red o analizar un archivo XML en el disco, la sobrecarga de lanzar una excepción probablemente sea insignificante. Es más importante no perder detalles de cualquier falla, ni siquiera accidentalmente, que regresar de una "falla no excepcional" extra rápido.
Una función magra reutilizable requiere más reflexión. Al emplear excepciones, está forzando una ralentización 100 veces mayor en las personas que llaman que verán la excepción en la mitad de sus (muchas) llamadas, si el cuerpo de la función se ejecuta muy rápido. Las excepciones siguen siendo una opción de diseño, pero tendrá que proporcionar una alternativa de bajo costo adicional para las personas que llaman que no pueden pagar esto. Veamos un ejemplo.
Enumera un gran ejemplo de
Dictionary<,>.Item
, que, en términos generales, cambió denull
valores devueltos a arrojarKeyNotFoundException
entre .NET 1.1 y .NET 2.0 (solo si está dispuesto a considerarHashtable.Item
su precursor práctico no genérico). La razón de este "cambio" no carece de interés aquí. La optimización del rendimiento de los tipos de valor (no más boxeo) hizo que el valor mágico original (null
) no fuera una opción;out
los parámetros solo traerían una pequeña parte del costo de rendimiento. Esta última consideración de rendimiento es completamente insignificante en comparación con la sobrecarga de lanzar aKeyNotFoundException
, pero el diseño de excepción todavía es superior aquí. ¿Por qué?Contains
antes de cualquier llamada al indexador, y este patrón se lee de forma completamente natural. Si un desarrollador quiere pero se olvida de llamarContains
, no pueden aparecer problemas de rendimiento;KeyNotFoundException
es lo suficientemente fuerte como para ser notado y reparado.fuente
Contains
antes de una llamada a un indexador puede causar una condición de carrera que no tiene que estar presenteTryGetValue
.ConcurrentDictionary
para el acceso de subprocesos múltiples yDictionary
para el acceso de subprocesos simples para un rendimiento óptimo. Es decir, no usar `` Contiene '' NO hace que el hilo del código sea seguro con estaDictionary
clase en particular .No debes permitir el fracaso.
Lo sé, es de onda manual e idealista, pero escúchame. Al hacer el diseño, hay una serie de casos en los que tiene la oportunidad de favorecer una versión que no tiene modos de falla. En lugar de tener un 'FindAll' que falla, LINQ usa una cláusula where que simplemente devuelve un enumerable vacío. En lugar de tener un objeto que necesita inicializarse antes de usarse, deje que el constructor inicialice el objeto (o inicialice cuando se detecte lo no inicializado). La clave es eliminar la rama de falla en el código del consumidor. Este es el problema, así que concéntrate en ello.
Otra estrategia para esto es el
KeyNotFound
sscenario. En casi todas las bases de código en las que he trabajado desde 3.0, existe algo como este método de extensión:No hay modo de falla real para esto.
ConcurrentDictionary
tiene un similarGetOrAdd
construido en.Dicho todo esto, siempre habrá momentos en los que simplemente será inevitable. Los tres tienen su lugar, pero yo preferiría la primera opción. A pesar de todo lo que está hecho del peligro nulo, es bien conocido y encaja en muchos de los escenarios de 'elemento no encontrado' o 'resultado no aplicable' que conforman el conjunto de "falla no excepcional". Especialmente cuando está haciendo tipos de valores anulables, la importancia de 'esto podría fallar' es muy explícita en el código y difícil de olvidar / arruinar.
La segunda opción es lo suficientemente buena cuando su usuario hace algo tonto. Le da una cadena con el formato incorrecto, intenta establecer la fecha para el 42 de diciembre ... algo que desea que explote rápidamente y espectacularmente durante las pruebas para que se identifique y corrija el código incorrecto.
La última opción es una que no me gusta cada vez más. Los parámetros de salida son incómodos y tienden a violar algunas de las mejores prácticas al hacer métodos, como centrarse en una cosa y no tener efectos secundarios. Además, el parámetro externo generalmente solo tiene sentido durante el éxito. Dicho esto, son esenciales para ciertas operaciones que generalmente están limitadas por preocupaciones de concurrencia o consideraciones de rendimiento (donde no desea hacer un segundo viaje a la base de datos, por ejemplo).
Si el valor de retorno y out param no son triviales, se prefiere la sugerencia de Scott Whitlock sobre un objeto de resultado (como la
Match
clase de Regex ).fuente
out
parámetros son completamente ortogonales a la cuestión de los efectos secundarios. Modificar unref
parámetro es un efecto secundario, y modificar el estado de un objeto que pasa a través de un parámetro de entrada es un efecto secundario, pero unout
parámetro es solo una forma incómoda de hacer que una función devuelva más de un valor. No hay efectos secundarios, solo múltiples valores de retorno.Siempre prefiero lanzar una excepción. Tiene una interfaz uniforme entre todas las funciones que podrían fallar, e indica la falla tan ruidosamente como sea posible, una propiedad muy deseable.
Tenga en cuenta que
Parse
yTryParse
no son realmente lo mismo, aparte de los modos de falla. El hecho de queTryParse
también puede devolver el valor es algo ortogonal, en realidad. Considere la situación en la que, por ejemplo, está validando alguna entrada. En realidad no te importa cuál es el valor, siempre que sea válido. Y no hay nada de malo en ofrecer un tipo deIsValidFor(arguments)
función. Pero nunca puede ser el modo primario de operación.fuente
Como otros han señalado, el valor mágico (incluido un valor de retorno booleano) no es una solución tan buena, excepto como un marcador de "fin de rango". Motivo: la semántica no es explícita, incluso si examina los métodos del objeto. Debes leer la documentación completa de todo el objeto hasta "oh sí, si devuelve -42, eso significa bla bla bla".
Esta solución puede usarse por razones históricas o por razones de rendimiento, pero de lo contrario debe evitarse.
Esto deja dos casos generales: sondeo o excepciones.
Aquí, la regla general es que el programa no debe reaccionar a las excepciones, excepto para manejar cuando el programa / involuntariamente / viola alguna condición. El sondeo debe usarse para garantizar que esto no suceda. Por lo tanto, una excepción significa que el sondeo relevante no se realizó por adelantado o que sucedió algo completamente inesperado.
Ejemplo:
Desea crear un archivo a partir de una ruta determinada.
Debe usar el objeto Archivo para evaluar de antemano si esta ruta es legal para la creación o escritura de archivos.
Si su programa de alguna manera todavía termina tratando de escribir en una ruta que es ilegal o de lo contrario no se puede escribir, debería obtener una excaption. Esto podría suceder debido a una condición de carrera (algún otro usuario eliminó el directorio, o lo hizo de solo lectura, después de que usted probablemente)
La tarea de manejar una falla inesperada (señalada por una excepción) y verificar si las condiciones son adecuadas para la operación por adelantado (sondeo) generalmente se estructurará de manera diferente y, por lo tanto, debe usar mecanismos diferentes.
fuente
Creo que el
Try
patrón es la mejor opción, cuando el código solo indica lo que sucedió. Odio a param y me gusta el objeto anulable. He creado la siguiente clasepara poder escribir el siguiente código
Parece anulable pero no solo para el tipo de valor
Otro ejemplo
fuente
try
catch
causará estragos en su rendimiento si llamaGetXElement()
y falla muchas veces.public struct Nullable<T> where T : struct
la principal diferencia en una restricción. por cierto, la última versión está aquí github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…Si está interesado en la ruta del "valor mágico", otra forma de resolver esto es sobrecargar el propósito de la clase Lazy. Aunque Lazy está destinado a diferir la creación de instancias, nada realmente le impide usarlo como un Quizás o una Opción. Por ejemplo:
fuente