De onde veio a noção de "apenas um retorno"?

1004

Frequentemente falo com programadores que dizem " Não coloque várias declarações de retorno no mesmo método. " Quando eu lhes peço que me digam as razões, tudo que consigo é " O padrão de codificação diz isso. "ou" É confuso. "Quando eles me mostram soluções com uma única instrução de retorno, o código parece mais feio para mim. Por exemplo:

if (condition)
   return 42;
else
   return 97;

" Isso é feio, você tem que usar uma variável local! "

int result;
if (condition)
   result = 42;
else
   result = 97;
return result;

Como é que este código de 50% faz com que o programa seja mais fácil de entender? Pessoalmente, acho mais difícil, porque o espaço de estado aumentou apenas com outra variável que poderia facilmente ter sido evitada.

Claro, normalmente eu apenas escrevia:

return (condition) ? 42 : 97;

Mas muitos programadores evitam o operador condicional e preferem o formato longo.

De onde veio essa noção de "apenas um retorno"? Existe uma razão histórica pela qual esta convenção surgiu?

    
por fredoverflow 06.07.2017 / 23:54
fonte

9 respostas

1053

"Single Entry, Single Exit" foi escrito quando a maioria das programações era feita em linguagem assembly, FORTRAN ou COBOL. Ele tem sido amplamente mal interpretado, porque as linguagens modernas não suportam as práticas contra as quais Dijkstra estava advertindo.

"Single Entry" significa "não crie pontos de entrada alternativos para funções". Na linguagem de montagem, é claro, é possível inserir uma função em qualquer instrução. O FORTRAN suportou várias entradas para funções com a instrução ENTRY :

      SUBROUTINE S(X, Y)
      R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
      ENTRY S2(R)
      ...
      RETURN
      END

C USAGE
      CALL S(3,4)
C ALTERNATE USAGE
      CALL S2(5)

"Single Exit" significa que uma função só deve retornar para um local: a instrução imediatamente após a chamada. Não não significa que uma função só deve retornar de um lugar. Quando Programação Estruturada foi escrita, era prática comum para uma função indicar um erro retornando a um local alternativo. FORTRAN suportou isso via "retorno alternativo":

C SUBROUTINE WITH ALTERNATE RETURN.  THE '*' IS A PLACE HOLDER FOR THE ERROR RETURN
      SUBROUTINE QSOLVE(A, B, C, X1, X2, *)
      DISCR = B*B - 4*A*C
C NO SOLUTIONS, RETURN TO ERROR HANDLING LOCATION
      IF DISCR .LT. 0 RETURN 1
      SD = SQRT(DISCR)
      DENOM = 2*A
      X1 = (-B + SD) / DENOM
      X2 = (-B - SD) / DENOM
      RETURN
      END

C USE OF ALTERNATE RETURN
      CALL QSOLVE(1, 0, 1, X1, X2, *99)
C SOLUTION FOUND
      ...
C QSOLVE RETURNS HERE IF NO SOLUTIONS
99    PRINT 'NO SOLUTIONS'

Ambas as técnicas eram altamente propensas a erros. O uso de entradas alternativas muitas vezes deixava alguma variável não inicializada. O uso de retornos alternativos teve todos os problemas de uma instrução GOTO, com a complicação adicional de que a condição da ramificação não era adjacente à ramificação, mas em algum lugar da sub-rotina.

    
por 14.07.2017 / 04:48
fonte
891

Essa noção de Entrada Única, Saída Única (SESE) vem de idiomas com gerenciamento explícito de recursos , como C e assembly. Em C, código como esse vazará recursos:

void f()
{
  resource res = acquire_resource();  // think malloc()
  if( f1(res) )
    return; // leaks res
  f2(res);
  release_resource(res);  // think free()
}

Em tais idiomas, você basicamente tem três opções:

  • Replicar o código de limpeza.
    Ugh. Redundância é sempre ruim.

  • Use um goto para ir para o código de limpeza.
    Isso requer que o código de limpeza seja a última coisa na função. (E é por isso que alguns argumentam que goto tem seu lugar. E de fato - em C.)

  • Introduza uma variável local e manipule o fluxo de controle através dela.
    A desvantagem é que o fluxo de controle manipulado através da sintaxe (pense em break , return , if , while ) é muito mais fácil de seguir do que controlar o fluxo manipulado através do estado de variáveis (porque essas variáveis não têm estado quando você olhe o algoritmo).

