Uma função deve ter apenas uma declaração de retorno?


780

Existem boas razões pelas quais é uma prática melhor ter apenas uma declaração de retorno em uma função?

Ou está certo retornar de uma função assim que estiver logicamente correto, significando que pode haver muitas instruções de retorno na função?


25
Não concordo que a questão seja independente da linguagem. Em alguns idiomas, ter vários retornos é mais natural e conveniente do que em outros. Eu teria mais chances de reclamar sobre retornos antecipados em uma função C do que em um C ++ que usa RAII.
Adrian McCarthy

3
Isso está intimamente relacionado e tem excelentes respostas: programmers.stackexchange.com/questions/118703/…
Tim Schmelter

independente de idioma? Explique a alguém que usa linguagem funcional que ele deve usar um retorno por função: p
Boiethios

Respostas:


741

Costumo ter várias instruções no início de um método para retornar para situações "fáceis". Por exemplo, isto:

public void DoStuff(Foo foo)
{
    if (foo != null)
    {
        ...
    }
}

... pode ficar mais legível (IMHO) assim:

public void DoStuff(Foo foo)
{
    if (foo == null) return;

    ...
}

Então, sim, acho que é bom ter vários "pontos de saída" de uma função / método.


83
Acordado. Embora ter vários pontos de saída possa ficar fora de controle, eu definitivamente acho que é melhor do que colocar toda a sua função em um bloco IF. Use return quantas vezes fizer sentido para manter seu código legível.
19378 Joshua Carmody

172
Isso é conhecido como "declaração de guarda", é a refatoração de Fowler.
Lars Westergren

12
Quando as funções são mantidas relativamente curtas, não é difícil seguir a estrutura de uma função com um ponto de retorno próximo ao meio.
KJAWolf 20/03/2009

21
Bloco enorme de instruções if-else, cada uma com um retorno? Isso não é nada. Geralmente, isso é facilmente refatorado. (pelo menos a variação de saída única mais comum com uma variável de resultado é, portanto, duvido que a variação de saída múltipla seja mais difícil.) Se você quiser uma dor de cabeça real, observe uma combinação de loops if-else e while (controlados por locais booleanos), onde as variáveis ​​de resultado são definidas, levando a um ponto de saída final no final do método. Essa é a idéia de saída única que enlouqueceu, e sim, estou falando da experiência real que tive que lidar com isso.
Marcus Andrén

7
'Imagine: você precisa fazer o método "AumentarStuffCallCounter" no final da função' DoStuff '. O que você fará neste caso? :) '-DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
Jim Balter 31/01

355

Ninguém mencionou ou citou Code Complete, então eu o farei.

17.1 retorno

Minimize o número de retornos em cada rotina . É mais difícil entender uma rotina se, lendo-a na parte inferior, você não tem consciência da possibilidade de que ela retorne a algum lugar acima.

Use um retorno quando melhorar a legibilidade . Em certas rotinas, depois de saber a resposta, você deseja devolvê-la à rotina de chamadas imediatamente. Se a rotina for definida de tal forma que não exija nenhuma limpeza, não retornar imediatamente significa que você precisa escrever mais código.


64
+1 para a nuance de "minimizar", mas não proíbe vários retornos.
Raedwald

13
"mais difícil de entender" é muito subjetivo, especialmente quando o autor não fornece nenhuma evidência empírica para apoiar a afirmação geral ... ter uma variável única que deve ser definida em muitos locais condicionais no código da declaração de retorno final é igualmente sujeita a "desconhecem a possibilidade de que a variável foi atribuído em algum lugar acima int ele funcionar!
Heston T. Holtmann

26
Pessoalmente, sou a favor de voltar mais cedo, sempre que possível. Por quê? Bem, quando você vê a palavra-chave de retorno para um determinado caso, você sabe instantaneamente "Eu terminei" - você não precisa continuar lendo para descobrir o que ocorre, se houver alguma coisa depois.
Mark Simpson

12
@ HestonT.Holtmann: O que tornou o Code Complete único entre os livros de programação foi que o conselho é apoiado por evidências empíricas.
Adrian McCarthy

9
Essa provavelmente deve ser a resposta aceita, pois menciona que ter vários pontos de retorno nem sempre é bom, mas às vezes necessário.
Rafid

229

Eu diria que seria incrivelmente imprudente decidir arbitrariamente contra vários pontos de saída, pois eu achei a técnica útil na prática repetidamente , na verdade, muitas vezes refatorei o código existente para vários pontos de saída para maior clareza. Podemos comparar as duas abordagens assim:

string fooBar(string s, int? i) {
  string ret = "";
  if(!string.IsNullOrEmpty(s) && i != null) {
    var res = someFunction(s, i);

    bool passed = true;
    foreach(var r in res) {
      if(!r.Passed) {
        passed = false;
        break;
      }
    }

    if(passed) {
      // Rest of code...
    }
  }

  return ret;
}

Compare isso com o código em que vários pontos de saída estão permitidos: -

