¿Debo seguir la ruta normal o fallar antes de tiempo?

73

Del libro Code Complete viene la siguiente cita:

  

"Coloque el caso normal después de if en lugar de después de else "

Lo que significa que las excepciones / desviaciones de la ruta estándar se deben colocar en el caso else .

Pero El programador pragmático nos enseña a "fallar temprano" (pág. 120) .

¿Qué regla debo seguir?

    
pregunta jao 20.05.2014 - 09:02

7 respuestas

189

"Crash early" no se trata de qué línea de código aparece antes textualmente. Le indica que detecte los errores en el paso más temprano posible de procesamiento , para que no tome decisiones y cálculos sin darse cuenta basándose en un estado ya defectuoso.

En una construcción if / else , solo se ejecuta uno de los bloques, por lo que no se puede decir que ninguno constituya un paso "anterior" o "posterior". La forma de ordenarlos es, por lo tanto, una cuestión de legibilidad, y "fallar antes" no entra en la decisión.

    
respondido por el Kilian Foth 20.05.2014 - 09:19
116

Si su declaración else solo contiene un código de falla, lo más probable es que no esté allí.

En lugar de hacer esto:

if file.exists() :
  if validate(file) :
    # do stuff with file...
  else :
    throw foodAtMummy
else :
  throw toysOutOfPram

haz esto

if not file.exists() :
  throw toysOutOfPram

if not validate(file) :
  throw foodAtMummy

# do stuff with file...

No desea anidar profundamente su código simplemente para incluir la comprobación de errores.

Y, como ya han dicho todos los demás, los dos consejos no son contradictorios. Una es sobre orden de ejecución , la otra es sobre orden de código .

    
respondido por el Jack Aidley 20.05.2014 - 16:21
27

Deberías seguirlos a ambos.

El aviso de "Fallo temprano" / Fallo temprano significa que debe probar sus entradas para detectar posibles errores tan pronto como sea posible.
Por ejemplo, si su método acepta un tamaño o un recuento que se supone que es positivo (> 0), entonces el aviso de falla anticipada significa que usted prueba esa condición justo al comienzo de su método en lugar de esperar a que el algoritmo produzca resultados sin sentido.

El consejo para poner primero el caso normal significa que si prueba una condición, el camino más probable debería ser lo primero. Esto ayuda en el rendimiento (ya que la predicción de bifurcación del procesador será más a menudo) y en la legibilidad, porque no tiene que saltearse bloques de código al tratar de averiguar qué está haciendo la función en el caso normal.
Este consejo no se aplica realmente cuando se prueba una condición previa y se rescata inmediatamente (mediante el uso de aserciones o construcciones if (!precondition) throw ), porque no hay un manejo de errores que se pueda omitir al leer el código.

    
respondido por el Bart van Ingen Schenau 20.05.2014 - 09:23
18

Creo que @JackAidley dijo lo esencial de esto , pero permítame formularlo así:

sin excepciones (por ejemplo, C)

En el flujo de código normal, tienes:

if (condition) {
    statement;
} else if (less_likely_condition) {
    less_likely_statement;
} else {
    least_likely_statement;
}
more_statements;

En el caso de "error out early", su código se lee de repente:

/* demonstration example, do NOT code like this */
if (condition) {
    statement;
} else {
    error_handling;
    return;
}

Si ve este patrón: un return en un bloque else (o incluso if ), vuelva a trabajar de inmediato para que el código en cuestión no tenga un bloque else :

/* only code like this at University, to please structured programming professors */
function foo {
    if (condition) {
        lots_of_statements;
    }
    return;
}

En el mundo real ...

/* code like this instead */
if (!condition) {
    error_handling;
    return;
}
lots_of_statements;

Esto evita el anidamiento demasiado profundo y cumple con el caso de "romperse temprano" (ayuda a mantener la mente, y el flujo de código, limpio) y no viola el "poner lo más probable en la parte if " porque simplemente no hay parte else .

C y limpieza

Inspirado por una respuesta a una pregunta similar (que se equivocó), así es como se hace la limpieza con C. Puedes usar uno o dos puntos de salida allí, aquí hay uno para dos puntos de salida:

struct foo *
alloc_and_init(size_t arg1, int arg2)
{
    struct foo *res;

    if (!(res = calloc(sizeof(struct foo), 1)))
        return (NULL);

    if (foo_init1(res, arg1))
        goto err;
    res.arg1_inited = true;
    if (foo_init2(&(res->blah), arg2))
        goto err;
    foo_init_complete(res);
    return (res);

 err:
    /* safe because we use calloc and false == 0 */
    if (res.arg1_inited)
        foo_dispose1(res);
    free(res);
    return (NULL);
}

Puede contraerlos en un punto de salida si hay menos limpieza que hacer:

char *
NULL_safe_strdup(const char *arg)
{
    char *res = NULL;

    if (arg == NULL)
        goto out;

    /* imagine more lines here */
    res = strdup(arg);

 out:
    return (res);
}

