ti-enxame.com

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

Costumo conversar com programadores que dizem "Não coloque várias instruções de retorno no mesmo método." Quando peço que eles me digam os motivos, tudo o que recebo é "A codificação o padrão diz isso. "ou" É confuso. "Quando eles me mostram soluções com uma única declaração de retorno, o código fica mais feio para mim. Por exemplo:

if (condition)
   return 42;
else
   return 97;

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

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

Como esse inchaço de código de 50% facilita a compreensão do programa? Pessoalmente, acho isso mais difícil, porque o espaço de estado acaba de aumentar por outra variável que poderia ser facilmente evitada.

Claro, normalmente eu apenas escreveria:

return (condition) ? 42 : 97;

Mas muitos programadores evitam o operador condicional e preferem a forma longa.

De onde veio essa noção de "apenas um retorno"? Existe uma razão histórica para a criação desta convenção?

1077
fredoverflow

"Entrada única, saída única" foi escrita quando a maior parte da programação era feita na linguagem Assembly, FORTRAN ou COBOL. Foi amplamente mal interpretado, porque as línguas modernas não suportam as práticas contra as quais Dijkstra estava advertindo.

"Entrada única" significava "não cria pontos de entrada alternativos para funções". Na linguagem Assembly, é claro, é possível inserir uma função em qualquer instruçã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)

"Saída única" significava que uma função deveria retornar apenas para um lugar: a instrução imediatamente após a chamada. não significa que uma função deve retornar apenas de um local. Quando Programação Estruturada foi escrita, era prática comum para uma função indicar um erro retornando para um local alternativo. FORTRAN apoiou isso através de "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 geralmente deixa algumas variáveis ​​não inicializadas. O uso de retornos alternativos apresentava todos os problemas de uma instrução GOTO, com a complicação adicional de que a condição de ramificação não estava adjacente à ramificação, mas em algum lugar da sub-rotina.

1145
kevin cline

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ódigos como este vazarão recursos:

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

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

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

  • Use um goto para pular 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.)

  • Introduzir uma variável local e manipular o fluxo de controle através disso.
    A desvantagem é que o fluxo de controle manipulado através da sintaxe (é muito mais fácil seguir do que manipular break, return, if, while) fluxo de controle manipulado através do estado das variáveis ​​(porque essas variáveis ​​não têm estado quando você olha para o algoritmo).

Na montagem, é ainda mais estranho, porque você pode pular para qualquer endereço de uma função ao chamá-la, 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 do ponteiro this 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 entrada ou saída de uma função em qualquer lugar leva a um código mais complexo e, portanto, a erros. Portanto, surgiu uma escola de pensamento que propagou o SESE, a fim de obter um código mais limpo e menos erros.


No entanto, quando um idioma apresenta exceções, (quase) qualquer função pode ser encerrada prematuramente (quase) a qualquer momento, portanto, você precisa prever o retorno prematuro de qualquer maneira. (Eu acho que finally é usado principalmente para isso em Java e using (ao implementar IDisposable, finally caso contrário) em C #; C++, em vez disso, emprega RAII .) Depois de fazer isso, você não pode falha na limpeza depois de si próprio devido a uma declaração inicial return, então o que é provavelmente o argumento mais forte a favor do SESE desapareceu.

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

Nos idiomas em que 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, o SESE geralmente torna o código mais complexo. É um dinossauro que (exceto C) não se encaixa bem na maioria das línguas atuais. Em vez de ajudar a compreensibilidade do código, ele dificulta.


Por que Java mantêm isso? Eu não sei, mas pelo meu POV (externo)), Java tomou muitas convenções de C (onde eles fazem sentido) e os aplicaram ao seu mundo OO (onde são inúteis ou totalmente ruins)), onde agora se apega a eles, independentemente dos custos. (como a convenção para definir todos suas variáveis ​​no início do escopo.)

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

921
sbi

Por um lado, as instruções de retorno único facilitam o log, bem como as formas de depuração que dependem do log. Lembro-me de muitas vezes que tive que reduzir a função em retorno único 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.

82
perreal

"Entrada única, saída única" se originou com a revolução da programação estruturada do início dos anos 1970, iniciada pela carta de Edsger W. Dijkstra ao editor " Declaração GOTO considerada prejudicial ". Os conceitos por trás da programação estruturada foram detalhados no livro clássico "Structured Programming" de Ole Johan-Dahl, Edsger W. Dijkstra e Charles Anthony Richard Hoare.

A "declaração GOTO considerada prejudicial" é leitura obrigatória, ainda hoje. A "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, por exemplo Steve McConnell. (A seção de Dahl expõe os conceitos básicos de classes no Simula 67, que são a base técnica para classes em C++ e toda a programação orientada a objetos.)

53
John R. Strohm

Sempre é fácil vincular o Fowler.

Um dos principais exemplos contrários ao SESE são as cláusulas de guarda:

Substituir condicional aninhado por cláusulas de guarda

Use cláusulas de guarda para todos os casos especiais

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

Para obter mais informações, consulte a página 250 de Refatoração ...

36
Pieter B

Eu escrevi um post sobre este tópico há um tempo.

A conclusão é que essa regra vem da era dos idiomas que não têm coleta de lixo ou manipulação de exceção. Não há estudo formal que mostre que essa regra leva a um melhor código nas linguagens modernas. Fique à vontade para ignorá-lo sempre que isso levar a um código mais curto ou mais legível. Os caras do Java que insistem nisso são cegos e inquestionáveis, seguindo uma regra desatualizada e sem sentido.

Esta pergunta também foi feita no Stackoverflow

11
Anthony

Um retorno facilita a refatoração. Tente executar o "método de extração" no corpo interno de um loop for que contenha retorno, interrupção ou continuação. Isso falhará quando você interromper seu fluxo de controle.

O ponto é: acho que ninguém está fingindo escrever um código perfeito. Portanto, o código está regularmente sob refatoração para ser "aprimorado" e estendido. Portanto, meu objetivo seria manter meu código o mais amigável possível para a refatoração.

Muitas vezes, enfrento o problema de reformular completamente as funções se elas contiverem interruptores de fluxo de controle e se desejar adicionar pouca funcionalidade. Isso é muito suscetível a erros, pois você altera todo o fluxo de controle em vez de introduzir novos caminhos para aninhamentos isolados. Se você tiver apenas um único retorno no final ou se usar guardas para sair de um loop, é claro que terá mais aninhamento e mais código. Mas você ganha compilador e IDE suporta recursos de refatoração.

6
oopexpert

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

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

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

Como outros já mencionaram, é necessário ler a seguir Declaração GOTO considerada prejudicial
No entanto, lembre-se de que isso foi escrito em 1970, quando os GOTOs eram muito usados. Nem todo GOTO é prejudicial e eu não desencorajaria o uso deles, desde que você não os use em vez de construções normais, mas, no caso estranho, o uso de construções normais seria altamente inconveniente.

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

5
user606723

Complexidade ciclomática

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

Alteração de tipo de retorno

Retornos múltiplos significam que precisamos alterar em vários locais da função quando decidimos alterar nosso tipo de retorno.

Saída múltipla

É mais difícil depurar, pois 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 pelo polimorfismo e ter um único retorno após a resolução do objeto de implementação necessário.

4
Sorter