Invertendo uma instrução IF

39

Então, eu estou programando há alguns anos e recentemente comecei a usar o ReSharper mais. Uma coisa que o ReSharper sempre me sugere é "inverter 'se' para reduzir o aninhamento".

Digamos que eu tenho esse código:

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
    }
}

E o ReSharper sugerirá que eu faça isso:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

Parece que o ReSharper SEMPRE sugere que eu inverta os IFs, não importa quanto aninhamento esteja acontecendo. Esse problema é que eu meio que gosto de aninhar em pelo menos algumas situações. Para mim, parece mais fácil ler e descobrir o que está acontecendo em certos lugares. Nem sempre é esse o caso, mas às vezes me sinto mais confortável ao aninhar.

Minha pergunta é: além da preferência pessoal ou legibilidade, existe um motivo para reduzir o aninhamento? Existe uma diferença de desempenho ou outra coisa que eu possa não estar ciente?

Barry Franklin
fonte
1
Possível duplicata do nível de indentação
gnat
24
A principal coisa aqui é "Resharper Suggests". Dê uma olhada na sugestão, se ela facilitar a leitura do código e for consistente, use-a. Faz o mesmo com / para cada loop.
Jon Raynor
Geralmente, rebaixo essas dicas de afiação, nem sempre as sigo, para que elas não apareçam mais na barra lateral.
CodesInChaos
1
O ReSharper removeu o negativo, o que geralmente é uma coisa boa. No entanto, não sugeriu uma condicional ternária que poderia se encaixar muito bem aqui, se o bloco for realmente tão simples quanto o seu exemplo.
22617 dcorking
2
O ReSharper também não sugere mover o condicional para a consulta? foreach (someObject em someObjectList.Where (object => object! = null)) {...}
Roman Reiner

Respostas:

63

Depende. No seu exemplo, ter a ifdeclaração não invertida não é um problema, porque o corpo da ifé muito curto. No entanto, você geralmente deseja inverter as declarações quando os corpos crescerem.

Somos apenas humanos e é difícil manter várias coisas ao mesmo tempo em nossas cabeças.

Quando o corpo de uma declaração é grande, a inversão não apenas reduz o aninhamento, mas também informa ao desenvolvedor que eles podem esquecer tudo o que está acima da declaração e se concentrar apenas no restante. É muito provável que você use as instruções invertidas para se livrar de valores errados o mais rápido possível. Depois que você souber que eles estão fora do jogo, a única coisa que resta são valores que realmente podem ser processados.

Andy
fonte
64
Gosto de ver a maré da opinião do desenvolvedor indo dessa maneira. Há tanto aninhamento no código simplesmente porque existe uma ilusão popular extraordinária que break, continuee múltiplos returnnão são estruturados.
JimmyJames
6
o problema está quebrado, etc ainda são o fluxo de controle que você deve manter em mente 'esse não pode ser x ou teria saído do loop no topo' talvez não precise de mais reflexão se for uma verificação nula que, em qualquer caso, abriria uma exceção. Mas não é tão bom quando o seu mais nuances 'só correr metade deste código para esse status às quintas-feiras'
Ewan
2
@ Ewan: Qual é a diferença entre 'este não pode ser x ou teria saído do loop no topo' e 'isso não pode ser x ou não teria entrado nesse bloco'?
Collin
nada é a complexidade e significado do x que é importante
Ewan
4
@Ewan: Eu acho que break/ continue/ returnter uma vantagem real sobre um elsebloco. Com saídas antecipadas, existem 2 quarteirões : a saída e a estadia lá; com elseexistem 3 blocos : if, else, e o que vier depois (que é usado independentemente do ramo utilizado). Eu prefiro ter menos blocos.
26817 Matthieu M.
33

Não evite negativos. Os seres humanos sugam como analisá-los, mesmo quando a CPU os ama.

No entanto, respeite os idiomas. Nós, seres humanos, somos criaturas de hábitos, por isso preferimos caminhos bem gastos a caminhos diretos cheios de ervas daninhas.

