Por que uma função deve ter apenas um ponto de saída? [fechadas]

97

Sempre ouvi falar de uma função de ponto de saída único como uma maneira ruim de codificar porque você perde capacidade de leitura e eficiência. Nunca ouvi ninguém argumentar do outro lado.

Achei que isso tivesse algo a ver com CS, mas essa questão foi descartada em cstheory stackexchange.

hex bob-omb
fonte
6
A resposta é que não há uma resposta sempre certa. Costumo achar mais fácil codificar com várias saídas. Também descobri (ao atualizar o código acima) que modificar / estender o código era mais difícil devido às mesmas saídas múltiplas. Tomar essas decisões caso a caso é o nosso trabalho. Quando uma decisão sempre tem uma "melhor" resposta, não há necessidade de nós.
JS.
1
@finnw, os mods fascistas removeram as duas últimas perguntas, para ter certeza de que elas terão que ser respondidas novamente, e novamente, e novamente
Maarten Bodewes
Apesar da palavra "argumentar" na pergunta, realmente não acho que seja uma pergunta baseada em opinião. É bastante relevante para um bom design, etc. Não vejo razão para ser fechado, mas w / e.
Ungeheuer
1
Um único ponto de saída simplifica a depuração, leitura, medição e ajuste de desempenho, refatoração. Isso é objetivo e materialmente significativo. O uso de retornos antecipados (após verificações de argumento simples) contribui para uma mistura sensata de ambos os estilos. Dados os benefícios de um único ponto de saída, encher seu código com valores de retorno é simplesmente evidência de um programador preguiçoso, desleixado e descuidado - e pelo menos possivelmente não gosta de cachorros.
Rick O'Shea

Respostas:

108

Existem diferentes escolas de pensamento, e em grande parte se resume à preferência pessoal.

Uma é que é menos confuso se houver apenas um único ponto de saída - você tem um único caminho através do método e sabe onde procurar a saída. No lado negativo, se você usar indentação para representar aninhamento, seu código acabará recuado massivamente para a direita e se tornará muito difícil seguir todos os escopos aninhados.

Outra é que você pode verificar as pré-condições e sair mais cedo no início de um método, de modo que saiba no corpo do método que certas condições são verdadeiras, sem que todo o corpo do método seja recuado 5 milhas para a direita. Isso geralmente minimiza o número de escopos com os quais você precisa se preocupar, o que torna o código muito mais fácil de seguir.

Uma terceira é que você pode sair de onde quiser. Isso costumava ser mais confuso no passado, mas agora que temos editores e compiladores para colorir sintaxe que detectam código inacessível, é muito mais fácil lidar com ele.

Estou exatamente no meio do campo. Impor um único ponto de saída é uma restrição IMHO inútil ou mesmo contraproducente, enquanto sair aleatoriamente em um método pode às vezes levar a uma lógica complicada e difícil de seguir, onde se torna difícil ver se um determinado pedaço de código será ou não executado. Mas "bloquear" o seu método torna possível simplificar significativamente o corpo do método.

Jason Williams
fonte
1
O aninhamento profundo pode ser evitado no singe exitparadigma por meio de go todeclarações. Além disso, tem-se a oportunidade de realizar algum pós-processamento sob o Errorrótulo local da função , o que é impossível com vários returns.
Ant_222
2
Geralmente, há uma boa solução que evita a necessidade de ir ao. Eu prefiro 'return (Fail (...))' e colocar o código de limpeza compartilhado no método Fail. Isso pode exigir a passagem de alguns locais para permitir que a memória seja liberada, etc., mas, a menos que você esteja em um código de desempenho crítico, geralmente é uma solução muito mais limpa do que um goto IMO. Ele também permite que vários métodos compartilhem códigos de limpeza semelhantes.
Jason Williams,
Existe uma abordagem ótima baseada em critérios objetivos, mas podemos concordar que existem escolas de pensamento (corretas e incorretas) e isso se resume à preferência pessoal (uma preferência a favor ou contra uma abordagem correta).
Rick O'Shea
39

Minha recomendação geral é que as instruções de retorno devem, quando possível, ser localizadas antes do primeiro código que tem efeitos colaterais ou depois do último código que tem efeitos colaterais. Eu consideraria algo como:

  if (! argumento) // Verifique se não é nulo
    return ERR_NULL_ARGUMENT;
  ... processar argumento não nulo
  se (ok)
    return 0;
  outro
    return ERR_NOT_OK;

mais claro que:

  int return_value;
  if (argumento) // Não nulo
  {
    .. processar argumento não nulo
    .. definir o resultado apropriadamente
  }
  outro
    resultado = ERR_NULL_ARGUMENT;
  resultado de retorno;

Se uma determinada condição deve impedir uma função de fazer qualquer coisa, prefiro retornar antes da função em um ponto acima do ponto onde a função faria qualquer coisa. Uma vez que a função tenha empreendido ações com efeitos colaterais, porém, prefiro voltar do fundo, para deixar claro que todos os efeitos colaterais devem ser tratados.

