Prática recomendada em caso de retorno

60

Quero saber o que é considerado a melhor maneira de retornar quando tenho uma ifdeclaração.

Exemplo 1:

public bool MyFunction()
{
   // Get some string for this example
   string myString = GetString();

   if (myString == null)
   {
      return false;
   }
   else
   {
      myString = "Name " + myString;
      // Do something more here...
      return true;
   }
}

Exemplo 2:

public bool MyFunction()
{
   // Get some string for this example
   string myString = GetString();

   if (myString == null)
   {
      return false;
   }

   myString = "Name " + myString;
   // Do something more here...
   return true;
}

Como você pode ver nos dois exemplos, a função retornará, true/falsemas é uma boa ideia colocar uma elsedeclaração como no primeiro exemplo ou é melhor não colocá-la?

sed
fonte
7
Se você estiver verificando apenas erros no primeiro 'se', é melhor não incluir 'else' porque os erros não devem ser considerados parte da lógica real.
Mert Akcakaya
2
pessoalmente, eu ficaria mais preocupado com a função causando efeitos colaterais, mas acho que esse é apenas um exemplo mal escolhido?
jk.
14
Para mim, a primeira versão é como descartando todos os alunos na sala que não terminou sua lição de casa, e, em seguida, uma vez que eles se foram, dizendo ao resto dos alunos "Agora, se você queria terminar sua lição de casa ...". Faz sentido, mas é desnecessário. Como o condicional não é mais um condicional, eu tendem a largar o else.
22712 Nicole
11
O que é o 'Faça algo mais aqui' que você deseja fazer? Isso pode mudar completamente a maneira como você projeta sua função.
Bento

Respostas:

81

O exemplo 2 é conhecido como bloco de proteção. É mais adequado retornar / lançar a exceção mais cedo se algo der errado (parâmetro errado ou estado inválido). No fluxo lógico normal, é melhor usar o Exemplo 1

Kemoda
fonte
+1 - uma boa distinção, e o que eu ia responder.
Telastyn
3
O @ pR0Ps tem a mesma resposta abaixo e fornece um exemplo de código do motivo pelo qual acaba sendo um caminho mais limpo a seguir. +1 para a boa resposta.
Esta pergunta foi feita muitas vezes no SO e, embora possa ser controversa, é uma boa resposta. Muitas pessoas que foram treinadas para retornar somente no final têm alguns problemas com esse conceito.
Bill K
2
+1. Evitar inadequadamente os bloqueios de proteção tende a causar o código de seta.
19712 Brian
Os dois exemplos não mostram bloqueio de guarda?
Ernie
44

Meu estilo pessoal é usar o single ifpara blocos de proteção e o if/ elseno código de processamento do método real.

Nesse caso, você está usando a myString == nullcondição de guarda, então eu tenderia a usar o ifpadrão único .

Considere o código um pouco mais complicado:

Exemplo 1:

public bool MyFunction(myString: string){

    //guard block
    if (myString == null){
        return false;
    }
    else{
        //processing block
        myString = escapedString(myString);

        if (myString == "foo"){
            //some processing here
            return false;
        }
        else{
            myString = "Name " + myString;
            //other stuff
            return true;
        }
    }
}

Exemplo 2:

public bool MyFunction(myString: string){

    //guard block
    if (myString == null){
        return false;
    }

    //processing block
    myString = escapedString(myString);

    if (myString == "foo"){
        //some processing here
        return false;
    }
    else{
        myString = "Name " + myString;
        //other stuff
        return true;
    }
}

No exemplo 1, a proteção e o restante do método estão no formato if/ else. Compare isso ao Exemplo 2, onde o bloco de proteção está no ifformato único , enquanto o restante do método usa o if/ elseformulário. Pessoalmente, acho o exemplo 2 mais fácil de entender, enquanto o exemplo 1 parece confuso e com recuo excessivo.

