Devo seguir o caminho normal ou falhar cedo?

73

Do livro Code Complete , vem a seguinte citação:

"Coloque o caso normal após o em ifvez de depois do else"

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

Mas o Programador Pragmático nos ensina a "travar cedo" (p. 120).

Qual regra devo seguir?

jao
fonte
15
@gnat não duplicado #
2020
16
os dois não são mutuamente exclusivos, não há ambiguidade.
Jwenting
6
Não tenho tanta certeza de que a citação de código completo seja um ótimo conselho. Presumivelmente, é uma tentativa de melhorar a legibilidade, mas certamente há situações em que você deseja que casos incomuns sejam primeiro textualmente. Observe que isso não é uma resposta; as respostas existentes já explicam bem a confusão de sua pergunta.
Eamon Nerbonne
14
Crash early, crash hard! Se um de seus iframos retornar, use-o primeiro. E evite o elserestante do código, você já retornou se as pré-condições falharem. Código é mais fácil de ler, menos recuo ...
CodeAngry
5
Sou eu quem acha que isso não tem nada a ver com legibilidade, mas provavelmente foi apenas uma tentativa equivocada de otimizar a previsão de ramificação estática?
Mehrdad 22/05

Respostas:

189

"Crash early" não se refere a qual linha de código vem anteriormente em texto. Ele diz para você detectar erros na etapa mais inicial 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 uma etapa "anterior" ou "posterior". Como encomendá-los é, portanto, uma questão de legibilidade, e "falhar cedo" não entra na decisão.

Kilian Foth
fonte
2
Seu ponto geral é ótimo, mas vejo um problema. Se você possui (if / else if / else), a expressão avaliada em "else if" é avaliada após a expressão na instrução "if". O ponto importante, como você aponta, é a legibilidade versus a unidade conceitual de processamento. Apenas um bloco de código é executado, mas várias condições podem ser avaliadas.
Encaitar
7
@ Encaitar, esse nível de granularidade é muito menor do que o geralmente pretendido quando a frase "falha cedo" é usada.
Riwalk # 20/14
2
@ Encaitar Essa é a arte da programação. Quando confrontada com várias verificações, a idéia é tentar a que mais provavelmente é verdadeira primeiro. No entanto, essas informações podem não ser conhecidas no momento do design, mas no estágio de otimização, mas tenha cuidado com a otimização prematura.
BPugh
Comentários justos. Esta foi uma boa resposta e eu só queria tentar ajudar torná-lo melhor para referência futura
Encaitar
Linguagens de script como JavaScript, Python, perl, PHP, bash etc. são exceções porque são interpretadas linearmente. Em pequenas if/elseconstruções, provavelmente não importa. Mas aqueles chamados em loop ou com muitas instruções em cada bloco podem executar mais rapidamente com a condição mais comum primeiro.
DocSalvager
116

Se a sua elsedeclaração contiver apenas 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

fazem isto

if not file.exists() :
  throw toysOutOfPram

if not validate(file) :
  throw foodAtMummy

# do stuff with file...

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

E, como todo mundo já afirmou, os dois conselhos não são contraditórios. Um é sobre ordem de execução , o outro é sobre ordem de código .

Jack Aidley
fonte
4
Vale ressaltar que o conselho para colocar um fluxo normal no bloco depois ife um fluxo excepcional no bloco depois elsenão se aplica se você não tiver um else! Declarações de guarda como essa são a forma preferida para lidar com condições de erro na maioria dos estilos de codificação.
Jules
+1 é um bom ponto e, na verdade, dá uma resposta para a verdadeira questão de como solicitar itens com condições de erro.
ashes999
Definitivamente muito mais claro e fácil de manter. É assim que eu gosto de fazer.
John
27

Você deve seguir os dois.

O conselho "Falha no início" / falha no início significa que você deve testar suas entradas quanto a possíveis erros o mais rápido possível.
Por exemplo, se seu método aceitar um tamanho ou contagem que deve ser positivo (> 0), o aviso antecipado de falha significa que você testa essa condição logo no início do método, em vez de esperar que o algoritmo produza bobagens. resultados.