foo != null, infelizmente, tornou-se um idioma. Eu mal percebo mais as partes separadas. Eu olho para isso e penso: "oh, é seguro destacar fooagora".

Se você quiser derrubar isso (oh, um dia, por favor), precisará de um argumento realmente forte. Você precisa de algo que permita que meus olhos deslizem sem esforço e entendam em um instante.

No entanto, o que você nos deu foi o seguinte:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

O que nem é estruturalmente o mesmo!

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    if(someGlobalObject == null) continue;
    someSideEffectObject = someGlobalObject.SomeProperty;
}

Isso não está fazendo o que meu cérebro preguiçoso esperava. Eu pensei que poderia continuar adicionando código independente, mas não posso. Isso me faz pensar em coisas que não quero pensar agora. Você quebrou meu idioma, mas não me deu um melhor. Tudo porque você queria me proteger daquele negativo que queimou, é um desagradável ego em minha alma.

Desculpe, talvez o ReSharper esteja certo ao sugerir isso 90% das vezes, mas em verificações nulas, acho que você encontrou um caso de esquina onde é melhor ignorá-lo. As ferramentas simplesmente não são boas para ensinar o que é um código legível. Pergunte a um humano. Faça uma revisão por pares. Mas não siga cegamente a ferramenta.

candied_orange
fonte
1
Bem, se você tiver mais código no loop, como ele funciona depende de como tudo acontece. Não é um código independente. O código refatorado na pergunta estava correto para o exemplo, mas pode não estar correto quando não está isolado - era esse o objetivo? (+1 para os que não evitam negativos)
Baldrickk
O @Baldrickk if (foo != null){ foo.something() }me permite continuar adicionando código sem me importar se fooera nulo. Isso não acontece. Mesmo que eu não precise fazer isso agora, não me sinto motivado a estragar o futuro dizendo "bem, eles podem refatorá-lo se precisarem". Feh. O software é macio apenas se for fácil mudar.
Candied_orange
4
"continue adicionando código sem se preocupar" O código deve estar relacionado (caso contrário, provavelmente deve ser separado) ou deve-se tomar cuidado para entender a funcionalidade da função / bloco que você está modificando, pois a adição de qualquer código pode alterar o comportamento além do pretendido. Em casos simples como esse, não acho que isso importe. Para casos mais complexos, eu esperaria que a compreensão do código precisasse, portanto, escolheria o que eu achasse mais claro, que poderia ser o estilo.
precisa
relacionados e interligados não são a mesma coisa.
68717 Julian
1
Não evite negativos: D
VitalyB 28/07/17
20

É o velho argumento sobre se uma pausa é pior do que o aninhamento.

As pessoas parecem aceitar retorno antecipado para verificações de parâmetros, mas isso é desaprovado na maior parte de um método.

Pessoalmente, evito continuar, quebrar, ir etc., mesmo que isso signifique mais aninhamento. Mas na maioria dos casos, você pode evitar os dois.

foreach (someObject in someObjectList.where(i=>i != null))
{
    someOtherObject = someObject.SomeProperty;
}

Obviamente, o aninhamento dificulta as coisas quando você tem muitas camadas

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
            }
        }
    }
}

O pior é quando você tem ambos

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
                return;
            }
            continue;
        }
        someObject.z --;
        if(myFunctionWithSideEffects(someObject) > 6) break;
    }
}
Ewan
fonte
11
Adoro esta linha: foreach (subObject in someObjectList.where(i=>i != null))não sei por que nunca pensei em algo assim.
Barry Franklin
O @BarryFranklin ReSharper deve ter um sublinhado verde pontilhado no foreach com "Convert to LINQ-expression" como a sugestão que faria isso por você. Dependendo da operação, ele também pode executar outros métodos linq, como Sum ou Max.
Kevin Fee
@ BarryFranklin É provavelmente uma sugestão que você deseja definir em um nível baixo e usar apenas discretamente. Embora funcione bem para casos triviais como este exemplo, encontro em códigos mais complexos a maneira como ele seleciona partes mais complexas em bits que são Linqificáveis ​​e o corpo restante tende a criar coisas muito mais difíceis de entender do que o original. Geralmente tentarei pelo menos a sugestão, porque quando ele pode converter um loop inteiro no Linq, os resultados geralmente são razoáveis; mas metade e metade dos resultados tendem a ter IMO rápido e feio.
Dan Neely
Ironicamente, a edição pelo resharper tem exatamente o mesmo nível de aninhamento, porque também possui um if seguido por um one-liner.
Peter Peter
heh, sem aparelho! que aparentemente é permitido: /
Ewan
6