Observe que este é um exemplo artificial e que você pode usar else ifinstruções para limpá-lo, mas pretendo mostrar a diferença entre os blocos de proteção e o código de processamento da função real.

Um compilador decente deve gerar a mesma saída para os dois de qualquer maneira. O único motivo para usar um ou outro é a preferência pessoal ou a conformidade com o estilo do código existente.

pR0Ps
fonte
18
O exemplo 2 seria ainda mais fácil de ler se você se livrasse do segundo.
Briddums 19/07/12
18

pessoalmente, eu prefiro o segundo método. Sinto que é mais curto, tem menos recuo e é mais fácil de ler.

GSto
fonte
+1 para menos recuo. Este gest obvoious se você tiver várias verificações
d.raev 21/04
15

Minha prática pessoal é a seguinte:

  • Não gosto de funções com vários pontos de saída, achei difícil manter e seguir; as modificações de código às vezes quebram a lógica interna porque é inerentemente um pouco desleixada. Quando se trata de um cálculo complexo, crio um valor de retorno no início e o retorno no final. Isso me obriga a seguir cuidadosamente cada caminho de if-else, switch, etc., definir o valor corretamente nos locais adequados. Também passo um pouco de tempo decidindo se devo definir um valor de retorno padrão ou deixá-lo não inicializado no início. Esse método também ajuda quando a lógica ou o tipo ou significado do valor de retorno é alterado.

Por exemplo:

public bool myFunction()
{
   // First parameter loading
   String myString = getString();

   // Location of "quick exits", see the second example
   // ...

   // declaration of external resources that MUST be released whatever happens
   // ...

   // the return variable (should think about giving it a default value or not) 
   // if you have no default value, declare it final! You will get compiler 
   // error when you try to set it multiple times or leave uninitialized!
   bool didSomething = false;

   try {
     if (myString != null)
     {
       myString = "Name " + myString;
       // Do something more here...

       didSomething = true;
     } else {
       // get other parameters and data
       if ( other conditions apply ) {
         // do something else
         didSomething = true;
       }
     }

     // Edit: previously forgot the most important advantage of this version
     // *** HOUSEKEEPING!!! ***

   } finally {

     // this is the common place to release all resources, reset all state variables

     // Yes, if you use try-finally, you will get here from any internal returns too.
     // As I said, it is only my taste that I like to have one, straightforward path 
     // leading here, and this works even if you don't use the try-finally version.

   }

   return didSomething;
}
  • A única exceção: "saída rápida" no início (ou em casos raros, dentro do processo). Se a lógica de cálculo real não pode lidar com uma certa combinação de parâmetros de entrada e estados internos, ou tem uma solução fácil sem executar o algoritmo, não ajuda ter todo o código encapsulado em blocos (às vezes profundos). Este é um "estado excepcional", que não faz parte da lógica principal, portanto, devo sair do cálculo assim que detectar. Nesse caso, não há mais ramo, em condições normais a execução simplesmente continua. (É claro que "estado excepcional" é melhor expresso ao lançar uma exceção, mas às vezes é um exagero.)

Por exemplo:

public bool myFunction()
{
   String myString = getString();

   if (null == myString)
   {
     // there is nothing to do if myString is null
     return false;
   } 

   myString = "Name " + myString;
   // Do something more here...

   // not using return value variable now, because the operation is straightforward.
   // if the operation is complex, use the variable as the previous example.

   return true;
}

A regra "uma saída" também ajuda quando o cálculo requer recursos externos que você precisa liberar ou afirma que é necessário redefinir antes de sair da função; às vezes, são adicionados posteriormente durante o desenvolvimento. Com várias saídas dentro do algoritmo, é muito mais difícil estender todas as ramificações corretamente. (E, se houver exceções, a liberação / redefinição também deve ser bloqueada para evitar efeitos colaterais em casos excepcionais raros ...).

Seu caso parece se encaixar na categoria "saída rápida antes do trabalho real", e eu o escreveria como sua versão do Exemplo 2.

