¿Debo seguir el camino normal o fallar temprano?

73

Del libro Code Complete viene la siguiente cita:

"Poner el caso normal después del en iflugar de después del else"

Lo que significa que las excepciones / desviaciones de la ruta estándar deben colocarse en el elsecaso.

Pero The Pragmatic Programmer nos enseña a "chocar temprano" (p. 120).

¿Qué regla debo seguir?

jao
fuente
15
@gnat no duplicado
B 20овић
16
los dos no son mutuamente excluyentes, no hay ambigüedad.
Jwenting
66
No estoy tan seguro de que la cita de código completo sea un gran consejo. Presumiblemente es un intento de mejorar la legibilidad, pero ciertamente hay situaciones en las que desearía que los casos poco comunes fueran primero textualmente. Tenga en cuenta que esto no es una respuesta; Las respuestas existentes ya explican la confusión de su pregunta.
Eamon Nerbonne
14
¡Choca temprano, choca duro! Si una de tus iframas regresa, úsala primero. Y evite el elseresto del código, ya ha regresado si fallaron las condiciones previas. El código es más fácil de leer, menos sangría ...
CodeAngry
55
¿Soy solo yo quien piensa que esto no tiene nada que ver con la legibilidad, sino que probablemente fue solo un intento equivocado de optimización para la predicción de rama estática?
Mehrdad

Respuestas:

189

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

En una if/ elseconstructo, sólo uno de los bloques se ejecuta, por lo que tampoco se puede decir que constituye una o etapa "más adelante" "anterior". Por lo tanto, cómo ordenarlos es una cuestión de legibilidad, y "fallar temprano" no entra en la decisión.

Kilian Foth
fuente
2
Su punto general es genial, pero veo un problema. Si tiene (if / else if / else), la expresión evaluada en "else if" se evalúa después de la expresión en la instrucción "if". Lo importante, como usted señala, es la legibilidad frente a la unidad conceptual de procesamiento. Solo se ejecuta un bloque de código, pero se pueden evaluar varias condiciones.
Encaitar
77
@Encaitar, ese nivel de granularidad es mucho más pequeño de lo que generalmente se pretende cuando se usa la frase "fallar temprano".
Riwalk
2
@Encaitar Ese es el arte de la programación. Cuando se enfrentan a múltiples comprobaciones, la idea es probar primero la más probable. Sin embargo, es posible que esa información no se conozca en el momento del diseño, sino en la etapa de optimización, pero tenga cuidado con la optimización prematura.
BPugh
Comentarios justos. Esta fue una buena respuesta y solo quería intentar ayudar a mejorarla para futuras referencias
Encaitar
Los lenguajes de secuencias de comandos como JavaScript, Python, perl, PHP, bash, etc. son excepciones porque se interpretan linealmente. En if/elseconstrucciones pequeñas , probablemente no importa. Pero los llamados en bucle o con muchas declaraciones en cada bloque podrían ejecutarse más rápido con la condición más común primero.
DocSalvager
116

Si su elseestado de cuenta contiene solo 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

hacer 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 todos los demás ya han dicho, los dos consejos no son contradictorios. Uno es sobre el orden de ejecución , el otro es sobre el orden del código .

Jack Aidley
fuente
44
Vale la pena ser explícito si el consejo de poner el flujo normal en el bloque después ify el flujo excepcional en el bloque después elseno se aplica si no tiene un else! Las declaraciones de protección como esta son la forma preferida para manejar condiciones de error en la mayoría de los estilos de codificación.
Julio
+1 este es un buen punto y en realidad da una respuesta a la pregunta real de cómo ordenar cosas con condiciones de error.
cenizas999
Definitivamente mucho más claro y fácil de mantener. Así es como me gusta hacerlo.
John
27

Deberías seguir a ambos.

El consejo "Crash early" / fail early significa que debe probar sus entradas para detectar posibles errores lo antes posible.
Por ejemplo, si su método acepta un tamaño o un recuento que se supone que es positivo (> 0), entonces el aviso inicial de falla significa que usted prueba esa condición justo al comienzo de su método en lugar de esperar a que el algoritmo produzca tonterías. resultados.

El consejo para poner el caso normal primero significa que si realiza una prueba para una condición, entonces el camino más probable debe venir primero. Esto ayuda en el rendimiento (ya que la predicción de bifurcación del procesador será correcta con más frecuencia) y la legibilidad, ya que no tiene que omitir bloques de código al intentar averiguar qué está haciendo la función en el caso normal.
Este consejo realmente no se aplica cuando se prueba una condición previa y se rescata de inmediato (mediante el uso de afirmaciones o if (!precondition) throwconstrucciones), porque no se puede omitir el manejo de errores al leer el código.

Bart van Ingen Schenau
fuente
1
¿Puedes dar más detalles sobre la parte de predicción de rama? No esperaría que el código que es más probable que vaya al caso if se ejecute más rápido que el código que es más probable que vaya al caso else. Quiero decir, ese es el punto central de la predicción de rama, ¿no?
Roman Reiner
@ user136712: en los procesadores modernos (rápidos), las instrucciones se obtienen antes de que la instrucción anterior haya finalizado el procesamiento. La predicción de rama se utiliza para aumentar la probabilidad de que las instrucciones obtenidas al ejecutar una rama condicional también sean las instrucciones correctas para ejecutar.
Bart van Ingen Schenau
2
Sé lo que es la predicción de rama. Si leo tu publicación correctamente, dices que se if(cond){/*more likely code*/}else{/*less likely code*/}ejecuta más rápido que if(!cond){/*less likely code*/}else{/*more likely code*/}debido a la predicción de rama. Creo que la predicción de rama no está sesgada ifni a la elsedeclaración ni a la declaración, y solo tiene en cuenta el historial. Entonces, si elsees más probable que suceda, debería ser capaz de predecir eso igual de bien. ¿Es esta suposición falsa?
Roman Reiner
18

