Devo seguir o caminho normal ou falhar cedo?

73

Do livro Código concluído , vem a seguinte citação:

"Put the normal case after the if rather than after the else"

O que significa que exceções / desvios do caminho padrão devem ser colocados no caso else .

Mas O Programador Pragmático nos ensina a "bater cedo" (p. 120) .

Qual regra devo seguir?

    
por jao 20.05.2014 / 09:02
fonte

7 respostas

189

"Crash early" não é sobre qual linha de código vem anteriormente textualmente. Ele lhe diz para detectar erros na primeira etapa possível do processamento , para que você não tome decisões e cálculos inadvertidamente com base no estado já defeituoso.

Em uma construção if / else , apenas um dos blocos é executado, portanto, nenhum deles pode ser considerado como uma etapa "anterior" ou "posterior". Como encomendá-los é, portanto, uma questão de legibilidade, e "falhar cedo" não entra na decisão.

    
por 20.05.2014 / 09:19
fonte
116

Se a sua declaração else contiver apenas um código de falha, provavelmente não deverá estar lá.

Em vez de fazer isso:

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

faça isso

if not file.exists() :
  throw toysOutOfPram

if not validate(file) :
  throw foodAtMummy

# do stuff with file...

Você não deseja aninhar seu código profundamente apenas para incluir a verificação de erros.

E, como todos já disseram, os dois conselhos não são contraditórios. Uma é sobre ordem de execução , a outra é sobre ordem de código .

    
por 20.05.2014 / 16:21
fonte
27

Você deve seguir os dois.

O aviso "Cessar cedo / falhar cedo" significa que você deve testar suas entradas para possíveis erros o mais rápido possível.
Por exemplo, se seu método aceita um tamanho ou uma contagem supostamente positiva (> 0), o aviso de falha antecipada significa que você testa essa condição logo no início do método, em vez de esperar que o algoritmo produza resultados sem sentido.

O conselho para colocar o caso normal em primeiro lugar significa que, se você testar uma condição, o caminho mais provável deve vir primeiro. Isso ajuda no desempenho (como a predição da ramificação do processador estará certa com mais frequência) e na legibilidade, porque você não precisa pular blocos de código ao tentar descobrir o que a função está fazendo no caso normal.
Este conselho não se aplica realmente quando você testa uma pré-condição e imediatamente salva (usando asserts ou if (!precondition) throw constructs), porque não há manipulação de erro para pular durante a leitura do código.

    
por 20.05.2014 / 09:23
fonte
18

Eu acho que o @JackAidley disse a essência dele , mas deixe-me formular assim:

sem exceções (por exemplo, C)

No fluxo de código normal, você tem:

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

No caso "error out early", seu código lê de repente:

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

Se você identificar esse padrão, um return em um bloco else (ou mesmo if ), refazer o código imediatamente para que o código em questão não tenha um bloco else :

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

No mundo real ...

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

Isso evita que o aninhamento muito profundo e atenda ao caso "break out early" (ajuda a manter a mente - e o fluxo de código - limpo) e não viola o "coloque a coisa mais provável na parte if " porque simplesmente não há else part.

C e limpeza

Inspirado por uma resposta em uma pergunta semelhante (que deu errado), aqui está como você faz a limpeza com C. Você pode usar um ou dois pontos de saída, aqui está um para dois pontos de saída:

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);
}

Você pode colapsá-los em um único ponto de saída se houver menos limpeza a fazer:

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á perfeitamente bem, se você puder lidar com isso; o conselho para ficar de fora usando goto é direcionado a pessoas que ainda não podem decidir sozinhas se um uso é bom, aceitável, ruim, código de espaguete ou qualquer outra coisa.

Exceções

O texto acima fala sobre idiomas sem exceções, que eu prefiro muito a mim mesmo (eu posso usar o erro explícito para lidar com muito melhor e com muito menos surpresa). Para citar o 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 ;)

Mas aqui está uma sugestão de como você faz isso em um idioma com exceções e quando deseja usá-las bem:

Retorno de erro

em face de exceções

Você pode substituir a maior parte dos primeiros return s com uma exceção. No entanto , seu fluxo de programa normal , ou seja, qualquer fluxo de código no qual o programa não encontrou, bem, uma exceção… uma condição de erro ou algo assim, não levantar quaisquer exceções.

Isso 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á bem, mas…

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

… não é. Basicamente, uma exceção não é um elemento de fluxo de controle . Isso também faz as Operações parecerem estranhas para você (“os programadores Java ™ sempre nos dizem que essas exceções são normais”) e podem impedir a depuração (por exemplo, dizer ao IDE para quebrar apenas qualquer exceção). As exceções geralmente exigem que o ambiente de tempo de execução libere a pilha para produzir tracebacks, etc. Provavelmente há mais motivos para isso.

Isso se resume a: em uma linguagem que aceita exceções, use o que corresponder à lógica e ao estilo existentes e pareça natural. Se estiver escrevendo algo do zero, aceite isso com antecedência. Se estiver escrevendo uma biblioteca do zero, pense nos seus consumidores. (Nunca use abort() em uma biblioteca ...) Mas, faça o que fizer, como regra geral, uma exceção será lançada se a operação continuar (mais ou menos) normalmente depois dela.

conselho geral. Exceções

Tente obter todas as exceções acordadas por toda a equipe de desenvolvedores primeiro no programa. Basicamente, planeje-os. Não os use em abundância. Às vezes, mesmo em C ++, Java ™, Python, um retorno de erro é melhor. Às vezes não é; use-os com pensamento.

    
por 21.05.2014 / 09:51
fonte
3

Na minha opinião, 'Condição de guarda' é uma das melhores e mais fáceis maneiras de tornar o código legível. Eu realmente odeio quando vejo if no início do método e não vejo o código else porque ele está fora da tela. Eu tenho que rolar para baixo apenas para ver throw new Exception .

Coloque as verificações no início para que a pessoa que está lendo o código não precise pular todo o método para lê-lo, mas, em vez disso, sempre escaneie-o de cima para baixo.

    
por 23.05.2014 / 11:30
fonte
2

(@ mirabilos ' resposta é excelente, mas eis como penso na questão de chegar à mesma conclusão :)

Estou pensando em mim mesmo (ou em outra pessoa) lendo o código da minha função mais tarde. Quando eu leio a primeira linha, não posso fazer nenhuma suposição sobre minha entrada (exceto aquelas que eu não verificarei de qualquer maneira). Então, meu pensamento é "Ok, eu sei que vou fazer as coisas com meus argumentos. Mas primeiro vamos limpá-los" - isto é, matar caminhos de controle nos quais eles não são do meu agrado. "Mas ao mesmo tempo , Não vejo o caso normal como algo que é condicionado, quero enfatizar que ele é 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. */
}
    
por 24.05.2014 / 20:32
fonte
-3

Esse tipo de ordenação de condição depende da criticidade da seção de código em questão e se há padrões que podem ser usados.

Em outras palavras:

seção crítica e sem padrões = > Falha no início

seção não crítica e padrões = > Use padrões na parte else

C. entre casos = > decidir por caso, conforme necessário

    
por 23.05.2014 / 13:17
fonte

Tags