O problema continueé que, se você adicionar mais linhas ao código, a lógica ficará complicada e provavelmente conterá erros. por exemplo

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    ...  imagine that there is some code here ...

    // added later by a Junior Developer
    doSomeOtherFunction(someObject);
}

Você quer chamar doSomeOtherFunction()para todos someObject, ou apenas se não nulo? Com o código original, você tem a opção e é mais clara. Com a continuepessoa pode esquecer "ah, sim, lá atrás, pulamos nulos" e você nem tem a opção de chamá-la para todos os alguns objetos, a menos que você faça essa chamada no início do método, o que pode não ser possível ou correto por outras razões.

Sim, muitos aninhamentos são feios e possivelmente confusos, mas nesse caso eu usaria o estilo tradicional.

Pergunta de bônus: Por que existem nulos nessa lista para começar? Talvez seja melhor nunca adicionar um nulo em primeiro lugar; nesse caso, a lógica de processamento é muito mais simples.

user949300
fonte
12
Não deve haver código suficiente dentro do loop para que você não possa ver a verificação nula na parte superior sem rolar.
Bryan Boettcher
@Bryan Boettcher concordou, mas nem todo o código está tão bem escrito. E mesmo várias linhas de código complexo podem fazer com que se esqueça (ou se engane) a continuação. Na prática, eu estou bem com returns porque eles formam uma "pausa limpa", mas a IMO breake continuecausa confusão e, na maioria dos casos, é melhor evitar.
user949300
4
@ user949300 Após dez anos de desenvolvimento, nunca encontrei interrupção ou continuo causando confusão. Eu os envolvi em confusão, mas eles nunca foram a causa. Geralmente, as más decisões sobre o desenvolvedor eram a causa e elas causariam confusão, não importando a arma usada.
corsiKa
1
@corsiKa O bug do SSL da Apple é realmente um erro de goto, mas poderia facilmente ter sido um erro de interrupção ou continuação. No entanto, o código é horrível ... #
31929300
3
@BryanBoettcher, o problema me parece que os mesmos desenvolvedores que pensam que continuar ou vários retornos estão bem também pensam que enterrá-los em corpos de métodos grandes também está bem. Portanto, toda experiência que tenho com retornos contínuos / múltiplos está em enormes bagunças de código.
217 Andy
3

A ideia é o retorno antecipado. No início returnde um ciclo torna-se cedo continue.

Muitas respostas boas para esta pergunta: Devo retornar de uma função mais cedo ou usar uma instrução if?

Observe que, embora a questão seja encerrada com base em opiniões, mais de 430 votos são a favor do retorno antecipado, ~ 50 votos são "depende" e 8 são contra o retorno antecipado. Portanto, há um consenso excepcionalmente forte (ou isso, ou sou mau em contar, o que é plausível).

Pessoalmente, se o corpo do loop estiver sujeito a continuação antecipada e tempo suficiente para importar (3-4 linhas), eu tendem a extrair o corpo do loop em uma função em que eu uso o retorno antecipado em vez de continuar antecipadamente.

Pedro
fonte
2

Essa técnica é útil para certificar pré-condições quando a maior parte do seu método ocorre quando essas condições são atendidas.

Freqüentemente invertemos ifsmeu trabalho e empregamos outro fantasma. Costumava ser bastante frequente que, ao revisar um PR, eu pudesse apontar como meu colega poderia remover três níveis de aninhamento! Isso acontece com menos frequência, pois lançamos nossos ifs.