Creo que @JackAidley ya dijo lo esencial , pero déjame formularlo así:

sin excepciones (por ejemplo, C)

En el flujo de código regular, tiene:

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

En el caso de "error fuera temprano", su código de repente dice:

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

Si ve este patrón, un returnen un bloque else(o incluso if), modifíquelo de inmediato para que el código en cuestión no tenga un elsebloque:

/* 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 el caso de "salir temprano" (ayuda a mantener limpia la mente y el flujo del código) y no viola el "poner lo más probable en la ifparte" porque simplemente no hay elseparte .

C y limpieza

Inspirado por una respuesta a una pregunta similar (que se equivocó), así es como se realiza la limpieza con C. Puede 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 gotoestá perfectamente bien, si puede manejarlo; El consejo para evitar el uso gotoestá dirigido a personas que aún no pueden decidir por sí mismas si un uso es bueno, aceptable, malo, con 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 mucha menos sorpresa). Para citar a 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 de cómo hacerlo bien en un idioma con excepciones, y cuándo quiere usarlos bien:

error de retorno ante excepciones

Puede reemplazar la mayoría de los primeros returns con una excepción. Sin embargo , el flujo normal de su programa, es decir, cualquier flujo de código en el que el programa no encontró, bueno, una excepción ... una condición de error o algo así, no generará ninguna 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 es. Básicamente, una excepción no es un elemento de flujo de control . Esto también hace que las operaciones te parezcan extrañas ("esos programadores de Java ™ siempre nos dicen que estas excepciones son normales") y pueden dificultar la depuración (por ejemplo, decirle al IDE que simplemente se salte cualquier excepción). Las excepciones a menudo requieren que el entorno de tiempo de ejecución desenrolle la pila para producir trazas, etc. Probablemente haya más razones en su contra.

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 escribe algo desde cero, acuerde esto pronto. Si escribe una biblioteca desde cero, piense en sus consumidores. (No, nunca, use abort()en una biblioteca tampoco ...) Pero haga lo que haga, por regla general, no haga que se lance una excepción si la operación continúa (más o menos) normalmente después de eso.

consejos generales wrt. Excepciones

Trate de obtener todo el uso del programa de Excepciones acordadas por todo el equipo de desarrollo primero. Básicamente, planifícalos. 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 pensamiento.

mirabilos
fuente
En general, veo los primeros retornos como código de olor. En cambio, lanzaría una excepción, si el siguiente código fallara porque no se cumplió una condición previa. Solo
digo
1
@DanMan: mi artículo fue escrito con C en mente ... Normalmente no uso Excepciones. Pero he extendido el artículo con una sugerencia (oops, se hizo bastante larga) wrt. Excepciones; por cierto, ayer tuvimos la misma pregunta en la lista de correo de desarrollo interno de la compañía ...
mirabilos
Además, use llaves incluso en ifs y fors de una línea. No quieres otro goto fail;escondido en la ideación.
Bruno Kim
1
@BrunoKim: esto depende completamente de la convención de estilo / codificación del proyecto con el que trabajas. Trabajo con BSD, donde esto está mal visto (más desorden óptico y pérdida de espacio vertical); en $ dayjob, sin embargo, los coloco según lo acordado (menos difícil para los novatos, menos posibilidades de errores, etc., como usted dijo).
mirabilos
3

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

Ponga los cheques 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.

Piotr Perak
fuente
2

(La respuesta de @mirabilos es excelente, pero así es como pienso en la pregunta para llegar a la misma conclusión :)

Estoy pensando en mí mismo (o en alguien más) leyendo el código de mi función más adelante. Cuando leo la primera línea, no puedo hacer ninguna suposición sobre mi entrada (excepto aquellas que no comprobaré de todos modos). Entonces mi pensamiento es "Ok, sé que voy a hacer cosas con mis argumentos. Pero primero vamos a" limpiarlos ", es decir, eliminar las rutas de control en las que no son de mi agrado". Pero al mismo tiempo , No veo el caso normal como algo condicionado; quiero enfatizar que es normal.

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. */
}
einpoklum - reinstalar a Monica
fuente
-3

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

En otras palabras:

A. sección crítica y sin valores predeterminados => Fail Early

B. sección y valores predeterminados no críticos => Usar valores predeterminados en otra parte

C. casos intermedios => decidir por caso según sea necesario

Nikos M.
fuente
¿Es esta simplemente su opinión o puede explicarla / respaldarla de alguna manera?
mosquito
¿Cómo es que esto no está respaldado, ya que cada opción explica (sin muchas palabras) por qué se usa?
Nikos M.
No me gustaría decir esto, pero los votos negativos en (mi) esta respuesta están fuera de contexto :). esta es la pregunta que se le hizo a OP, si tiene una respuesta alternativa es otra cuestión
Nikos M.
Sinceramente, no veo la explicación aquí. Digamos, si alguien escribe otra opinión, como "sección crítica y sin valores predeterminados => no falle temprano" , ¿cómo ayudaría esta respuesta al lector a elegir dos opiniones opuestas? Considere editarlo en una mejor forma, para ajustarse a las pautas de Cómo responder .
mosquito
ok, ya veo, esta podría ser otra explicación, pero al menos entiendes la palabra "sección crítica" y "no hay valores predeterminados", lo que puede implicar una estrategia para fracasar temprano y esta es una expansión, aunque minimalista
Nikos M.