2009-03-23 10 views
8

¿Cuál es una mejor práctica en programación?Excepciones vs Valores de devolución especiales

No estoy hablando de total exclusividad. Es más para cosas como:

list<T>.Find, donde obtiene default(T) o su valor, en lugar de la excepción ValueNotFound (ejemplo).

o

list<T>.IndexOf, donde se obtiene -1 o el índice correcto.

Respuesta

16

Bueno, la respuesta es que depende.

En el caso de que un artículo no se encuentre en una lista, lanzar una excepción es una práctica horrible, porque es completamente posible que el artículo no esté en la lista.

Ahora bien, si esto era una lista especializada de algún tipo, y el artículo debe absolutamente se encuentra en la lista, a continuación, que debe lanzar una excepción, ya que se ha encontrado con una situación que no era factible/razonable.

En términos generales, para cosas como las reglas de negocios y similares, los códigos de error especializados son mejores, porque usted es consciente de la posibilidad de que esto ocurra y desea reaccionar a esas posibilidades. Las excepciones son para los casos que no espera y no puede continuar con la ejecución del código si se producen.

+0

En caso de "debe ser absolutamente encontrado", usaría assertion. +1 para el último párrafo. –

+1

"no puede continuar con la ejecución del código si se producen". Usted puede. Id. La conexión de DB se pierde. El programa puede continuar y le pedirá al usuario que espere la reconexión. –

+0

Mykola, sí, el código de USUARIO será, no el código db. por lo tanto, excepciones. –

0

Desde una perspectiva puramente de diseño, prefiero arrojar excepciones cuando sucede algo "excepcional", p. la clave que pediste no fue encontrada. La razón principal es que hace la vida más fácil para el consumidor: no tienen que verificar el valor después de cada llamada a su función. También me gustan las funciones TrySomething, porque luego el usuario puede probar explícitamente si la operación fue exitosa. Desafortunadamente las excepciones en .Net son bastante caras (desde mi experiencia normalmente tardan unos 50 ms en arrojar una), así que desde la perspectiva práctica puede devolver uno de estos valores predeterminados en lugar de arrojar una excepción, especialmente en la programación GUI .

+0

¿Es esta su experiencia con el código compilado en modo de depuración o de producción? Hay una GRAN diferencia en el costo entre los dos modos. En modo de producción, el costo es muy pequeño. –

+0

Parecía comportarse de manera similar. Obtuve los 50 ms de los archivos de registro de la herramienta prod. Sin embargo, fue .Net 2. – Grzenio

0

Creo que es ligeramente situacional, pero se rige más por si el evento de devolución es en realidad una excepción lógica o no. El ejemplo de indexOf es una respuesta de salida perfectamente normal donde -1 es lo único que tiene sentido (o nulo para los tipos de referencia).

Una excepción debería ser exactamente eso: excepcional, lo que significa que desea todo el seguimiento y manejo de la pila que puede obtener de una excepción real.

+0

Lanzar una excepción de indexOf es más lógico que -1. Pero está mal porque hace que el código que utiliza indexOf sea más difícil de escribir. No busque la "teoría de excepciones", mire el código que va a usar la función. –

+0

Qué puedo decir, que no estoy de acuerdo - indexOf devuelve donde se encontró algo, no se encuentra es excepcional – annakata

3

En los casos que mencionaste preferiría el valor de retorno ya que sé exactamente qué hacer con eso. Y no hay razón para molestarse con atrapar en el mismo ámbito.

Por supuesto, si su lógica está construida de manera que los valores siempre deben estar en la lista y su ausencia es el error de lógica del programador, las excepciones son la manera.

1

Viniendo de un fondo de Java, prefiero las excepciones en la mayoría de los casos. Hace que su código sea mucho más limpio cuando la mitad no se gasta en comprobar los valores devueltos.

Dicho esto, también depende de la frecuencia con la que es probable que algo "falle". Las excepciones pueden ser costosas, por lo que no desea tirarlas innecesariamente por cosas que con frecuencia fallarán.

+0

también, las excepciones en todas partes hacen que el código sea más confuso ya que es necesario dispersar try/catch en todas partes para mantener el flujo de su programa. Los métodos pequeños ayudan, pero aún tienes mucho azúcar para probar/capturar en todas partes. Por lo tanto, diseñe sus excepciones para que no sean lanzadas todo el tiempo. – gbjbaanb

-1

Una excepción debería ser algo "excepcional". Entonces, si llamas a Find, y esperas encontrar algo, no importa qué, entonces lanzar una excepción si no encuentras algo es un buen comportamiento.

Si, sin embargo, es el flujo normal, que a veces no encuentras algo, entonces lanzar una excepción es malo.

1

Más eficaz C# por Bill Wagner hizo una excelente recomendación en lo que respecta a las excepciones. La idea es seguir adelante y lanzar una excepción al realizar una operación, solo asegúrese de proporcionar ganchos para verificar si el valor generará una excepción.

Por ejemplo

// if this throws an exception 
list.GetByName("Frank") // throws NotFound exception 
// provide a method to test 
list.TryGetByName("Frank") // returns false 

esa manera se puede optar por la excepción escribiendo algo así como