Na montagem é ainda mais estranho, porque você pode pular para qualquer endereço em uma função quando você chama essa função, o que efetivamente significa que você tem um número quase ilimitado de pontos de entrada para qualquer função. (Às vezes isso é útil. Esses thunks são uma técnica comum para os compiladores implementarem o ajuste this do ponteiro necessário para chamar as funções virtual em cenários de herança múltipla em C ++.)

Quando você precisa gerenciar recursos manualmente, explorar as opções de inserir ou sair de uma função em qualquer lugar leva a um código mais complexo e, portanto, a bugs. Portanto, surgiu uma escola de pensamento que propagava o SESE, a fim de obter um código mais limpo e menos bugs.

No entanto, quando um idioma apresenta exceções, (quase) qualquer função pode ser encerrada prematuramente em (quase) qualquer ponto, portanto, é necessário tomar providências para um retorno prematuro. (Eu acho que finally é usado principalmente para isso em Java e using (ao implementar IDisposable , finally caso contrário) em C #; em vez disso, C ++ emprega RAII .) Depois de ter feito isso, você não pode falhar na limpeza após uma declaração return , então qual é provavelmente o argumento mais strong em favor da SESE desapareceu.

Isso deixa legibilidade. Obviamente, uma função 200 LoC com meia dúzia de declarações return espalhadas aleatoriamente não é um bom estilo de programação e não garante um código legível. Mas tal função não seria fácil de entender sem esses retornos prematuros.

Em idiomas onde os recursos não são ou não devem ser gerenciados manualmente, há pouco ou nenhum valor em aderir à antiga convenção SESE. OTOH, como argumentei acima, SESE geralmente torna o código mais complexo . É um dinossauro que (exceto para C) não se encaixa bem na maioria das línguas de hoje. Em vez de ajudar a compreensibilidade do código, isso o impede.

Por que os programadores Java se atêm a isso? Eu não sei, mas do meu (fora) POV, o Java tirou muitas convenções do C (onde elas fazem sentido) e as aplicou ao seu mundo OO (onde elas são inúteis ou completamente ruins), onde agora ele adere eles, não importa quais sejam os custos. (Como a convenção para definir todas as variáveis no início do escopo).

Os programadores aderem a todos os tipos de notações estranhas por motivos irracionais. (Declarações estruturais profundamente aninhadas - "pontas de flechas" - eram, em linguagens como Pascal, vistas como um belo código.) A aplicação de um raciocínio lógico puro parece não conseguir convencer a maioria deles a se desviar de seus modos estabelecidos. A melhor maneira de mudar esses hábitos é provavelmente ensiná-los desde cedo a fazer o que é melhor, não o convencional. Você, sendo um professor de programação, tem em sua mão. :)

    
por 07.07.2017 / 01:12
fonte
80

Por um lado, as instruções de retorno único facilitam o registro, bem como as formas de depuração que dependem do registro em log. Lembro-me de várias vezes que tive que reduzir a função em um único retorno apenas para imprimir o valor de retorno em um único ponto.

  int function() {
     if (bidi) { print("return 1"); return 1; }
     for (int i = 0; i < n; i++) {
       if (vidi) { print("return 2"); return 2;}
     }
     print("return 3");
     return 3;
  }

Por outro lado, você pode refatorar isso em function() , que chama _function() e registra o resultado.

    
por 06.07.2017 / 23:57
fonte
51

"Single Entry, Single Exit" originou-se da revolução da Programação Estruturada do início dos anos 1970, que foi iniciada pela carta de Edsger W. Dijkstra ao Editor " Declaração do GOTO considerada prejudicial ". Os conceitos por trás da programação estruturada foram expostos em detalhes no livro clássico "Programação Estruturada", de Ole Johan-Dahl, Edsger W. Dijkstra e Charles Anthony Richard Hoare.

"Declaração GOTO Considerada Nociva" é leitura obrigatória, mesmo hoje. "Programação Estruturada" é datada, mas ainda muito, muito gratificante, e deve estar no topo da lista "Deve Ler" de qualquer desenvolvedor, muito acima de qualquer coisa de, e. Steve McConnell. (A seção de Dahl apresenta os fundamentos das aulas no Simula 67, que são a base técnica para classes em C ++ e toda a programação orientada a objetos.)

    
por 09.11.2011 / 15:22
fonte
31