Este uso de goto está perfectamente bien, si puedes manejarlo; El consejo de no usar goto está dirigido a personas que aún no pueden decidir por sí mismas si un uso es bueno, aceptable, malo, código de espagueti u otra cosa.

Excepciones

Lo anterior habla de idiomas sin excepciones, que yo prefiero (puedo usar el manejo explícito de errores mucho mejor y con mucho menos sorpresa). Para citar igli:

<igli> exceptions: a truly awful implementation of quite a nice idea.
<igli> just about the worst way you could do something like that, afaic.
<igli> it's like anti-design.
<mirabilos> that too… may I quote you on that?
<igli> sure, tho i doubt anyone will listen ;)

Pero aquí hay una sugerencia sobre cómo hacerlo bien en un idioma con excepciones, y cuándo quiere usarlas bien:

devolución de errores ante las excepciones

Puedes reemplazar la mayoría de los primeros return s con lanzar una excepción. Sin embargo, , su flujo de programa normal , es decir, cualquier flujo de código en el que el programa no haya encontrado, bueno, una excepción ... una condición de error o algo así, no deberá plantea cualquier excepción.

Esto significa que ...

# this page is only available to logged-in users
if not isLoggedIn():
    # this is Python 2.5 style; insert your favourite raise/throw here
    raise "eh?"

... está bien, pero ...

/* do not code like this! */
try {
    openFile(xyz, "rw");
} catch (LockedException e) {
    return "file is locked";
}
closeFile(xyz);
return "file is not locked";

... no lo es. Básicamente, una excepción no es un elemento de flujo de control . Esto también hace que las Operaciones se vean raras para usted ("esos programadores de Java ™ siempre nos dicen que estas excepciones son normales") y pueden dificultar la depuración (por ejemplo, decirle al IDE que solo debe interrumpir cualquier excepción). Las excepciones a menudo requieren que el entorno de tiempo de ejecución desenrolle la pila para producir rastreos, etc. Probablemente haya más razones para ello.

Esto se reduce a: en un lenguaje que admite excepciones, use lo que coincida con la lógica y el estilo existentes y se sienta natural. Si escribes algo desde cero, haz que esto se acuerde pronto. Si escribe una biblioteca desde cero, piense en sus consumidores. (No, nunca, use abort() en una biblioteca tampoco ...) Pero haga lo que haga, como regla general, no se lance una excepción si la operación continúa (más o menos) normalmente después de ella.

consejo general wrt. Excepciones

Intente obtener todo el uso de Excepciones acordado en el programa primero por todo el equipo de desarrollo. Básicamente, planificarlos. No los uses en abundancia. A veces, incluso en C ++, Java ™, Python, un retorno de error es mejor. A veces no lo es; Úsalos con el pensamiento.

    
respondido por el mirabilos 21.05.2014 - 09:51
3

En mi opinión, 'Condición de guardia' es una de las formas mejores y más fáciles de hacer que el código sea legible. Realmente odio cuando veo if al comienzo del método y no veo el código else porque está fuera de la pantalla. Tengo que desplazarme hacia abajo solo para ver throw new Exception .

Coloque los controles al principio para que la persona que lee el código no tenga que saltar todo el método para leerlo, sino que siempre lo escanee de arriba a abajo.

    
respondido por el Piotr Perak 23.05.2014 - 11:30
2

(@mirabilos ' respuesta es excelente, pero así es como pienso sobre la pregunta para llegar a la misma conclusión. :)

Estoy pensando en mí mismo (o en otra persona) leyendo el código de mi función más adelante. Cuando leo la primera línea, no puedo hacer suposiciones sobre mi entrada (excepto las que no revisaré de todos modos). Así que mi pensamiento es "Ok, sé que voy a estar haciendo cosas con mis argumentos. Pero primero limpiemos", es decir, elimine las rutas de control en las que no son de mi agrado ". Pero al mismo tiempo , No veo el caso normal como algo que está condicionado; quiero enfatizar que es normal. Por lo tanto,

int foo(int* bar, int baz) {

   if (bar == NULL) /* I don't like you, leave me alone */;
   if (baz < 0) /* go away */;

   /* there, now I can do the work I came into this function to do,
      and I can safely forget about those if's above and make all 
      the assumptions I like. */

   /* etc. */
}
    
respondido por el einpoklum 24.05.2014 - 20:32
-3

Este tipo de orden de condición depende de la criticidad de la sección de código en cuestión y si hay valores predeterminados que se pueden usar.

En otras palabras:

A. sección crítica y sin valores predeterminados = > Fallo temprano

B. sección no crítica y valores predeterminados = > Utilice los valores predeterminados en else parte

C. entre los casos = > Decida por caso según sea necesario

    
respondido por el Nikos M. 23.05.2014 - 13:17

Lea otras preguntas en las etiquetas