supergato
fonte
Seu primeiro exemplo, gerenciando a okvariável, parece a abordagem de retorno único para mim. Além disso, o bloco if-else pode ser simplesmente reescrito para:return ok ? 0 : ERR_NOT_OK;
Melebius
2
O primeiro exemplo tem um returnno início antes de todo o código que faz tudo. Quanto ao uso do ?:operador, escrevê-lo em linhas separadas torna mais fácil em muitos IDEs anexar um ponto de interrupção de depuração ao cenário não ok. BTW, a verdadeira chave para "único ponto de saída" está em entender que o que importa é que para cada chamada específica para uma função normal, o ponto de saída é o ponto imediatamente após a chamada . Os programadores hoje em dia consideram isso normal, mas as coisas nem sempre foram assim. Em alguns casos raros, o código pode ter que sobreviver sem espaço de pilha, levando a funções ...
supercat
... que saem via gotos condicionais ou computados. Geralmente, qualquer coisa com recursos suficientes para ser programado em qualquer coisa que não seja a linguagem assembly será capaz de suportar uma pilha, mas eu escrevi o código assembly que teve que operar sob algumas restrições muito rígidas (até ZERO bytes de RAM em um caso), e ter vários pontos de saída pode ser útil nesses casos.
supercat
1
O chamado exemplo mais claro é muito menos claro e difícil de ler. Um ponto de saída é sempre mais fácil de ler, manter e depurar.
GuidoG
8
@GuidoG: Qualquer um dos padrões pode ser mais legível, dependendo do que aparece nas seções omitidas. Usando "return x;" deixa claro que se a instrução for alcançada, o valor de retorno será x. Usando "resultado = x;" deixa em aberto a possibilidade de que algo mais possa alterar o resultado antes que ele seja retornado. Isso pode ser útil se de fato for necessário alterar o resultado, mas os programadores que examinam o código teriam que inspecioná-lo para ver como o resultado pode ser alterado mesmo se a resposta for "não pode".
supercat
15

Um único ponto de entrada e saída era o conceito original de programação estruturada vs Spaghetti Coding passo a passo. Acredita-se que múltiplas funções de ponto de saída requerem mais código, uma vez que é necessário limpar adequadamente os espaços de memória alocados para as variáveis. Considere um cenário em que a função aloca variáveis ​​(recursos) e sair da função mais cedo e sem a limpeza adequada resultaria em vazamentos de recursos. Além disso, construir uma limpeza antes de cada saída criaria uma grande quantidade de código redundante.

PSS
fonte
Isso realmente não é um problema com RAII
BigSandwich 01 de
14

Com quase tudo, tudo se resume às necessidades do produto final. Nos "velhos tempos", o código espaguete com vários pontos de retorno provocava vazamentos de memória, já que os programadores que preferiam esse método normalmente não faziam uma boa limpeza. Também houve problemas com alguns compiladores "perdendo" a referência à variável de retorno quando a pilha foi estourada durante o retorno, no caso de retornar de um escopo aninhado. O problema mais geral era o do código reentrante, que tenta fazer com que o estado de chamada de uma função seja exatamente o mesmo que seu estado de retorno. Mutadores de oop violaram isso e o conceito foi arquivado.

Existem produtos, principalmente kernels, que precisam da velocidade que vários pontos de saída fornecem. Esses ambientes normalmente têm sua própria memória e gerenciamento de processos, de modo que o risco de vazamento é minimizado.

Pessoalmente, gosto de ter um único ponto de saída, já que costumo usá-lo para inserir um ponto de interrupção na instrução de retorno e executar uma inspeção de código de como o código determinou essa solução. Eu poderia simplesmente ir até a entrada e passar, o que faço com soluções extensivamente aninhadas e recursivas. Como revisor de código, vários retornos em uma função requerem uma análise muito mais profunda - portanto, se você está fazendo isso para acelerar a implementação, está roubando Peter para salvar Paul. Mais tempo será necessário nas revisões de código, invalidando a presunção de implementação eficiente.

- 2 centavos

Consulte este documento para obter mais detalhes: NISTIR 5459

sscheider
fonte
8
multiple returns in a function requires a much deeper analysisapenas se a função já for enorme (> 1 tela), caso contrário, torna a análise mais fácil
dss539
2
retornos múltiplos nunca tornam a análise mais fácil, apenas o
oposto
1
o link está morto (404).
fusi
1
@fusi - encontrei em archive.org e atualizei o link aqui
sscheider de
4

Em minha opinião, o conselho para sair de uma função (ou outra estrutura de controle) em apenas um ponto muitas vezes é exagerado. Normalmente, são apresentados dois motivos para sair em apenas um ponto:

  1. O código de saída única é supostamente mais fácil de ler e depurar. (Admito que não penso muito sobre esse motivo, mas é dado. O que é substancialmente mais fácil de ler e depurar é o código de entrada única .)
  2. O código de saída única vincula e retorna de forma mais limpa.

