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 uno NullReferenceException
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ó de null
valores devueltos a arrojar KeyNotFoundException
entre .NET 1.1 y .NET 2.0 (solo si está dispuesto a considerar Hashtable.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 a KeyNotFoundException
, pero el diseño de excepción todavía es superior aquí. ¿Por qué?
- los parámetros de ref / out incurren en sus costos cada vez, no solo en el caso de "falla"
- Cualquiera a quien le importe puede llamar
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 llamar Contains
, no pueden aparecer problemas de rendimiento; KeyNotFoundException
es lo suficientemente fuerte como para ser notado y reparado.