Quando executei o ReSharper no meu código, por exemplo:
if (some condition)
{
Some code...
}
O ReSharper me deu o aviso acima (inverta a instrução "if" para reduzir o aninhamento) e sugeriu a seguinte correção:
if (!some condition) return;
Some code...
Eu gostaria de entender por que isso é melhor. Eu sempre pensei que usar "return" no meio de um método problemático, algo como "goto".
ASSERT( exampleParam > 0 )
.Respostas:
Um retorno no meio do método não é necessariamente ruim. Talvez seja melhor retornar imediatamente se tornar mais clara a intenção do código. Por exemplo:
Nesse caso, se
_isDead
for verdade, podemos sair imediatamente do método. Talvez seja melhor estruturá-lo dessa maneira:Eu escolhi esse código do catálogo de refatoração . Essa refatoração específica é chamada: Substituir condicional aninhado por cláusulas de guarda.
fonte
Não é apenas estética , mas também reduz o nível máximo de aninhamento dentro do método. Isso geralmente é considerado uma vantagem porque facilita a compreensão dos métodos (e, de fato, muitas ferramentas de análise estática fornecem uma medida disso como um dos indicadores de qualidade do código).
Por outro lado, também faz com que seu método tenha vários pontos de saída, algo que outro grupo de pessoas acredita ser um não-não.
Pessoalmente, concordo com o ReSharper e o primeiro grupo (em um idioma que tem exceções, acho tolo discutir "vários pontos de saída"; quase tudo pode ser lançado, então existem vários pontos de saída possíveis em todos os métodos).
Em relação ao desempenho : ambas as versões devem ser equivalentes (se não no nível da IL, certamente depois que o tremor terminar com o código) em todos os idiomas. Teoricamente, isso depende do compilador, mas praticamente qualquer compilador amplamente usado hoje em dia é capaz de lidar com casos muito mais avançados de otimização de código que isso.
fonte
Este é um argumento um pouco religioso, mas concordo com o ReSharper que você deve preferir menos aninhamento. Acredito que isso supera os negativos de ter vários caminhos de retorno de uma função.
O principal motivo para ter menos aninhamento é melhorar a legibilidade e a manutenção do código . Lembre-se de que muitos outros desenvolvedores precisarão ler seu código no futuro, e código com menos recuo geralmente é muito mais fácil de ler.
As condições prévias são um ótimo exemplo de onde é bom retornar cedo no início da função. Por que a legibilidade do restante da função deve ser afetada pela presença de uma verificação de pré-condição?
Quanto aos aspectos negativos do retorno várias vezes de um método - os depuradores são bem poderosos agora, e é muito fácil descobrir exatamente onde e quando uma função específica está retornando.
Ter vários retornos em uma função não afetará o trabalho do programador de manutenção.
Legibilidade de código ruim.
fonte
Como outros já mencionaram, não deve haver um impacto no desempenho, mas há outras considerações. Além dessas preocupações válidas, isso também pode abrir você para pegadinhas em algumas circunstâncias. Suponha que você estivesse lidando com um problema
double
:Compare isso com a inversão aparentemente equivalente:
Portanto, em certas circunstâncias, o que parece ser um invertido corretamente
if
pode não ser.fonte
A idéia de retornar apenas no final de uma função voltou nos dias anteriores aos idiomas terem suporte para exceções. Ele permitiu que os programas confiassem em poder colocar o código de limpeza no final de um método e, em seguida, tendo a certeza de que seria chamado e algum outro programador não ocultaria um retorno no método que fazia com que o código de limpeza fosse ignorado. . O código de limpeza ignorado pode resultar em vazamento de memória ou recurso.
No entanto, em um idioma que suporta exceções, ele não oferece tais garantias. Em um idioma que suporta exceções, a execução de qualquer instrução ou expressão pode causar um fluxo de controle que faz com que o método termine. Isso significa que a limpeza deve ser feita através do uso de finalmente ou de palavras-chave.
De qualquer forma, estou dizendo que acho que muitas pessoas citam a diretriz de 'único retorno no final de um método' sem entender por que isso sempre foi uma coisa boa a se fazer, e que reduzir o aninhamento para melhorar a legibilidade é provavelmente um objetivo melhor.
fonte
If you've got deep nesting, maybe your function is trying to do too many things.
=> isso não é uma conseqüência correta da sua frase anterior. Porque, pouco antes de você dizer que pode refatorar o comportamento A com o código C para o comportamento A com o código D., o código D é mais limpo, concedido, mas "muitas coisas" se referem ao comportamento, que NÃO foi alterado. Portanto, você não faz nenhum sentido com esta conclusão.Eu gostaria de acrescentar que existe um nome para os ifs invertidos - Cláusula Guard. Eu uso sempre que posso.
Eu odeio ler código onde existe, se no início, duas telas de código e nada mais. Apenas inverta se e retorne. Dessa forma, ninguém perderá tempo rolando.
http://c2.com/cgi/wiki?GuardClause
fonte
if
s ou não. lolIsso não afeta apenas a estética, mas também evita o aninhamento de código.
Na verdade, ele pode funcionar como uma pré-condição para garantir que seus dados também sejam válidos.
fonte
Isso é obviamente subjetivo, mas acho que melhora fortemente em dois pontos:
condition
mantida.fonte
Vários pontos de retorno eram um problema em C (e, em menor grau, em C ++) porque forçavam você a duplicar o código de limpeza antes de cada um dos pontos de retorno. Com coleta de lixo, o
try
|finally
construção eusing
blocos, não há realmente nenhuma razão pela qual você deve ter medo deles.Em última análise, tudo se resume ao que você e seus colegas acham mais fácil de ler.
fonte
a loop that is not vectorizable due to a second data-dependent exit
Em termos de desempenho, não haverá diferença perceptível entre as duas abordagens.
Mas a codificação é mais do que desempenho. Clareza e manutenção também são muito importantes. E, em casos como este em que não afeta o desempenho, é a única coisa que importa.
Existem escolas de pensamento concorrentes sobre qual abordagem é preferível.
Uma visão é a que outras pessoas mencionaram: a segunda abordagem reduz o nível de aninhamento, o que melhora a clareza do código. Isso é natural em um estilo imperativo: quando você não tem mais nada a fazer, é melhor voltar cedo.
Outra visão, da perspectiva de um estilo mais funcional, é que um método deve ter apenas um ponto de saída. Tudo em uma linguagem funcional é uma expressão. Portanto, as instruções if devem sempre ter cláusulas else. Caso contrário, a expressão if nem sempre terá um valor. Portanto, no estilo funcional, a primeira abordagem é mais natural.
fonte
As cláusulas de guarda ou pré-condições (como você provavelmente pode ver) verificam se uma determinada condição é atendida e, em seguida, interrompe o fluxo do programa. Eles são ótimos para lugares onde você realmente só está interessado em um resultado de uma
if
declaração. Então, ao invés de dizer:Você reverte a condição e quebra se essa condição revertida for atendida
return
não está nem de longe tão sujo quantogoto
. Ele permite que você passe um valor para mostrar ao restante do seu código que a função não pôde ser executada.Você verá os melhores exemplos de onde isso pode ser aplicado em condições aninhadas:
vs:
Você encontrará poucas pessoas argumentando que o primeiro é mais limpo, mas é claro que é completamente subjetivo. Alguns programadores gostam de saber em que condições alguma coisa está operando por indentação, enquanto eu prefiro manter o fluxo do método linear.
Por um momento, não vou sugerir que precons mudem sua vida ou te deixem transar, mas você pode achar seu código um pouco mais fácil de ler.
fonte
Existem vários pontos positivos apresentados aqui, mas vários pontos de retorno também podem ser ilegíveis , se o método for muito longo. Dito isto, se você usar vários pontos de retorno, verifique se o seu método é curto; caso contrário, o bônus de legibilidade de vários pontos de retorno poderá ser perdido.
fonte
O desempenho é dividido em duas partes. Você tem desempenho quando o software está em produção, mas também deseja ter desempenho durante o desenvolvimento e a depuração. A última coisa que um desenvolvedor deseja é "esperar" por algo trivial. No final, compilar isso com a otimização ativada resultará em código semelhante. Portanto, é bom conhecer esses pequenos truques que valem a pena nos dois cenários.
O caso da pergunta é claro, o ReSharper está correto. Em vez de aninhar
if
instruções e criar novo escopo no código, você está definindo uma regra clara no início do seu método. Aumenta a legibilidade, será mais fácil de manter e reduz a quantidade de regras necessárias para descobrir para onde eles querem ir.fonte
Pessoalmente, prefiro apenas 1 ponto de saída. É fácil de realizar se você mantiver seus métodos curtos e direto ao ponto, além de fornecer um padrão previsível para a próxima pessoa que trabalha no seu código.
por exemplo.
Isso também é muito útil se você quiser apenas verificar os valores de determinadas variáveis locais dentro de uma função antes que ela saia. Tudo o que você precisa fazer é colocar um ponto de interrupção no retorno final e você tem a garantia de atingi-lo (a menos que uma exceção seja lançada).
fonte
Muitas boas razões para a aparência do código . Mas e os resultados ?
Vamos dar uma olhada em algum código C # e seu formulário compilado IL:
Esse snippet simples pode ser compilado. Você pode abrir o arquivo .exe gerado com o ildasm e verificar qual é o resultado. Não vou postar toda a coisa do montador, mas vou descrever os resultados.
O código IL gerado faz o seguinte:
Então, parece que o código vai pular para o final. E se fizermos um normal se com código aninhado?
Os resultados são bastante semelhantes nas instruções de IL. A diferença é que antes havia saltos por condição: se false, vá para o próximo trecho de código, se true, vá para o fim. E agora o código IL flui melhor e tem 3 saltos (o compilador otimizou isso um pouco): 1. Primeiro salto: quando Length é 0 para uma parte em que o código salta novamente (terceiro salto) até o fim. 2. Segundo: no meio da segunda condição para evitar uma instrução. 3. Terceiro: se a segunda condição for falsa, pule para o final.
De qualquer forma, o contador de programas sempre pulará.
fonte
Em teoria, a inversão
if
pode levar a um melhor desempenho se aumentar a taxa de acerto de previsão de ramificação. Na prática, acho muito difícil saber exatamente como a previsão de ramificação se comportará, especialmente após a compilação, para que eu não o faça no desenvolvimento do dia a dia, exceto se estiver escrevendo código de montagem.Mais sobre previsão de ramificação aqui .
fonte
Isso é simplesmente controverso. Não existe "acordo entre programadores" sobre a questão do retorno antecipado. É sempre subjetivo, tanto quanto eu sei.
É possível fazer um argumento de desempenho, já que é melhor ter condições escritas para que elas sejam verdadeiras com mais freqüência; também se pode argumentar que é mais claro. Por outro lado, cria testes aninhados.
Eu não acho que você receberá uma resposta conclusiva para esta pergunta.
fonte
Já existem muitas respostas perspicazes, mas ainda assim eu gostaria de direcionar para uma situação um pouco diferente: em vez da pré-condição, que deve ser colocada no topo de uma função, pense em uma inicialização passo a passo, onde você é necessário verificar se cada etapa é bem-sucedida e, em seguida, continuar com a próxima. Nesse caso, você não pode verificar tudo no topo.
Achei meu código realmente ilegível ao escrever um aplicativo host ASIO com o ASIOSDK de Steinberg, enquanto seguia o paradigma de aninhamento. Foi como oito níveis de profundidade, e não consigo ver uma falha de design lá, como mencionado por Andrew Bullock acima. Obviamente, eu poderia ter empacotado algum código interno em outra função e depois aninhado os níveis restantes para torná-lo mais legível, mas isso me parece bastante aleatório.
Ao substituir o aninhamento por cláusulas de guarda, eu até descobri um equívoco meu sobre uma parte do código de limpeza que deveria ter ocorrido muito mais cedo na função, em vez de no final. Com galhos aninhados, eu nunca teria visto isso, você poderia até dizer que eles levaram ao meu equívoco.
Portanto, essa pode ser outra situação em que os ifs invertidos podem contribuir para um código mais claro.
fonte
Evitar vários pontos de saída pode levar a ganhos de desempenho. Não tenho certeza sobre C #, mas em C ++ a otimização do valor de retorno nomeado (Copy Elision, ISO C ++ '03 12.8 / 15) depende de ter um único ponto de saída. Essa otimização evita que a cópia construa seu valor de retorno (no seu exemplo específico, isso não importa). Isso pode levar a ganhos consideráveis de desempenho em loops apertados, pois você está salvando um construtor e um destruidor cada vez que a função é invocada.
Porém, para 99% dos casos, salvar as chamadas adicionais de construtor e destruidor não vale a perda de legibilidade
if
introduzida pelos blocos aninhados (como outros já apontaram).fonte
É uma questão de opinião.
Minha abordagem normal seria evitar ifs de linha única e retornar no meio de um método.
Você não gostaria de linhas como as sugeridas em todos os lugares do seu método, mas há algo a ser dito para verificar várias suposições no topo do seu método, e apenas fazer o seu trabalho real se todas forem aprovadas.
fonte
Na minha opinião, o retorno antecipado é bom se você está retornando nulo (ou algum código de retorno inútil que você nunca vai verificar) e pode melhorar a legibilidade porque você evita o aninhamento e ao mesmo tempo explicita que sua função está concluída.
Se você realmente está retornando um returnValue - o aninhamento geralmente é a melhor maneira de fazê-lo, pois você retorna seu returnValue apenas em um local (no final - duh), e isso pode tornar seu código mais sustentável em muitos casos.
fonte
Não tenho certeza, mas acho que o R # tenta evitar saltos distantes. Quando você possui o IF-ELSE, o compilador faz algo assim:
Condição false -> salto em distância para false_condition_label
true_condition_label: instrução1 ... instrução_n
false_condition_label: instrução1 ... instrução_n
bloco final
Se a condição for verdadeira, não há salto nem cache L1 de lançamento, mas o salto para false_condition_label pode estar muito longe e o processador deve lançar seu próprio cache. A sincronização do cache é cara. O R # tenta substituir saltos distantes em saltos curtos e, nesse caso, há uma probabilidade maior de que todas as instruções já estejam no cache.
fonte
Eu acho que depende do que você preferir, como mencionado, não existe um acordo geral. Para reduzir o aborrecimento, você pode reduzir esse tipo de aviso para "Dica"
fonte
Minha ideia é que o retorno "no meio de uma função" não seja tão "subjetivo". O motivo é bem simples, pegue este código:
Talvez o primeiro "retorno" não seja tão intuitivo, mas é realmente muito bom. Existem muitas "idéias" sobre códigos limpos, que simplesmente precisam de mais prática para perder suas más idéias "subjetivas".
fonte
Existem várias vantagens nesse tipo de codificação, mas para mim a grande vitória é que, se você puder retornar rapidamente, poderá melhorar a velocidade do seu aplicativo. Ou seja, eu sei que, devido à pré-condição X, posso retornar rapidamente com um erro. Isso elimina os casos de erro primeiro e reduz a complexidade do seu código. Em muitos casos, como o pipeline da CPU agora pode ser mais limpo, ele pode interromper falhas ou interruptores no pipeline. Em segundo lugar, se você estiver em um loop, interromper ou retornar rapidamente pode economizar uma grande quantidade de CPU. Alguns programadores usam invariantes de loop para fazer esse tipo de saída rápida, mas com isso você pode interromper o pipeline da CPU e até criar um problema de busca de memória e significa que a CPU precisa carregar do cache externo. Mas basicamente acho que você deve fazer o que pretendia, que é o fim do loop ou a função não cria um caminho de código complexo apenas para implementar alguma noção abstrata de código correto. Se a única ferramenta que você tem é um martelo, tudo parece um prego.
fonte