string fooBar(string s, int? i) {
  var ret = "";
  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Eu acho que o último é consideravelmente mais claro. Tanto quanto posso dizer, a crítica a vários pontos de saída é um ponto de vista bastante arcaico atualmente.


12
A clareza está nos olhos de quem vê - olho para uma função e procuro um começo, meio e fim. Quando a função é pequeno o seu bem - mas quando você está tentando descobrir por que algo está quebrado e "Resto do Código" acaba por ser não-trivial você pode gastar idades procurando por isso ret é
Murph

7
Primeiro de tudo, este é um exemplo artificial. Segundo, onde: string ret; "entra na segunda versão? Terceiro, Ret não contém informações úteis. Quarto, por que tanta lógica em uma função / método? Quinto, por que não tornar DidValuesPass (type res) e RestOfCode () separados subfunções?
Rick Minerich

25
@ Rick 1. Não artificial na minha experiência, esse é realmente um padrão que já encontrei muitas vezes. 2. Ele é atribuído no "restante do código", talvez isso não esteja claro. 3. Hum? É um exemplo? 4. Bem, eu acho que é inventado a este respeito, mas é possível justificar este muito, 5. podia fazer ...
LJS

5
@Rick the point é que muitas vezes é mais fácil retornar mais cedo do que agrupar o código em uma declaração if enorme. Isso aparece muito na minha experiência, mesmo com a refatoração adequada.
Ljs

5
O ponto de não ter várias instruções de retorno é facilitar a depuração, um segundo distante para facilitar a leitura. Um ponto de interrupção no final permite ver o valor da saída, sem exceções. Quanto à legibilidade, as 2 funções mostradas não se comportam da mesma forma. O retorno padrão é uma sequência vazia se! R.Passed, mas o "mais legível" altera isso para retornar nulo. O autor interpretou mal que anteriormente havia um padrão após apenas algumas linhas. Mesmo em exemplos triviais, é fácil obter retornos padrão pouco claros, algo que um retorno único no final ajuda a impor.
19613 Mitch

191

Atualmente, estou trabalhando em uma base de código onde duas das pessoas que trabalham nela assinam cegamente a teoria do "ponto único de saída" e posso dizer que, por experiência, é uma prática horrível. Isso torna o código extremamente difícil de manter e eu mostrarei o porquê.

Com a teoria do "ponto único de saída", você inevitavelmente acaba com um código parecido com este:

function()
{
    HRESULT error = S_OK;

    if(SUCCEEDED(Operation1()))
    {
        if(SUCCEEDED(Operation2()))
        {
            if(SUCCEEDED(Operation3()))
            {
                if(SUCCEEDED(Operation4()))
                {
                }
                else
                {
                    error = OPERATION4FAILED;
                }
            }
            else
            {
                error = OPERATION3FAILED;
            }
        }
        else
        {
            error = OPERATION2FAILED;
        }
    }
    else
    {
        error = OPERATION1FAILED;
    }

    return error;
}

Isso não apenas torna o código muito difícil de seguir, mas agora, digamos mais tarde, você precisa voltar e adicionar uma operação entre 1 e 2. Você precisa recuar quase toda a função, e boa sorte para garantir que todos suas condições e chaves if / else são correspondidas corretamente.

Esse método torna a manutenção do código extremamente difícil e propensa a erros.


5
@ Murph: Você não tem como saber que nada mais ocorre após cada condição sem ler cada uma delas. Normalmente, eu diria que esse tipo de tópico é subjetivo, mas isso é claramente errado. Com um retorno sobre cada erro, você está pronto, sabe exatamente o que aconteceu.
GEOCHET 15/09/08

6
@ Murph: Eu vi esse tipo de código usado, abusado e usado em excesso. O exemplo é bastante simples, pois não existe um verdadeiro if / else dentro. Tudo o que esse tipo de código precisa quebrar é outro "esquecido". AFAIK, esse código precisa muito de exceções.
paercebal

15
Você pode refatorá-lo para isso, mantém sua "pureza": if (! SUCCEEDED (Operation1 ())) {} else error = OPERATION1FAILED; if (erro! = S_OK) {if (SUCCEEDED (Operation2 ())) {} else error = OPERATION2FAILED; } if (erro! = S_OK) {if (SUCCEEDED (Operation3 ())) {} else error = OPERATION3FAILED; } // etc.
Joe Pineda

6
Este código não possui apenas um ponto de saída: cada uma dessas instruções "error =" está no caminho de saída. Não se trata apenas de sair de funções, é de sair de qualquer bloco ou sequência.
Jay Bazuzi

6
Eu discordo que o retorno único "inevitavelmente" resulta em aninhamento profundo. Seu exemplo pode ser escrito como uma função simples e linear com um único retorno (sem gotos). E se você não confiar ou não puder confiar exclusivamente no RAII para o gerenciamento de recursos, os retornos iniciais acabam causando vazamentos ou código duplicado. Mais importante ainda, retornos antecipados tornam impraticável a afirmação de pós-condições.
Adrian McCarthy

72

A programação estruturada diz que você só deve ter uma instrução de retorno por função. Isso é para limitar a complexidade. Muitas pessoas, como Martin Fowler, argumentam que é mais simples escrever funções com várias instruções de retorno. Ele apresenta esse argumento no livro clássico de refatoração que ele escreveu. Isso funciona bem se você seguir os outros conselhos e escrever pequenas funções. Eu concordo com esse ponto de vista e apenas puristas estritos de programação estruturada aderem a declarações de retorno únicas por função.


44
A programação estruturada não diz nada. Algumas pessoas (mas não todas) que se descrevem como defensoras da programação estruturada dizem isso.
Wnoise 26/09/08

15
"Isso funciona bem se você seguir os outros conselhos e escrever pequenas funções." Este é o ponto mais importante. Funções pequenas raramente precisam de muitos pontos de retorno.

6
@wnoise +1 no comentário, é verdade. Toda a "estrutura de programação" diz que não deve usar o GOTO.
paxos1977

1
@ceretullis: a menos que seja necessário. Claro que não é essencial, mas pode ser útil no C. O kernel do linux o usa e por boas razões. O GOTO considerou prejudicial falar sobre o uso GOTOpara mover o fluxo de controle, mesmo quando a função existia. Nunca diz "nunca use GOTO".
Esteban Küber

1
"são sobre ignorar a 'estrutura' do código." - Não, pelo contrário. "faz sentido dizer que eles devem ser evitados" - não, não.
Jim Balter 05/01

62

Como Kent Beck observa ao discutir as cláusulas de guarda nos Padrões de implementação que fazem uma rotina ter um único ponto de entrada e saída ...

"era para evitar a confusão possível ao saltar para dentro e fora de muitos locais na mesma rotina. Fazia sentido quando aplicado a programas de linguagem assembly ou FORTRAN escritos com muitos dados globais, onde até mesmo entender quais instruções eram executadas era um trabalho árduo. com métodos pequenos e principalmente dados locais, é desnecessariamente conservador ".

Acho uma função escrita com cláusulas de guarda muito mais fácil de seguir do que um longo monte de if then elseinstruções aninhadas .


Obviamente, "um longo monte aninhado de instruções if-then-else" não é a única alternativa para guardar cláusulas.
Adrian McCarthy

@AdrianMcCarthy você tem uma alternativa melhor? Seria mais útil que o sarcasmo.
Shinzou 02/04

@kuhaku: Não tenho certeza se chamaria isso de sarcasmo. A resposta sugere que é uma situação de ou / ou: cláusulas de guarda ou longos grupos aninhados de se-então-outro. Muitas linguagens de programação (a maioria?) Oferecem muitas maneiras de fatorar essa lógica além das cláusulas de guarda.
Adrian McCarthy

61

Em uma função que não tem efeitos colaterais, não há boas razões para obter mais de um retorno e você deve escrevê-las em um estilo funcional. Em um método com efeitos colaterais, as coisas são mais seqüenciais (indexadas por tempo), então você escreve em um estilo imperativo, usando a instrução de retorno como um comando para interromper a execução.

Em outras palavras, quando possível, favorece esse estilo

return a > 0 ?
  positively(a):
  negatively(a);

por cima disto

if (a > 0)
  return positively(a);
else
  return negatively(a);

Se você se encontra escrevendo várias camadas de condições aninhadas, provavelmente há uma maneira de refatorar isso, usando a lista de predicados, por exemplo. Se você achar que seus ifs e elses estão distantes sintaticamente, convém dividir isso em funções menores. É difícil ler um bloco condicional que abrange mais de uma tela cheia de texto.

Não existe uma regra rígida que se aplique a todos os idiomas. Algo como ter uma única declaração de retorno não tornará seu código bom. Mas um bom código tenderá a permitir que você escreva suas funções dessa maneira.


6
+1 "Se você achar que seus ifs e elses estão distantes sintaticamente, convém dividir isso em funções menores."
Andres Jaan Tack

4
+1, se esse for um problema, geralmente significa que você está fazendo muito em uma única função. Isso realmente me deprime que este não é o maior resposta votou
Matt Briggs

1
As declarações de guarda também não têm efeitos colaterais, mas a maioria das pessoas os consideraria úteis. Portanto, pode haver razões para interromper a execução mais cedo, mesmo se não houver efeitos colaterais. Esta resposta não resolve totalmente o problema na minha opinião.
Maarten Bodewes

@ MaartenBodewes-owlstead Veja "Rigor e preguiça"
Apocalisp

43

Eu já vi isso nos padrões de codificação para C ++ que eram uma ressaca do C, como se você não tivesse RAII ou outro gerenciamento automático de memória, então seria necessário limpar para cada retorno, o que significa recortar e colar da limpeza ou do goto (logicamente o mesmo que 'finalmente' nos idiomas gerenciados), ambos considerados ruins. Se suas práticas são usar ponteiros e coleções inteligentes em C ++ ou outro sistema de memória automática, não há um motivo forte para isso, e tudo se resume à legibilidade e mais uma decisão de julgamento.


Bem dito, embora eu acredito que é melhor para copiar as exclusões ao tentar escrever código altamente otimizado (tais como software esfola malhas 3D complexos!)
Grant Peters

1
O que faz você acreditar nisso? Se você possui um compilador com otimização ruim, onde há alguma sobrecarga na remoção de referência a auto_ptr, você pode usar ponteiros simples em paralelo. Embora fosse estranho escrever código 'otimizado' com um compilador não otimizador em primeiro lugar.
Pete Kirkham

Isso cria uma exceção interessante à regra: se sua linguagem de programação não contiver algo que é automaticamente chamado no final do método (como try... finallyem Java) e você precisar fazer a manutenção de recursos, você poderia fazer com um único retornar no final de um método. Antes de fazer isso, considere seriamente refatorar o código para se livrar da situação.
Maarten Bodewes

@PeteKirkham Por que ir para a limpeza é ruim? yes goto pode ser mal utilizado, mas esse uso em particular não é ruim.
Q126y

1
@ q126y em C ++, diferente da RAII, falha quando uma exceção é lançada. Em C, é uma prática perfeitamente válida. Veja stackoverflow.com/questions/379172/use-goto-or-not
Pete Kirkham

40

Eu me apóio à ideia de que declarações de retorno no meio da função são ruins. Você pode usar retornos para criar algumas cláusulas de guarda na parte superior da função e, é claro, informar ao compilador o que retornar no final da função sem problemas, mas é possível perder e retornar no meio da função. tornar a função mais difícil de interpretar.


38

Existem boas razões pelas quais é uma prática melhor ter apenas uma declaração de retorno em uma função?

Sim , existem:

  • O único ponto de saída é um excelente local para afirmar suas pós-condições.
  • Ser capaz de colocar um ponto de interrupção do depurador em um retorno no final da função geralmente é útil.
  • Menos retornos significa menos complexidade. Código linear é geralmente mais simples de entender.
  • Se tentar simplificar uma função para um único retorno causa complexidade, é um incentivo para refatorar para funções menores, mais gerais e mais fáceis de entender.
  • Se você estiver em um idioma sem destruidores ou se não usar RAII, um único retorno reduzirá o número de locais que você precisará limpar.
  • Alguns idiomas exigem um único ponto de saída (por exemplo, Pascal e Eiffel).

A questão geralmente é colocada como uma falsa dicotomia entre retornos múltiplos ou declarações if profundamente aninhadas. Quase sempre há uma terceira solução que é muito linear (sem aninhamento profundo) com apenas um único ponto de saída.

Atualização : Aparentemente, as diretrizes MISRA também promovem uma saída única .

Para ser claro, não estou dizendo que é sempre errado ter vários retornos. Mas, dadas as soluções equivalentes, existem muitas boas razões para preferir a que tem um único retorno.


2
Outra boa razão, provavelmente a melhor atualmente, para ter uma única declaração de retorno é o log. se você deseja adicionar o log a um método, pode colocar uma única instrução de log que transmita o que o método retorna.
inor

Quão comum foi a instrução FORTRAN ENTRY? Consulte docs.oracle.com/cd/E19957-01/805-4939/6j4m0vn99/index.html . E se você é aventureiro, você pode logar métodos com AOP e pós-aconselhamento
Eric Jablow

1
+1 Os dois primeiros pontos foram suficientes para me convencer. Isso com o segundo último parágrafo. Eu discordaria do elemento de log pelo mesmo motivo que desencorajo condicionais aninhados profundos, pois eles incentivam a quebra da regra de responsabilidade única, que é a principal razão pela qual o polimorfismo foi introduzido nos OOPs.
22826 Francis Rodgers #

Gostaria de acrescentar que, com C # e Code Contracts, o problema pós-condição não é um problema, pois você ainda pode usar Contract.Ensurescom vários pontos de retorno.
julealgon

1
@ q126y: Se você está usando gotopara obter um código de limpeza comum, provavelmente simplificou a função para que exista um returnno final do código de limpeza. Então, você poderia dizer que resolveu o problema goto, mas eu diria que resolveu simplificando para um único return.
Adrian McCarthy

33

Ter um único ponto de saída fornece uma vantagem na depuração, porque permite definir um único ponto de interrupção no final de uma função para ver qual valor será realmente retornado.


6
BRAVO! Você é a única pessoa a mencionar esse motivo objetivo . É por isso que prefiro pontos de saída únicos versus vários pontos de saída. Se meu depurador pudesse definir um ponto de interrupção em qualquer ponto de saída, eu provavelmente preferiria vários pontos de saída. Minha opinião atual é que as pessoas que codificam vários pontos de saída fazem isso para seu próprio benefício às custas daqueles que precisam usar um depurador em seu código (e sim, estou falando de todos os colaboradores de código aberto que escrevem código com vários pontos de saída.)
MikeSchinkel 1/11

3
SIM. Estou adicionando código de log a um sistema que se comporta de maneira intermitente na produção (onde não consigo avançar). Se o codificador anterior tivesse usado saída única, teria sido MUITO mais fácil.
Michael Blackburn

3
É verdade que, na depuração, é útil. Mas, na prática, na maioria dos casos, consegui definir o ponto de interrupção na função de chamada, logo após a chamada - efetivamente para o mesmo resultado. (E essa posição é encontrada na pilha de chamadas, é claro.) YMMV.
foo

Isto é, a menos que seu depurador forneça uma função de saída ou retorno de etapa (e todo e qualquer depurador faça o que sei), que mostra o valor retornado logo após o retorno. Alterar o valor posteriormente pode ser um pouco complicado, se não for atribuído a uma variável.
Maarten Bodewes

7
Não vejo um depurador há muito tempo que não permita que você defina um ponto de interrupção no "fechamento" do método (final, chaves, qualquer que seja o seu idioma) e atinja esse ponto de interrupção, independentemente de onde ou quantos , statemets de retorno estão no método Além disso, mesmo que a função tenha apenas um retorno, isso não significa que você não pode sair da função com uma exceção (explícita ou herdada). Então, acho que esse não é realmente um ponto válido.
Scott Gartner

19

Em geral, tento ter apenas um único ponto de saída de uma função. Porém, há momentos em que isso acaba criando um corpo de função mais complexo do que o necessário; nesse caso, é melhor ter vários pontos de saída. Realmente precisa ser uma "decisão de julgamento" com base na complexidade resultante, mas o objetivo deve ser o menor número possível de pontos de saída, sem sacrificar a complexidade e a compreensibilidade.


"Em geral, tento ter apenas um único ponto de saída de uma função" - por quê? "o objetivo deve ser o menor número possível de pontos de saída" - por quê? E por que 19 pessoas votaram nessa não resposta?
Jim Balter 05/01

@ JimBalter Por fim, tudo se resume a preferências pessoais. Mais pontos de saída geralmente levam a um método mais complexo (embora nem sempre) e dificultam a compreensão de alguém.
Scott Dorman

"tudo se resume à preferência pessoal." - Em outras palavras, você não pode oferecer uma razão. "Mais pontos de saída geralmente levam a um método mais complexo (embora nem sempre)" - Não, na verdade eles não. Dadas duas funções que são logicamente equivalentes, uma com cláusulas de guarda e outra com saída única, a última terá uma complexidade ciclomática mais alta, que numerosos estudos mostram resultados em código mais suscetível a erros e difícil de entender. Você se beneficiaria da leitura das outras respostas aqui.
perfil completo de Jim Balter

14

Não, porque não vivemos mais na década de 1970 . Se sua função é longa o suficiente para que vários retornos sejam um problema, é muito longo.

(Além do fato de que qualquer função de várias linhas em um idioma com exceções terá vários pontos de saída de qualquer maneira.)


14

Minha preferência seria pela saída única, a menos que isso realmente complique as coisas. Descobri que, em alguns casos, vários pontos existentes podem mascarar outros problemas de design mais significativos:

public void DoStuff(Foo foo)
{
    if (foo == null) return;
}

Ao ver esse código, eu perguntaria imediatamente:

  • 'Foo' é sempre nulo?
  • Em caso afirmativo, quantos clientes de 'DoStuff' chamam a função com um nulo 'foo'?

Dependendo das respostas a essas perguntas, pode ser que

  1. o cheque é inútil, pois nunca é verdade (ou seja, deve ser uma afirmação)
  2. a verificação raramente é verdadeira e, portanto, pode ser melhor alterar essas funções específicas do chamador, pois elas provavelmente devem executar outra ação.

Nos dois casos acima, o código provavelmente pode ser reformulado com uma asserção para garantir que 'foo' nunca seja nulo e que os chamadores relevantes sejam alterados.

Há duas outras razões (acho que o código C ++ específico) em que múltiplas existem realmente podem ter um efeito negativo . Eles são tamanho do código e otimizações do compilador.

Um objeto que não seja POD C ++ no escopo na saída de uma função terá seu destruidor chamado. Onde existem várias instruções de retorno, pode ser que haja objetos diferentes no escopo e, portanto, a lista de destruidores a serem chamados será diferente. O compilador, portanto, precisa gerar código para cada instrução de retorno:

void foo (int i, int j) {
  A a;
  if (i > 0) {
     B b;
     return ;   // Call dtor for 'b' followed by 'a'
  }
  if (i == j) {
     C c;
     B b;
     return ;   // Call dtor for 'b', 'c' and then 'a'
  }
  return 'a'    // Call dtor for 'a'
}

Se o tamanho do código for um problema - isso pode ser algo que vale a pena evitar.

A outra questão diz respeito à "Otimização do valor de retorno nomeado" (também conhecida como Copy Elision, ISO C ++ '03 12.8 / 15). O C ++ permite que uma implementação pule a chamada do construtor de cópia, se puder:

A foo () {
  A a1;
  // do something
  return a1;
}

void bar () {
  A a2 ( foo() );
}

Tomando o código como está, o objeto 'a1' é construído em 'foo' e, em seguida, sua construção de cópia será chamada para construir 'a2'. No entanto, a cópia elision permite que o compilador construa 'a1' no mesmo local na pilha que 'a2'. Portanto, não há necessidade de "copiar" o objeto quando a função retornar.

Vários pontos de saída complicam o trabalho do compilador na tentativa de detectar isso e, pelo menos para uma versão relativamente recente do VC ++, a otimização não ocorreu onde o corpo da função teve vários retornos. Consulte Otimização de valor de retorno nomeado no Visual C ++ 2005 para obter mais detalhes.


1
Se você retirar o último dtor do seu exemplo C ++, exceto o código para destruir B e C e B posteriores, ainda terá que ser gerado quando o escopo da instrução if terminar, para que você realmente não ganhe nada por não ter retornos múltiplos .
Eclipse

4
+1 E, no final da lista, temos a verdadeira razão pela qual essa prática de codificação existe - NRVO. No entanto, esta é uma micro-otimização; e, como todas as práticas de micro-otimização, provavelmente foi iniciado por um "especialista" de 50 anos que está acostumado a programar em um PDP-8 de 300 kHz e não entende a importância do código limpo e estruturado. Em geral, siga o conselho de Chris S e use várias declarações de retorno sempre que fizer sentido.
precisa saber é o seguinte

Embora eu discorde de sua preferência (em minha opinião, sua sugestão de afirmação também é um ponto de retorno, como throw new ArgumentNullException()em C # neste caso), gostei muito de suas outras considerações, todas são válidas para mim e podem ser críticas em alguns casos. contextos de nicho.
precisa saber é

Isto está cheio de homens de palha. A questão de por que fooestá sendo testado não tem nada a ver com o assunto, que é se fazer if (foo == NULL) return; dowork; ouif (foo != NULL) { dowork; }
Jim Balter

11

Ter um único ponto de saída reduz a Complexidade Ciclomática e, portanto, em teoria , reduz a probabilidade de que você introduza erros no seu código ao alterá-lo. A prática, no entanto, tende a sugerir que é necessária uma abordagem mais pragmática. Portanto, tenho a tendência de ter um único ponto de saída, mas permiti que meu código tivesse vários, se isso for mais legível.


Muito perspicaz. Embora eu sinta que, até que um programador saiba quando usar vários pontos de saída, ele deve ser restrito a um.
21311 Rick Minerich

5
Na verdade não. A complexidade ciclomática de "if (...) return; ... return;" é o mesmo que "se (...) {...} retornar;". Ambos têm dois caminhos através deles.
21119 Steve Emmerson

11

Eu me forço a usar apenas uma returndeclaração, pois, de certa forma, gerará cheiro de código. Deixe-me explicar:

function isCorrect($param1, $param2, $param3) {
    $toret = false;
    if ($param1 != $param2) {
        if ($param1 == ($param3 * 2)) {
            if ($param2 == ($param3 / 3)) {
                $toret = true;
            } else {
                $error = 'Error 3';
            }
        } else {
            $error = 'Error 2';
        }
    } else {
        $error = 'Error 1';
    }
    return $toret;
}

(As condições são arbritárias ...)

Quanto mais condições, maior a função, mais difícil é a leitura. Portanto, se você estiver sintonizado com o cheiro do código, perceberá e desejará refatorar o código. Duas soluções possíveis são:

  • Vários retornos
  • Refatorando em funções separadas

Devoluções múltiplas

function isCorrect($param1, $param2, $param3) {
    if ($param1 == $param2)       { $error = 'Error 1'; return false; }
    if ($param1 != ($param3 * 2)) { $error = 'Error 2'; return false; }
    if ($param2 != ($param3 / 3)) { $error = 'Error 3'; return false; }
    return true;
}

Funções separadas

function isEqual($param1, $param2) {
    return $param1 == $param2;
}

function isDouble($param1, $param2) {
    return $param1 == ($param2 * 2);
}

function isThird($param1, $param2) {
    return $param1 == ($param2 / 3);
}

function isCorrect($param1, $param2, $param3) {
    return !isEqual($param1, $param2)
        && isDouble($param1, $param3)
        && isThird($param2, $param3);
}

Concedido, é mais longo e um pouco confuso, mas no processo de refatorar a função dessa maneira, temos

  • criou várias funções reutilizáveis,
  • tornou a função mais legível por humanos e
  • o foco das funções é por que os valores estão corretos.

5
-1: Mau exemplo. Você omitiu o tratamento da mensagem de erro. Se isso não fosse necessário, o isCorrect poderia ser expresso como retorno xx && yy && zz; onde xx, yy e z são as expressões isEqual, isDouble e isThird.
21129 kauppi

10

Eu diria que você deve ter quantos forem necessários ou qualquer um que torne o código mais limpo (como cláusulas de guarda ).

Eu pessoalmente nunca ouvi / vi nenhuma "prática recomendada" dizer que você deve ter apenas uma declaração de retorno.

Na maioria das vezes, costumo sair de uma função o mais rápido possível com base em um caminho lógico (as cláusulas de guarda são um excelente exemplo disso).


10

Acredito que vários retornos geralmente são bons (no código que escrevo em C #). O estilo de retorno único é uma reserva de C. Mas você provavelmente não está codificando em C.

Não há lei exigindo apenas um ponto de saída para um método em todas as linguagens de programação . Algumas pessoas insistem na superioridade desse estilo, e às vezes o elevam a uma "regra" ou "lei", mas essa crença não é apoiada por nenhuma evidência ou pesquisa.

Mais de um estilo de retorno pode ser um mau hábito no código C, onde os recursos precisam ser desalocados explicitamente, mas linguagens como Java, C #, Python ou JavaScript que têm construções como coleta automática de lixo e try..finallyblocos (eusing blocos em C # ) e esse argumento não se aplica - nesses idiomas, é muito incomum precisar da desalocação manual centralizada de recursos.

Há casos em que um único retorno é mais legível e casos em que não é. Veja se isso reduz o número de linhas de código, torna a lógica mais clara ou reduz o número de chaves e recuos ou variáveis ​​temporárias.

Portanto, use quantos retornos forem adequados às suas sensibilidades artísticas, pois é um problema de layout e legibilidade, não técnico.

Eu falei sobre isso mais detalhadamente no meu blog .


10

Há coisas boas a dizer sobre ter um único ponto de saída, assim como há coisas ruins a dizer sobre a inevitável programação "flecha" resultante.

Se estiver usando vários pontos de saída durante a validação de entrada ou alocação de recursos, tento colocar todas as 'saídas de erro' muito visivelmente na parte superior da função.

O artigo Spartan Programming do "SSDSLPedia" e o ponto de saída de função única artigo de do "Wiki do Portland Pattern Repository" têm alguns argumentos interessantes sobre isso. Além disso, é claro, existe este post a considerar.

Se você realmente deseja um único ponto de saída (em qualquer idioma habilitado para não-exceção), por exemplo, para liberar recursos em um único local, acho que a aplicação cuidadosa de goto é boa; veja, por exemplo, este exemplo bastante artificial (compactado para salvar o espaço da tela):

int f(int y) {
    int value = -1;
    void *data = NULL;

    if (y < 0)
        goto clean;

    if ((data = malloc(123)) == NULL)
        goto clean;

    /* More code */

    value = 1;
clean:
   free(data);
   return value;
}

Pessoalmente, em geral, não gosto mais de programar flechas do que de vários pontos de saída, embora ambos sejam úteis quando aplicados corretamente. O melhor, é claro, é estruturar seu programa para não exigir nenhum. Dividir sua função em vários pedaços geralmente ajuda :)

Embora, ao fazer isso, descubra que acabe com vários pontos de saída de qualquer maneira, como neste exemplo, onde alguma função maior foi dividida em várias funções menores:

int g(int y) {
  value = 0;

  if ((value = g0(y, value)) == -1)
    return -1;

  if ((value = g1(y, value)) == -1)
    return -1;

  return g2(y, value);
}

Dependendo do projeto ou das diretrizes de codificação, a maior parte do código da placa da caldeira pode ser substituída por macros. Como observação lateral, dividi-la dessa maneira torna as funções g0, g1, g2 muito fáceis de serem testadas individualmente.

Obviamente, em uma linguagem OO e habilitada para exceção, eu não usaria declarações if como essa (ou de todo, se eu pudesse fazer isso com pouco esforço), e o código seria muito mais claro. E não-flecha. E a maioria dos retornos não finais provavelmente seriam exceções.

Em resumo;

  • Poucos retornos são melhores que muitos retornos
  • Mais de um retorno é melhor que grandes flechas, e as cláusulas de guarda geralmente são válidas.
  • Exceções podem / provavelmente devem substituir a maioria das 'cláusulas de guarda' quando possível.

O exemplo cai para y <0, porque tenta libertar o ponteiro nulo ;-)
Erich Kitzmueller

2
opengroup.org/onlinepubs/009695399/functions/free.html "Se ptr for um ponteiro nulo, nenhuma ação deverá ocorrer."
27616 Henrik Gustafsson

1
Não, ele não trava, porque passar NULL para livre é um no-op definido. É um equívoco irritantemente comum que você deve testar primeiro o NULL.
hlovdal

O padrão de "flecha" não é uma alternativa inevitável. Esta é uma falsa dicotomia.
Adrian McCarthy

9

Você conhece o ditado - a beleza está nos olhos de quem vê .

Algumas pessoas juram pelo NetBeans e outras pelo IntelliJ IDEA , outras pelo Python e outras pelo PHP .

Em algumas lojas, você pode perder o emprego se insistir em fazer isso:

public void hello()
{
   if (....)
   {
      ....
   }
}

A questão é toda sobre visibilidade e manutenção.

Sou viciado em usar álgebra booleana para reduzir e simplificar a lógica e o uso de máquinas de estado. No entanto, havia colegas do passado que acreditavam que meu emprego de "técnicas matemáticas" na codificação é inadequado, porque não seria visível e sustentável. E isso seria uma má prática. Desculpe pessoal, as técnicas que emprego são muito visíveis e sustentáveis ​​para mim - porque, quando voltasse ao código seis meses depois, entenderia claramente o código, ao invés de ver uma bagunça de espaguete proverbial.

Ei, amigo (como costumava dizer um cliente anterior), faça o que quiser, desde que saiba como consertar quando eu precisar que você conserte.

Lembro-me de 20 anos atrás, um colega meu foi demitido por empregar o que hoje seria chamado de estratégia de desenvolvimento ágil . Ele tinha um plano incremental meticuloso. Mas o gerente dele estava gritando: "Você não pode liberar recursos progressivamente para os usuários! Você deve ficar com a cascata ". Sua resposta ao gerente foi que o desenvolvimento incremental seria mais preciso para as necessidades do cliente. Ele acreditava no desenvolvimento para as necessidades dos clientes, mas o gerente acreditava na codificação da "exigência do cliente".

Somos frequentemente culpados por quebrar a normalização de dados, os limites de MVP e MVC . Nós alinhamos em vez de construir uma função. Tomamos atalhos.

Pessoalmente, acredito que o PHP é uma má prática, mas o que eu sei. Todos os argumentos teóricos se resumem a tentar cumprir um conjunto de regras

qualidade = precisão, manutenção e rentabilidade.

Todas as outras regras desaparecem em segundo plano. E é claro que essa regra nunca desaparece:

A preguiça é a virtude de um bom programador.


1
"Ei, amigo (como costumava dizer um cliente), faça o que quiser, desde que saiba como consertar quando eu precisar que você conserte". Problema: geralmente não é você quem 'corrige'.
Dan Barron

Marcar esta resposta com +1 porque concordo com onde você está indo, mas não necessariamente como você chega lá. Eu argumentaria que existem níveis de entendimento. isto é, o entendimento de que o funcionário A possui 5 anos de experiência em programação e 5 anos em uma empresa é muito diferente do empregado B, recém-formado recém-formado em uma empresa. O que quero dizer é que, se o funcionário A é o único que pode entender o código, NÃO é sustentável e, portanto, todos devemos nos esforçar para escrever um código que o funcionário B possa entender. É aqui que a verdadeira arte está no software.
9608 Francis Rodgers #

9

Eu me inclino a usar cláusulas de guarda para retornar cedo e sair no final de um método. A regra de entrada e saída única tem importância histórica e foi particularmente útil ao lidar com código legado que rodava em 10 páginas A4 para um único método C ++ com vários retornos (e muitos defeitos). Mais recentemente, a boa prática aceita é manter os métodos pequenos, o que torna as saídas múltiplas menos uma impedância ao entendimento. No exemplo a seguir do Kronoz copiado de cima, a questão é o que ocorre em // Restante do código ... ?:

void string fooBar(string s, int? i) {

  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Sei que o exemplo é um tanto artificial, mas ficaria tentado a refatorar o loop foreach em uma instrução LINQ que poderia ser considerada uma cláusula de guarda. Mais uma vez, em um exemplo artificial a intenção do código não é aparente e someFunction () pode ter qualquer outro efeito colateral ou o resultado pode ser utilizado no // Resto do código ... .

if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;

Fornecendo a seguinte função refatorada:

void string fooBar(string s, int? i) {

  if (string.IsNullOrEmpty(s) || i == null) return null;
  if (someFunction(s, i).Any(r => !r.Passed)) return null;

  // Rest of code...

  return ret;
}

C ++ não tem exceções? Então, por que você retornaria em nullvez de lançar uma exceção indicando que o argumento não foi aceito?
Maarten Bodewes

1
Como indiquei, o código de exemplo é copiado de uma resposta anterior ( stackoverflow.com/a/36729/132599 ). O exemplo original retornou nulos e a refatoração para lançar exceções de argumento não era material ao ponto que eu estava tentando fazer ou à pergunta original. Por uma questão de boas práticas, sim, eu normalmente (em C #) lançaria um ArgumentNullException em uma cláusula de guarda, em vez de retornar um valor nulo.
David Clarke

7

Um bom motivo para pensar é na manutenção do código: você tem um único ponto de saída. Se você deseja alterar o formato do resultado, ..., é muito mais simples de implementar. Além disso, para depuração, você pode simplesmente colocar um ponto de interrupção lá :)

Dito isto, certa vez tive que trabalhar em uma biblioteca onde os padrões de codificação impunham 'uma declaração de retorno por função' e achei muito difícil. Eu escrevo muitos códigos de cálculos numéricos, e geralmente existem 'casos especiais', então o código acabou sendo muito difícil de seguir ...


Isso realmente não faz diferença. Se você alterar o tipo da variável local retornada, precisará corrigir todas as atribuições a essa variável local. E provavelmente é melhor definir um método com uma assinatura diferente, pois você também precisará corrigir todas as chamadas de método.
Maarten Bodewes

@ MaartenBodewes-owlstead - Pode fazer a diferença. Para nomear apenas dois exemplos em que você não precisaria corrigir todas as atribuições à variável local ou alterar as chamadas de método, a função pode retornar uma data como uma string (a variável local seria uma data real, formatada como uma string apenas em último momento) ou pode retornar um número decimal e você deseja alterar o número de casas decimais.
Nnnnnn

@nnnnnn OK, se você quiser fazer o pós-processamento na saída ... Mas eu apenas geraria um novo método que faça o pós-processamento e deixaria o antigo em paz. Isso é apenas um pouco mais difícil de refatorar, mas você precisará verificar se as outras chamadas são compatíveis com o novo formato. Mas é uma razão válida, no entanto.
Maarten Bodewes

7

Vários pontos de saída são adequados para funções pequenas o suficiente - ou seja, uma função que pode ser visualizada em uma tela inteira. Se uma função longa também inclui vários pontos de saída, é um sinal de que a função pode ser dividida ainda mais.

Dito isto, evito funções de saída múltipla, a menos que seja absolutamente necessário . Senti dor de bugs devido a algum retorno perdido em alguma linha obscura em funções mais complexas.


6

Eu trabalhei com terríveis padrões de codificação que forçaram um único caminho de saída para você, e o resultado quase sempre é espaguete não estruturado, se a função for qualquer coisa menos trivial - você acaba com muitas pausas e continua que apenas atrapalham.


Para não mencionar ter que dizer a sua mente para ignorar a ifdeclaração na frente de cada chamada de método que retorna sucesso ou não :(
Maarten Bodewes

6

Ponto de saída único - todas as outras coisas iguais - torna o código significativamente mais legível. Mas há um problema: construção popular

resulttype res;
if if if...
return res;

é falso, "res =" não é muito melhor que "retorno". Ele possui uma declaração de retorno único, mas vários pontos em que a função realmente termina.

Se você possui uma função com vários retornos (ou "res =" s), geralmente é uma boa ideia dividi-la em várias funções menores com um único ponto de saída.


6

Minha política usual é ter apenas uma declaração de retorno no final de uma função, a menos que a complexidade do código seja bastante reduzida ao adicionar mais. Na verdade, sou um fã de Eiffel, que aplica a única regra de retorno por não ter uma declaração de retorno (há apenas uma variável 'resultado' criada automaticamente para colocar o resultado).

Certamente há casos em que o código pode ser esclarecido com retornos múltiplos do que a versão óbvia sem eles. Alguém poderia argumentar que é necessário mais retrabalho se você tiver uma função muito complexa para ser compreensível sem várias declarações de retorno, mas às vezes é bom ser pragmático sobre essas coisas.


5

Se você terminar com mais de alguns retornos, pode haver algo errado com seu código. Caso contrário, eu concordaria que, às vezes, é bom poder retornar de vários locais em uma sub-rotina, especialmente quando torna o código mais limpo.

Perl 6: Exemplo ruim

sub Int_to_String( Int i ){
  given( i ){
    when 0 { return "zero" }
    when 1 { return "one" }
    when 2 { return "two" }
    when 3 { return "three" }
    when 4 { return "four" }
    ...
    default { return undef }
  }
}

seria melhor escrito assim

Perl 6: bom exemplo

@Int_to_String = qw{
  zero
  one
  two
  three
  four
  ...
}
sub Int_to_String( Int i ){
  return undef if i < 0;
  return undef unless i < @Int_to_String.length;
  return @Int_to_String[i]
}

Observe que este é apenas um exemplo rápido


Ok Por que isso foi rejeitado? Não é como se não fosse uma opinião.
Brad Gilbert

5

Eu voto no retorno único no final como uma diretriz. Isso ajuda na manipulação comum de limpeza de código ... Por exemplo, veja o código a seguir ...

void ProcessMyFile (char *szFileName)
{
   FILE *fp = NULL;
   char *pbyBuffer = NULL:

   do {

      fp = fopen (szFileName, "r");

      if (NULL == fp) {

         break;
      }

      pbyBuffer = malloc (__SOME__SIZE___);

      if (NULL == pbyBuffer) {

         break;
      }

      /*** Do some processing with file ***/

   } while (0);

   if (pbyBuffer) {

      free (pbyBuffer);
   }

   if (fp) {

      fclose (fp);
   }
}

Você vota no retorno único - no código C. Mas e se você estivesse codificando em um idioma que tivesse a coleta de lixo e tentasse..finalmente bloqueia?
Anthony

4

Essa é provavelmente uma perspectiva incomum, mas acho que quem acredita que várias declarações de retorno devem ser favorecidas nunca precisou usar um depurador em um microprocessador que suporta apenas quatro pontos de interrupção de hardware. ;-)

Embora os problemas do "código de seta" estejam completamente corretos, um problema que parece desaparecer ao usar várias instruções de retorno está na situação em que você está usando um depurador. Você não tem uma posição conveniente para colocar um ponto de interrupção para garantir que verá a saída e, portanto, a condição de retorno.


5
Esse é apenas um tipo diferente de otimização prematura. Você nunca deve otimizar para o caso especial. Se você se depurar muito de uma seção específica do código, há mais coisas erradas do que quantos pontos de saída ela possui.
cunha

Depende do seu depurador também.
Maarten Bodewes

4

Quanto mais instruções de retorno você tiver em uma função, maior será a complexidade desse método. Se você se perguntar se possui muitas instruções de retorno, pode se perguntar se possui muitas linhas de código nessa função.

Mas, não, não há nada errado com uma / muitas declarações de retorno. Em algumas línguas, é uma prática melhor (C ++) do que em outras (C).

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.