Lorand Kedves
fonte
9
+1 para "Eu não gosto de funções com vários pontos de saída"
Corv1nus 19/07/12
@LorandKedves - acrescentou um exemplo - espero que você não se importe.
22612 Matthew
@MatthewFlynn bem, se você não se importa que seja o oposto do que sugeri no final da minha resposta ;-) Continuo o exemplo, espero que finalmente seja bom para nós dois :-)
Lorand Kedves
@MatthewFlynn (. Desculpe, eu sou apenas um bastardo crabby, e obrigado por me ajudar a esclarecer a minha opinião, eu espero que você como a versão atual.)
Lorand Kedves
13
De funções com múltiplos pontos de saída e funções com uma variável de resultado mutável, a primeira me parece de longe o menor dos dois males.
21412 Jon Purdy
9

Prefiro empregar blocos de guarda sempre que possível por dois motivos:

  1. Eles permitem uma saída rápida, devido a alguma condição específica.
  2. Remove a necessidade de instruções if complexas e desnecessárias posteriormente no código.

De um modo geral, prefiro ver métodos em que a funcionalidade principal do método é clara e mínima. Blocos de proteção ajudam a fazer isso acontecer visualmente.

drekka
fonte
5

Gosto da abordagem "Fall Through":

public bool MyFunction()
{
   string myString = GetString();

   if (myString != null)
   {
     myString = "Name " + myString;
     return true;
    }
    return false;
}

A ação tem uma condição específica; qualquer outra coisa é apenas o retorno "fall through" padrão.

Noite escura
fonte
1

Se eu tiver uma condição única, não gastaria muito tempo pensando sobre o estilo. Mas se eu tiver várias condições de guarda, prefiro o style2

Picuture isso. Suponha que os testes sejam complexos e você realmente não deseja vinculá-los a uma única condição if-ORed para evitar a complexidade:

//Style1
if (this1 != Right)
{ 
    return;
}
else if(this2 != right2)
{
    return;
}
else if(this3 != right2)
{
    return;
}
else
{
    //everything is right
    //do something
    return;
}

versus

//Style 2
if (this1 != Right)
{ 
   return;
}
if(this2 != right2)
{
    return;
}
if(this3 != right2)
{
    return;
}


//everything is right
//do something
return;

Aqui existem duas vantagens principais

  1. Você está separando o código em uma única função em dois blocos visualmente logais: um bloco superior de validações (condições de guarda) e um bloco inferior de código executável.

  2. Se você precisar adicionar / remover uma condição, reduz as chances de estragar toda a escada if-elseif-else.

Outra pequena vantagem é que você tem um conjunto a menos de aparelhos para cuidar.

DPD
fonte
0

Isso deve ser uma questão de que soa melhor.

If condition
  do something
else
  do somethingelse

se expressa melhor então

if condition
  do something
do somethingelse

para métodos pequenos, não haverá muita diferença, mas para métodos compostos maiores, pode ser mais difícil de entender, pois não será adequadamente separado.

José Valente
fonte
3
Se um método é tão longo que a segunda forma é difícil de entender, é muito longo.
Kevin cline
7
Seus dois casos são equivalentes apenas se do somethingincluir um retorno. Caso contrário, o segundo código será executado do somethingelseindependentemente do ifpredicado, que não é como o primeiro bloco funciona.
Adnan
Eles também são o que o OP estava explicando em seus exemplos. Apenas seguindo a linha da pergunta
José Valente
0

Acho o exemplo 1 irritante, porque a declaração de retorno ausente no final de uma função de retorno de valor dispara imediatamente o sinalizador "Aguarde, há algo errado". Então, em casos como esse, eu iria com o exemplo 2.

No entanto, geralmente há mais envolvimento, dependendo do objetivo da função, por exemplo, tratamento de erros, registro etc. Então, eu estou com a resposta de Lorand Kedves sobre isso, e geralmente tenho um ponto de saída no final, mesmo com o custo de uma variável de flag adicional. Facilita a manutenção e as extensões posteriores na maioria dos casos.