O segundo motivo é sutil e tem algum mérito, especialmente se a função retornar uma grande estrutura de dados. No entanto, eu não me preocuparia muito com isso, exceto ...

Se for aluno, você deseja obter as melhores notas em sua classe. Faça o que o instrutor preferir. Ele provavelmente tem um bom motivo de sua perspectiva; então, no mínimo, você aprenderá sua perspectiva. Isso tem valor em si.

Boa sorte.

thb
fonte
4

Eu costumava ser um defensor do estilo de saída única. Meu raciocínio veio principalmente da dor ...

A saída única é mais fácil de depurar.

Dadas as técnicas e ferramentas que temos hoje, essa é uma posição muito menos razoável de se tomar, pois os testes de unidade e o registro podem tornar a saída única desnecessária. Dito isso, quando você precisa observar a execução do código em um depurador, é muito mais difícil entender e trabalhar com o código que contém vários pontos de saída.

Isso se tornou especialmente verdadeiro quando você precisava interpor atribuições para examinar o estado (substituído por expressões de observação em depuradores modernos). Também era muito fácil alterar o fluxo de controle de maneiras que ocultassem o problema ou interrompessem totalmente a execução.

Os métodos de saída única eram mais fáceis de percorrer no depurador e mais fáceis de separar sem quebrar a lógica.

Michael Murphree
fonte
0

A resposta depende muito do contexto. Se você estiver fazendo uma GUI e tiver uma função que inicializa APIs e abre janelas no início de sua principal, ela estará cheia de chamadas que podem lançar erros, cada um dos quais faria com que a instância do programa fosse fechada. Se você usasse instruções IF aninhadas e indentasse, seu código poderia rapidamente ficar muito inclinado para a direita. Retornar um erro em cada estágio pode ser melhor e realmente mais legível, além de ser igualmente fácil de depurar com alguns sinalizadores no código.

Se, entretanto, você estiver testando condições diferentes e retornando valores diferentes dependendo dos resultados em seu método, pode ser uma prática muito melhor ter um único ponto de saída. Eu costumava trabalhar com scripts de processamento de imagens no MATLAB, que podiam ficar muito grandes. Vários pontos de saída podem tornar o código extremamente difícil de seguir. As declarações switch eram muito mais apropriadas.

A melhor coisa a fazer seria aprender conforme você avança. Se você está escrevendo código para algo, tente encontrar o código de outras pessoas e ver como elas o implementam. Decida quais partes você gosta e quais não gosta.

Flash_Steel
fonte
-5

Se você sentir que precisa de vários pontos de saída em uma função, a função é muito grande e está executando muito.

Eu recomendaria a leitura do capítulo sobre funções no livro de Robert C. Martin, Clean Code.

Essencialmente, você deve tentar escrever funções com 4 linhas de código ou menos.

Algumas notas do blog de Mike Long :

  • A primeira regra de funções: devem ser pequenas
  • A segunda regra de funções: eles devem ser menores do que isso
  • Blocos dentro de instruções if, instruções while, loops for, etc devem ter uma linha de comprimento
  • … E essa linha de código geralmente será uma chamada de função
  • Não deve haver mais do que um ou talvez dois níveis de indentação
  • As funções devem fazer uma coisa
  • As declarações de função devem estar todas no mesmo nível de abstração
  • Uma função não deve ter mais de 3 argumentos
  • Os argumentos de saída são um cheiro de código
  • Passar um sinalizador booleano em uma função é realmente terrível. Você está, por definição, fazendo duas coisas - na função.
  • Os efeitos colaterais são mentiras.
Roger Dahl
fonte
29
4 linhas? O que você codifica que permite tanta simplicidade? Eu realmente duvido que o kernel do Linux ou git, por exemplo, faça isso.
shinzou
7
"Passar um sinalizador booleano em uma função é realmente terrível. Você está, por definição, fazendo duas coisas --coisas na função." Por definição? Não ... esse booleano poderia afetar apenas uma de suas quatro linhas. Além disso, embora eu concorde em manter o tamanho da função pequeno, quatro é um pouco restritivo. Isso deve ser considerado uma orientação muito vaga.
Jesse
12
Adicionar restrições como essas inevitavelmente tornará o código confuso. É mais sobre os métodos serem claros, concisos e se limitarem a fazer apenas o que devem fazer, sem efeitos colaterais desnecessários.
Jesse
10
Uma das raras respostas no SO que eu gostaria de poder votar contra ele várias vezes.
Steven Rands,
8
Infelizmente, essa resposta está fazendo várias coisas e provavelmente precisava ser dividida em várias outras - todas com menos de quatro linhas.
Eli