O conselho para colocar o caso normal primeiro significa que, se você testar uma condição, o caminho mais provável deverá vir primeiro. Isso ajuda no desempenho (como a previsão de ramificação do processador estará correta 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.
Esse conselho realmente não se aplica quando você testa uma pré-condição e sai imediatamente (usando declarações ou if (!precondition) throwconstruções), porque não há manipulação de erro a ser ignorada durante a leitura do código.

Bart van Ingen Schenau
fonte
11
Você pode elaborar sobre a parte de previsão do ramo? Eu não esperaria que o código com maior probabilidade de ir para o caso if fosse executado mais rápido que o código com maior probabilidade de ir para o caso else. Quero dizer, esse é o ponto principal da previsão de ramificação, não é?
Roman Reiner
@ user136712: Nos processadores modernos (rápidos), as instruções são buscadas antes que a instrução anterior termine o processamento. A previsão de ramificação é usada para aumentar a probabilidade de que as instruções obtidas ao executar uma ramificação condicional também sejam as instruções corretas para executar.
Bart van Ingen Schenau
2
Eu sei o que é previsão de ramificação. Se eu li o seu post corretamente, você diz que if(cond){/*more likely code*/}else{/*less likely code*/}corre mais rápido do que if(!cond){/*less likely code*/}else{/*more likely code*/}por causa da previsão do ramo. Eu pensaria que a previsão de ramificação não é tendenciosa para ifa elsedeclaração ou a e apenas leva em consideração o histórico. Portanto, se elseé mais provável que isso aconteça, deve ser capaz de prever isso tão bem. Essa suposição é falsa?
Roman Reiner
18

Acho que o @JackAidley já disse o essencial , mas deixe-me formulá-lo assim:

sem exceções (por exemplo, C)

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

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

No caso de "erro de saída antecipada", seu código diz:

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

Se você detectar esse padrão - a returnem um else(ou mesmo if) bloco, refaça-o imediatamente para que o código em questão não tenha um elsebloco:

/* 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 seja profundo demais e atenda ao caso “sair cedo” (ajuda a manter a mente - e o fluxo do código - limpos) e não viola o “colocar a coisa mais provável na ifparte”, porque simplesmente não há elseparte .

C e limpeza

Inspirado por uma resposta em uma pergunta semelhante (que entendeu errado), veja 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 recolhê-los em um 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);
}

Esse uso de gotoé perfeitamente bom, se você puder lidar com isso; o conselho para não usar gotoé dirigido a pessoas que ainda não podem decidir por si mesmas 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, o que eu prefiro muito (posso usar o tratamento explícito de erros muito melhor e com muito menos surpresa). 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 ;)

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

retorno de erro em face de exceções

Você pode substituir a maioria dos primeiros returns com lançando uma exceção. No entanto , o fluxo normal do programa, ou seja, qualquer fluxo de código no qual o programa não tenha encontrado, bem, uma exceção ... uma condição de erro ou algo assim, não deve gerar 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 com que o Operations pareça estranho para você (“esses programadores Java ™ sempre nos dizem que essas exceções são normais”) e pode dificultar a depuração (por exemplo, diga ao IDE para interromper qualquer exceção). As exceções geralmente exigem que o ambiente de tempo de execução desenrole a pilha para produzir rastreios, etc. Há provavelmente mais razões contra isso.

Isso se resume a: em um idioma que suporta exceções, use o que corresponder à lógica e estilo existentes e parecer natural. Se escrever algo do zero, faça isso com antecedência. Se estiver escrevendo uma biblioteca do zero, pense nos seus consumidores. (Também nunca use abort()em uma biblioteca ...) Mas, faça o que fizer, como regra geral, não haverá uma exceção se a operação continuar (mais ou menos) normalmente após ela.

conselho geral wrt. Exceções

Tente obter todo o uso de exceções no programa acordado por toda a equipe de desenvolvedores primeiro. 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 o pensamento.

mirabilos
fonte
Em geral, vejo retornos iniciais como cheiro de código. Em vez disso, lançaria uma exceção, se o código a seguir falhasse porque uma pré-condição não foi atendida. Apenas sayin
DanMan
11
@ DanMan: meu artigo foi escrito com C em mente ... Eu normalmente não uso Exceções. Mas estendi o artigo com uma sugestão (opa, ficou um pouco longa) wrt. Exceções; aliás, tivemos a mesma pergunta na lista dev interna da empresa ontem ...
mirabilos
Além disso, use chaves mesmo em ifs e fors de uma linha. Você não quer outro goto fail;oculto na identificação.
Bruno Kim
11
@BrunoKim: isso depende totalmente do estilo / convenção de codificação do projeto com o qual você trabalha. Eu trabalho com o BSD, onde isso é mal visto (mais confusão óptica e perda de espaço vertical); no $ dayjob, no entanto, eu os coloco como combinamos (menos difícil para iniciantes, menor chance de erros, etc., como você disse).
mirabilos
3

Na minha opinião, 'Guard Condition' é uma das melhores e mais fáceis maneiras de tornar o código legível. Eu realmente odeio quando vejo ifno início do método e não vejo o elsecódigo porque 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 sempre faça uma varredura de cima para baixo.

Piotr Perak
fonte
2

(A resposta dos @mirabilos é excelente, mas eis como eu penso sobre a questão para chegar à mesma conclusão :)

Estou pensando em mim (ou em outra pessoa) lendo o código da minha função mais tarde. Quando leio a primeira linha, não posso fazer suposições sobre minha entrada (exceto aquelas que 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" - ou seja, matar os 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 condicionado, quero enfatizar que é 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 - restabelece Monica
fonte
-3

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

Em outras palavras:

A. seção crítica e sem padrões => Fail Early

B. seção e padrões não críticos => Usar padrões na outra parte

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

Nikos M.
fonte
Essa é apenas sua opinião ou você pode explicar / fazer backup de alguma forma?
gnat 23/05
como exatamente isso não é feito backup, como cada opção explica (sem muitas palavras) por que é usada?
Nikos M.
Eu não gostaria de dizer isso, mas os votos negativos (minha) nesta resposta estão fora de contexto :). este é o OP pergunta, se você tem resposta alternativa é outra questão
Nikos M.
Sinceramente, falho em ver a explicação aqui. Digamos, se alguém escrever outra opinião, como "seção crítica e nenhum padrão => não falhe cedo" , como essa resposta ajudaria o leitor a escolher duas opiniões opostas? Considere editá -lo em uma forma melhor, para se ajustar às diretrizes de Como responder .
gnat 23/05
Ok, entendo, isso realmente poderia ser outra explicação, mas pelo menos você entende a palavra "seção crítica" e "sem padrões", o que pode implicar uma estratégia para falhar mais cedo e isso é de fato uma expansão, embora minimalista
Nikos M.