Seguro
fonte
0

Quando sua ifinstrução sempre retorna, não há razão para usar uma outra coisa para o código restante na função. Fazer isso adiciona linhas extras e recuo extra. A adição de código desnecessário torna mais difícil a leitura e, como todos sabem, o código de leitura é difícil .

briddums
fonte
0

No seu exemplo, o elseobviamente é desnecessário, MAS ...

Ao percorrer rapidamente as linhas de código, os olhos normalmente olham para os aparelhos e os recuos para ter uma idéia do fluxo de código; antes que eles realmente se instalem no próprio código. Portanto, escrever elsee colocar chaves e recuo em torno do segundo bloco de código torna mais rápido para o leitor ver que essa é uma situação "A ou B", na qual um bloco ou outro será executado. Ou seja, o leitor vê as chaves e o recuo antes de ver o returndepois da inicial if.

Na minha opinião, este é um daqueles casos raros em que adicionar algo redundante ao código realmente o torna mais legível, e não menos.

Dawood ibn Kareem
fonte
0

Eu prefiro o exemplo 2 porque é imediatamente óbvio que a função retorna algo . Mas mais do que isso, eu preferiria retornar de um lugar, assim:

public bool MyFunction()
{
    bool result = false;

    string myString = GetString();

    if (myString != nil) {
        myString = "Name " + myString;

        result = true;
    }

    return result;
}

Com este estilo eu posso:

  1. Veja imediatamente que estou retornando algo.

  2. Capture o resultado de cada chamada da função com apenas um ponto de interrupção.

Caleb
fonte
Isso é semelhante a como eu abordaria o problema. A única diferença é que eu usaria a variável result para capturar a avaliação myString e usá-la como o valor de retorno.
Chuck Conway
@ChuckConway Concordou, mas eu estava tentando manter o protótipo do OP.
Caleb
0

Meu estilo pessoal tende a ir