MyItem item; 
if (list.TryGetByName("Frank")) 
    item = list.GetByName("Frank"); 
+0

Gracias, pero ¿no sería 2 veces más lento? –

+0

Buena sugerencia. De esta manera, la elección de la llamada que utiliza también transmite la intención. Si llama al Try ..., sugiere a los futuros lectores de su código que * sabe * que podría fallar. Si llama a la línea recta Obtener, sugiere que a priori espere que siempre haya un resultado. – JeffH

+0

@Joan - tienes razón sobre la velocidad. Una forma de evitarlo es hacer que el retorno sea el código de resultado y tener un argumento de salida en el que se presente la respuesta. Verifica el código de retorno y, si está bien, usa la respuesta. Consulte http://msdn.microsoft.com/en-us/library/system.int32.tryparse.aspx como un ejemplo. – JeffH

6

Una regla de oro es usar excepciones sólo cuando sucede algo que "no debe ocurrir".

Si esperaría que una llamada a IndexOf() no encontrara el valor en cuestión (una expectativa razonable), entonces debería tener un código de retorno para eso (probablemente -1 como usted dice). Algo que nunca debería fallar, como la asignación de memoria, debería arrojar una excepción en un caso de falla.

Otra cosa para recordar es que el manejo de excepciones es "costoso" en términos de rendimiento. Por lo tanto, si su código maneja regularmente las excepciones como parte de las operaciones normales, no funcionará tan rápido como podría.

+0

Para mis casos de uso, la mayoría de las veces IndexOf no debería devolver -1. Entonces, ¿debería ser entonces una excepción? No, "no debería suceder" es un mal criterio para las excepciones. Debería ser "lo que hace que el código del usuario sea más fácil". –

3

se puede disfrutar de mi serie del blog de dos partes que se analiza una gran cantidad de ventajas y desventajas aquí dependiendo de qué características admite el lenguaje de programación, ya que parece bastante relevante:

An example of the interplay between language features and library design, part one

An example of the interplay between language features and library design, part two

Agregaré que creo que muchas de las respuestas a esta pregunta son deficientes (he votado negativamente a muchas de mis cohortes). Especialmente malo son las API a lo largo de las líneas de

if (ItWillSucceed(...)) { 
    DoIt(...) 
} 

que crean todo tipo de problemas infeliz (ver mi blog para más detalles).

1

Como en muchas cuestiones relacionadas con la programación, todo depende ...

Me parece que uno realmente debe primero tratar de definir su API casos excepcionales por lo que no pueden suceder en el primer lugar.

Usar Design By Contract puede ayudar a hacer esto. Aquí uno insertaría funciones que generan un error o falla e indican un error de programación (no error del usuario). (En algunos casos, estas comprobaciones se eliminan en el modo de lanzamiento).

Luego, mantenga excepciones para fallas genéricas que no se pueden evitar, como la conexión de DB falló, la transacción optimista falló, la escritura de disco falló.

Por lo general, no es necesario capturar estas excepciones hasta que lleguen al "usuario". Y provocará que el usuario tenga que volver a intentarlo.

Si el error es un error del usuario como un error tipográfico en un nombre o algo, entonces trate eso directamente en el código de la interfaz de la aplicación. Dado que este es un error común, necesitaría ser manejado con un mensaje de error amigable al usuario potencialmente traducido, etc.

La aplicación de capas también es útil aquí. Así que vamos a tomar el ejemplo de traslación de efectivo de una cuenta de una otra cuenta:

transferInternal(int account_id_1, int account_id_2, double amount) 
{ 
    // This is an internal function we require the client to provide us with 
    // valid arguments only. (No error handling done here.) 
    REQUIRE(accountExists(account_id_1)); // Design by contract argument checks. 
    REQUIRE(accountExists(account_id_2)); 
    REQUIRE(balance(account_id_1) > amount); 
    ... do the actual transfer work 
} 

string transfer(int account_id_1, int account_id_2, double amount) 
{ 
    DB.start(); // start transaction 
    string msg; 
    if (!checkAccount(account_id_1, msg)) return msg; // common checking code used from multiple operations. 
    if (!checkAccount(account_id_2, msg)) return msg; 
    if (!checkBalance(account_id_1, amount)) return msg; 
    transferInternal(account_id_1, account_id_2, amount); 
    DB.commit(); // This could fail with an exception if someone else changed the balance while this transaction was active. (Very unlikely but possible) 
    return "cash transfer ok"; 
} 
11

He leído en alguna parte una buena regla sobre esto que me gusta mucho. Dice: "una función debe arrojar una excepción si y solo si no puede realizar la tarea para la que estaba destinado".

Entonces, lo que suelo hacer es decidir qué debe hacer una función (que generalmente proviene de requisitos comerciales o algo así), y luego lanzar excepciones para todo lo demás.

Si ha diseñado bien su aplicación, sus funciones serán bastante pequeñas y realizará tareas relativamente simples con valores de retorno simples. Decidir sobre excepciones por la regla anterior no será difícil.