É sempre fácil vincular o Fowler.

Um dos principais exemplos que vão contra o SESE são cláusulas de guarda:

Substituir condicional aninhada por cláusulas de proteção

Use Guard Clauses for all the special cases

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
return result;
};  

                                                                                                         http://www.refactoring.com/catalog/arrow.gif

double getPayAmount() {
    if (_isDead) return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired) return retiredAmount();
    return normalPayAmount();
};  

For more information see page 250 of Refactoring...

    
por 06.07.2017 / 23:58
fonte
10

Eu escrevi um post sobre este assunto um tempo atrás.

O resultado é que essa regra vem da idade dos idiomas que não possuem coleta de lixo ou tratamento de exceções. Não há estudo formal que mostre que essa regra leva a um código melhor nas linguagens modernas. Sinta-se à vontade para ignorá-lo sempre que isso levar a um código mais curto ou mais legível. Os caras de Java que insistem nisso são cegos e inquestionáveis seguindo uma regra desatualizada e sem sentido.

Essa pergunta também foi feita no Stackoverflow

    
por 23.05.2017 / 14:40
fonte
6

Um retorno torna a refatoração mais fácil. Tente executar o "método de extração" para o corpo interno de um loop for que contém um retorno, quebra ou continua. Isso falhará quando você tiver quebrado o fluxo de controle.

O ponto é: eu acho que ninguém está fingindo escrever código perfeito. Então, o código está reguaramente sob a refatoração para ser "melhorado" e estendido. Assim, meu objetivo seria manter meu código como refatorando o mais amigável possível.

Muitas vezes, enfrento o problema que tenho de reformular completamente as funções se elas contiverem disjuntores de fluxo de controle e se eu quiser adicionar pouca funcionalidade. Isso é muito propenso a erros, pois você altera todo o fluxo de controle em vez de introduzir novos caminhos para aninhamentos isolados. Se você tem apenas um único retorno no final, ou se você usa guardas para sair de um loop, é claro que você tem mais aninhamento e mais código. Mas você ganha recursos de refatoração compatíveis com o compilador e o IDE.

    
por 16.10.2017 / 21:03
fonte
5

Considere o fato de que várias instruções de retorno são equivalentes a ter GOTOs para uma única instrução de retorno. Este é o mesmo caso com instruções de quebra. Assim, alguns, como eu, consideram-nos GOTO para todos os efeitos.

No entanto, eu não considero esses tipos de GOTO prejudiciais e não hesitarei em usar um GOTO real no meu código se eu encontrar uma boa razão para isso.

Minha regra geral é que os GOTOs são apenas para controle de fluxo. Eles nunca devem ser usados para qualquer looping, e você nunca deve ir para cima ou para trás. (o que é como as quebras / devoluções funcionam)

Como outros já mencionaram, o seguinte é uma leitura obrigatória Declaração do GOTO considerada prejudicial No entanto, tenha em mente que isso foi escrito em 1970, quando os GOTO's foram muito usados. Nem todo GOTO é prejudicial e eu não desencorajaria seu uso, desde que você não os use em vez de construções normais, mas sim no caso estranho de que usar construções normais seria altamente inconveniente.

Acho que usá-los em casos de erro, onde você precisa escapar de uma área por causa de uma falha que nunca deve ocorrer em casos normais, às vezes útil. Mas você também deve considerar colocar este código em uma função separada para que você possa retornar cedo em vez de usar um GOTO ... mas às vezes isso também é inconveniente.

    
por 10.11.2011 / 17:31
fonte
1

Complexidade Ciclomática

Eu vi o SonarCube usar várias declarações de retorno para determinar a complexidade ciclomática. Então, mais as declarações de retorno, quanto maior a complexidade ciclomática

Alterar o tipo de retorno

Vários retornos significam que precisamos mudar em vários lugares na função quando decidimos alterar nosso tipo de retorno.

Saída múltipla

É mais difícil depurar uma vez que a lógica precisa ser cuidadosamente estudada em conjunto com as instruções condicionais para entender o que causou o valor retornado.

Solução Refatorada

A solução para várias instruções de retorno é substituí-las por um polimorfismo com um único retorno após a resolução do objeto de implementação necessário.

    
por 08.10.2018 / 13:33
fonte