function doQuery(string) {
    if (!query(string)) {
        query("ABORT");
        return false;
    } // else
    if(!anotherquery(string) {
        query("ABORT");
        return false;
    } // else
    return true;
}

Usando as elseinstruções comentadas para indicar o fluxo do programa e mantê-lo legível, mas evitando recuos massivos que podem chegar facilmente na tela se houver várias etapas envolvidas.

Shadur
fonte
11
Pessoalmente, espero nunca ter que trabalhar com seu código.
um CVn
Se você tiver várias verificações, esse método pode demorar um pouco. Retornar de cada cláusula if é semelhante ao uso da instrução goto para ignorar seções do código.
Chuck Conway
Se elsehouvesse uma escolha entre este e o código com as cláusulas incluídas, eu escolheria o último, porque o compilador também os ignorará. Embora isso dependa de else if não aumentar o recuo mais do que o original uma vez - se isso acontecer, eu concordo com você. Mas minha escolha seria abandonar elsecompletamente se esse estilo fosse permitido.
Mark-Hurd
0

eu escreveria

public bool SetUserId(int id)
{
   // Get user name from id
   string userName = GetNameById(id);

   if (userName != null)
   {
       // update local ID and userName
       _id = id;
       _userNameLabelText = "Name: " + userName;
   }

   return userName != null;
}

Eu prefiro esse estilo porque é mais óbvio que eu voltaria userName != null.

tia
fonte
11
A questão é por que você prefere esse estilo?
Caleb
... Francamente, não sei por que você quer a string neste caso, para não mencionar por que você está adicionando um teste booleano extra no final que realmente não precisa por qualquer motivo.
Shadur
@Shadur Estou ciente de testes booleanos extras e myStringresultados não utilizados . Acabei de copiar a função do OP. Vou mudar para algo que parece mais real. O teste booleano é trivial e não deve ser problema.
tia
11
@ Shahadur Não estamos fazendo otimização aqui, certo? Meu objetivo é tornar meu código fácil de ler e manter, e pessoalmente pagaria isso com o custo de uma verificação nula de referência.
tia
11
@ tia Eu gosto do espírito do seu código. Em vez de avaliar o userName duas vezes, eu capturaria a primeira avaliação em uma variável e a retornaria.
21712 Chuck Conway
-1

Minha regra de ouro é fazer um if-return sem outro (principalmente por erros) ou uma cadeia if-else-if completa sobre todas as possibilidades.

Dessa forma, evito tentar ser extravagante demais com minha ramificação, pois sempre que acabo fazendo isso codifico a lógica do aplicativo nos meus ifs, dificultando sua manutenção (por exemplo, se um enum está OK ou ERR e escrevo todos os meus ifs tirando proveito do fato de que! OK <-> ERR, torna-se uma chatice adicionar uma terceira opção ao enum da próxima vez.)

No seu caso, por exemplo, eu usaria um if simples, já que "return if null" certamente é um padrão comum e não é provável que você precise se preocupar com uma terceira possibilidade (além de null / not null) no futuro.

No entanto, se o teste fosse algo que fizesse uma inspeção mais aprofundada dos dados, eu iria errar no sentido de um if-else completo.

hugomg
fonte
-1

Penso que existem muitas armadilhas no Exemplo 2, que no futuro poderiam levar a códigos não intencionais. O primeiro do foco aqui é baseado na lógica em torno da variável 'myString'. Portanto, para ser explícito, todos os testes de condições devem ocorrer em um bloco de código que contabilize lógica conhecida e padrão / desconhecida .

E se, mais tarde, o código for introduzido acidentalmente no exemplo 2 que alterou significativamente a saída:

   if (myString == null)
   {
      return false;
   }

   //add some v1 update code here...
   myString = "And the winner is: ";
   //add some v2 update code here...
   //buried in v2 updates the following line was added
   myString = null;
   //add some v3 update code here...
   //Well technically this should not be hit because myString = null
   //but we already passed that logic
   myString = "Name " + myString;
   // Do something more here...
   return true;

Eu acho que com o elsebloco imediatamente após a verificação de um nulo, os programadores que adicionaram os aprimoramentos às versões futuras juntam toda a lógica, porque agora temos uma sequência de lógica não intencional para a regra original (retornando se o valor for nulo).

Eu acredito muito nisso em algumas das diretrizes C # no Codeplex (link para isso aqui: http://csharpguidelines.codeplex.com/ ) que afirmam o seguinte:

"Adicione um comentário descritivo se o bloco padrão (else) estiver vazio. Além disso, se esse bloco não for atingido, lance uma InvalidOperationException para detectar alterações futuras que possam ocorrer nos casos existentes. Isso garante um código melhor, porque todos os caminhos que o código pode percorrer foram considerados ".

Eu acho que é uma boa prática de programação ao usar blocos lógicos como este, sempre adicionar um bloco padrão (caso contrário, case: default) para explicar explicitamente todos os caminhos de código e não deixar o código aberto para consequências lógicas não intencionais.

atconway
fonte
Como não ter um outro no exemplo dois não leva todos os casos em consideração? O resultado pretendido é a execução continuada. Adicionar uma cláusula else nesse caso apenas aumenta a complexidade do método.
Chuck Conway
Discordo com base na capacidade de não ter toda a lógica em questão em um bloco conciso e determinístico. O foco está na manipulação do myStringvalor e o código que sucede ao ifbloco é a lógica resultante se a sequência! = Null. Portanto, como o foco está na lógica adicional que manipula esse valor específico , acho que ele deve ser encapsulado em um elsebloco. Caso contrário, isso abre potencial, como mostrei para separar involuntariamente a lógica e introduzir consequências não intencionais. Pode não acontecer o tempo todo, mas essa realmente era uma questão de estilo de opinião sobre práticas recomendadas .
atconway