Por supuesto, siempre hay casos ambiguos (como con una clave que no se encuentra en un diccionario). Esos deben ser lejanos e intermedios, pero allí solo tendrá que usar su intuición sobre cuál es la solución más elegante.

Ah, y con todo esto, nunca lo olvides: para que esto funcione bien, solo puedes atrapar excepciones que puedes procesar. Sobre todo, eso significa que los atrapará solo en los niveles superiores de UI, donde puede mostrarlos al usuario y registrarlos o algo así. Los niveles inferiores pueden usar bloques finally o volver a lanzar excepciones después de algún procesamiento propio, pero las excepciones capturadas genuinamente en niveles bajos generalmente indican un diseño incorrecto.

+0

Depende, la captura de excepciones de niveles inferiores generalmente no es un mal diseño. Debería envolverlos en otras excepciones y pasarlos a la interfaz de usuario, con un mensaje más amigable. – Martin

+0

Hmm, sí, no había pensado en eso. –

+0

Tengo un sitio web donde un usuario puede recortar y cargar una imagen de perfil. Si el usuario carga una imagen no válida, ¿sería una buena práctica arrojar (y atrapar) una InvalidImageException? –

3

¿Qué prefieres usar?

A:

item = list.Find(x); 

B:

If (list.Contains(x)) 
    item = list.Find(x); 
else 
    item = null; 

C:

try { 
    item = list.Find(x); 
} 
catch { 
    item = null; 
} 

estoy dispuesto a apostar que la respuesta es A. Por lo tanto, en este caso de regresar por defecto (T) es la elección correcta.

+0

La devolución por defecto (T) implica que nunca devolverá el valor predeterminado (T) en la operación 'normal'. Si bien esto puede justificarse para los tipos de referencia, no es fácil para los tipos de valores porque, por ejemplo, cero es un resultado común. Puedes elegir un número mágico pero eso tampoco es realmente inteligente. –

+0

Nullable (T) puede ser la salida. Por supuesto, la verificación previa solo debería hacerse si controlas el objetivo y no hay riesgo de introducir condiciones de carrera. En estos casos, me gusta la composibilidad que se pierde con la solución de parámetro out. –

+0

No estoy en desacuerdo con usted en ningún punto. –

2

Los mejores idiomas le permiten hacer lo que se ajuste a sus necesidades. Al igual que en Smalltalk-80:

A continuación se lanzará una excepción si no hay usuario para el id:

user := userDictionary at: id 

Éste evaluará el bloque dado que es una función de alto nivel:

user := userDictionary at: id ifAbsent: [ 
    "no such id, let's return the user named Anonymous" 
    users detect: [ :each | each name = 'Anonymous' ] ] 

Tenga en cuenta que el método real es: ifAbsent :.

0

Para el primero, que no me gusta mucho la opción default(T): ¿y si usted tiene una lista de int donde 0 (presumiblemente esto es por defecto int 's; yo no uso C#) es un valor perfectamente permisible?

(Disculpas por la sintaxis de Java, pero si trataba de adivinar C# probablemente me hacen un lío de ella en alguna parte :))

class Maybe<T> { 
    public Maybe() { 
     set = false; 
     value = null; 
    } 

    public Maybe(T value) { 
     set = true; 
     this.value = value; 
    } 

    public T get(T defaultVal) { 
     if (set) 
      return value; 
     return defaultVal; 
    } 

    private boolean set; 
    private T value; 
} 

Luego, por supuesto Find devolvería un Maybe<T>, y la persona que llama elige algún valor que sea un valor predeterminado en este contexto. Quizás podría agregar getDefault como un método conveniente para cuando default(T)es la respuesta correcta, por supuesto.

Para IndexOf, -1 es un valor razonable para esto, ya que los valores válidos obviamente siempre son> = 0, así que simplemente lo haría.

0

Lo que suelo hacer en este caso es definir una clase de opción (inspirada en F #) y extender IEnumerable con un método TryFind() que devuelve la clase Option.

public class Option<T> 
{ 
    string _msg = ""; 
    T _item; 

    public bool IsNone 
    { 
     get { return _msg != "" ? true : false; } 
    } 

    public string Msg 
    { 
     get { return _msg; } 
    } 

    internal Option(T item) 
    { 
     this._item = item; 
     this._msg = ""; 
    } 

    internal Option(string msg) 
    { 
     if (String.IsNullOrEmpty(msg)) 
      throw new ArgumentNullException("msg"); 

     this._msg = msg; 
    } 

    internal T Get() 
    { 
     if (this.IsNone) 
      throw new Exception("Cannot call Get on a NONE instance."); 

     return this._item; 
    } 

    public override string ToString() 
    { 
     if (this.IsNone) 
      return String.Format("IsNone : {0}, {1}", this.IsNone, typeof(T).Name); 

     else 
      return String.Format("IsNone : {0}, {1}, {2}", this.IsNone, typeof(T).Name, this._item.ToString()); 
    } 

}

A continuación, puede utilizarlo como

var optionItem = list.TryFind(x => trueorFalseTest()); 
if (!optionItem.IsNone) 
    var myItem = optionItem.Get(); 

De esta manera, si existe o no el artículo, la colección es atravesada sólo una vez.

Cuestiones relacionadas