Aqui está um exemplo de brinquedo que pode ser implementado

function foobar(x, y, z) {
    if (x > 0) {
        if (z != null && isGamma(y)) {
            // ~10 lines of code
            // return something
        }
        else {
           return y
        }
    }
    else {
      return y + z
    }
}

Código como este, onde há UM caso interessante (onde o resto são simplesmente retornos ou pequenos blocos de instrução) são comuns. É trivial reescrever o item acima como

function foobar(x, y, z) {
    if (x <=0) {
        return y + z
    }
    if (z == null || !isGamma(y) {
       return y
    }
    // ~ 10 lines of code
    // return something
}

Aqui, nosso código significativo, as 10 linhas não incluídas, não é recuado três vezes na função. Se você tem o primeiro estilo, ou tenta refatorar o bloco longo em um método ou tolera o recuo. É aqui que a economia entra em cena. Em um local, essa é uma economia trivial, mas em muitos métodos em várias classes, você economiza a necessidade de gerar uma função extra para tornar o código mais limpo e você não tem tantos níveis hediondos em vários níveis recuo .


Em resposta ao exemplo de código do OP, concordo que esse é um colapso excessivo. Especialmente porque em 1 TB, no estilo que uso, a inversão faz com que o código aumente de tamanho.

Lan
fonte
0

As sugestões de novos compartilhadores são muitas vezes úteis, mas devem sempre ser avaliadas criticamente. Ele procura alguns padrões de código predefinidos e sugere transformações, mas não pode levar em consideração todos os fatores que tornam o código mais ou menos legível para humanos.

Sendo todas as outras coisas iguais, menos aninhamento é preferível, e é por isso que o ReSharper sugere uma transformação que reduz o aninhamento. Mas todas as outras coisas não são iguais: nesse caso, a transformação requer a introdução de a continue, o que significa que o código resultante é realmente mais difícil de seguir.

Em alguns casos, trocar um nível de aninhamento por um continue poderia realmente ser uma melhoria, mas isso depende da semântica do código em questão, que está além do entendimento das regras mecânicas do ReSharpers.

No seu caso, a sugestão do ReSharper tornaria o código pior. Então apenas ignore. Ou melhor: não use a transformação sugerida, mas pense um pouco sobre como o código pode ser aprimorado. Observe que você pode estar atribuindo a mesma variável várias vezes, mas apenas a última atribuição terá efeito, pois a anterior seria substituída. Isso faz parecer um bug. A sugestão ReSharpers não corrige o bug, mas torna um pouco mais óbvio que alguma lógica dúbia está acontecendo.

JacquesB
fonte
0

Eu acho que o ponto mais importante aqui é que o objetivo do loop determina a melhor maneira de organizar o fluxo dentro do loop. Se você estiver filtrando alguns elementos antes de percorrer o restante, a continuesaída antecipada faz sentido. No entanto, se você estiver executando diferentes tipos de processamento em um loop, pode fazer mais sentido tornar isso ifrealmente óbvio, como:

for(obj in objectList) {
    if(obj == null) continue;
    if(obj.getColour().equals(Color.BLUE)) {
        // Handle blue objects
    } else {
        // Handle non-blue objects
    }
}

Observe que no Java Streams ou em outros idiomas funcionais, é possível separar a filtragem do loop usando:

items.stream()
    .filter(i -> (i != null))
    .filter(i -> i.getColour().equals(Color.BLUE))
    .forEach(i -> {
        // Process blue objects
    });

Aliás, essa é uma das coisas que eu amo sobre o invertido if ( unless) do Perl aplicado a instruções únicas, o que permite o seguinte tipo de código, que eu acho muito fácil de ler:

foreach my $item (@items) {
    next unless defined $item;
    carp "Only blue items are supported! Failed on item $item" 
       unless ($item->get_colour() eq 'blue');
    ...
}
Gaurav
fonte