while (true) e quebra de loop - anti-padrão?

32

Considere o seguinte código:

public void doSomething(int input)
{
   while(true)
   {
      TransformInSomeWay(input);

      if(ProcessingComplete(input))
         break;

      DoSomethingElseTo(input);
   }
}

Suponha que esse processo envolva um número finito, mas dependente de entrada, de etapas; o loop é projetado para terminar sozinho como resultado do algoritmo e não é projetado para ser executado indefinidamente (até ser cancelado por um evento externo). Como o teste para ver se o loop deve terminar está no meio de um conjunto lógico de etapas, o próprio loop while atualmente não verifica nada significativo; a verificação é realizada no local "apropriado" dentro do algoritmo conceitual.

Foi-me dito que esse código é ruim, porque é mais suscetível a erros devido ao fato de a condição final não ter sido verificada pela estrutura do loop. É mais difícil descobrir como você sairia do loop e poderia convidar bugs, pois a condição de quebra pode ser ignorada ou omitida acidentalmente, devido a alterações futuras.

Agora, o código pode ser estruturado da seguinte maneira:

public void doSomething(int input)
{
   TransformInSomeWay(input);

   while(!ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
      TransformInSomeWay(input);
   }
}

No entanto, isso duplica uma chamada para um método no código, violando DRY; se TransformInSomeWayposteriormente fossem substituídos por outro método, as duas chamadas teriam que ser encontradas e alteradas (e o fato de existirem duas pode ser menos óbvio em um trecho de código mais complexo).

Você também pode escrever como:

public void doSomething(int input)
{
   var complete = false;
   while(!complete)
   {
      TransformInSomeWay(input);

      complete = ProcessingComplete(input);

      if(!complete) 
      {
         DoSomethingElseTo(input);          
      }
   }
}

... mas agora você tem uma variável cujo único objetivo é mudar a verificação da condição para a estrutura do loop, e também deve ser verificada várias vezes para fornecer o mesmo comportamento da lógica original.

Pela minha parte, digo que, dado o algoritmo que esse código implementa no mundo real, o código original é o mais legível. Se você estivesse passando por isso você mesmo, seria assim que pensaria e, portanto, seria intuitivo para as pessoas familiarizadas com o algoritmo.

Então, o que é "melhor"? é melhor atribuir a responsabilidade da verificação de condições ao loop while, estruturando a lógica em torno do loop? Ou é melhor estruturar a lógica de uma maneira "natural", conforme indicado por requisitos ou por uma descrição conceitual do algoritmo, mesmo que isso possa significar ignorar os recursos internos do loop?

KeithS
fonte
12
Possivelmente, mas, apesar dos exemplos de código, a pergunta que está sendo feita é mais conceitual da OMI, o que está mais alinhado com esta área do SE. O que é um padrão ou antipadrão é discutido aqui com frequência, e essa é a verdadeira questão; está estruturando um loop para executar infinitamente e, em seguida, quebrá-lo é uma coisa ruim?
29412 KeithS
3
A do ... untilconstrução também é útil para esses tipos de loops, pois eles não precisam ser "preparados".
Blrfl
2
O case de loop e meio ainda carece de uma resposta realmente boa.
Loren Pechtel 29/03/12
1
"No entanto, isso duplica uma chamada para um método no código, violando o DRY" Eu discordo dessa afirmação e parece um mal-entendido do princípio.
213 Andy
1
Além de eu preferir o estilo C para (;;), que significa explicitamente "Eu tenho um loop e não faço check-in na declaração do loop", sua primeira abordagem está absolutamente correta. Você tem um loop. Há algum trabalho a ser feito antes que você possa decidir se deseja sair. Então você sai se algumas condições forem atendidas. Então você faz o trabalho real e começa tudo de novo. Isso é absolutamente bom, fácil de entender, lógico, não redundante e fácil de acertar. A segunda variante é simplesmente horrível, enquanto a terceira é apenas inútil; tornando-se horrível se o resto do loop que entra no if for muito longo.
gnasher729

Respostas:

14

Fique com sua primeira solução. Os loops podem ficar muito envolvidos e uma ou mais quebras limpas podem ser muito mais fáceis para você, qualquer outra pessoa olhando para o seu código, o otimizador e o desempenho do programa do que instruções if e variáveis ​​adicionadas. Minha abordagem usual é configurar um loop com algo como "while (true)" e obter a melhor codificação possível. Frequentemente, eu posso substituir as quebras por um controle de loop padrão; a maioria dos loops é bem simples, afinal. A condição final deve ser verificada pela estrutura do loop pelos motivos mencionados, mas não ao custo de adicionar variáveis ​​totalmente novas, adicionar instruções if e repetir o código, os quais são ainda mais propensos a erros.

RalphChapin
fonte
"instruções if e código repetido, todos propensos a erros ainda mais." - sim ao tentar pessoas que eu chamar se de "futuros erros"
Pat
13

É apenas um problema significativo se o corpo do loop não for trivial. Se o corpo é tão pequeno, analisá-lo dessa maneira é trivial e não é um problema.

DeadMG
fonte
7

Tenho problemas para encontrar as outras soluções melhores que as que estão com problemas. Bem, eu prefiro a maneira Ada, que permite fazer

loop
  TransformInSomeWay(input);
exit when ProcessingComplete(input);
  DoSomethingElseTo(input);
end loop;

mas a solução de interrupção é a renderização mais limpa.

Meu ponto de vista é que, quando há uma estrutura de controle ausente, a solução mais limpa é imitá-la com outras estruturas de controle, sem usar o fluxo de dados. (E da mesma forma, quando tenho um problema de fluxo de dados, prefiro soluções puras de fluxo de dados àquelas que envolvem estruturas de controle *).

É mais difícil descobrir como você sairia do loop,

Não se engane, a discussão não aconteceria se a dificuldade não fosse a mesma: a condição é a mesma e está presente no mesmo local.

Qual dos

while (true) {
   XXXX
if (YYY) break;
   ZZZZ;
}

e

while (TTTT) {
    XXXX
    if (YYYY) {
        ZZZZ
    }
}

tornar melhor a estrutura pretendida? Estou votando pela primeira vez, você não precisa verificar se as condições correspondem. (Mas veja meu recuo).

AProgrammer
fonte
1
Ah, olha, uma linguagem que suporta diretamente loops no meio da saída! :)
mjfgates
Eu gosto do seu ponto de emular estruturas de controle ausentes com estruturas de controle. Essa provavelmente é uma boa resposta para perguntas relacionadas ao GOTO também. Deve-se usar as estruturas de controle de uma linguagem sempre que elas se ajustam aos requisitos semânticos, mas se um algoritmo exigir algo mais, deve-se usar a estrutura de controle exigida pelo algoritmo.
Supercat
3

while (true) e break são expressões comuns. É a maneira canônica de implementar loops de saída média em linguagens tipo C. Adicionar uma variável para armazenar a condição de saída e um if () na segunda metade do loop é uma alternativa razoável. Algumas lojas podem padronizar oficialmente uma ou outra, mas nenhuma é superior; eles são equivalentes.

Um bom programador que trabalha nessas linguagens deve saber rapidamente o que está acontecendo, caso veja uma delas. Pode ser uma boa e pequena pergunta para entrevista; entregue a alguém dois loops, um usando cada idioma, e pergunte qual é a diferença (resposta adequada: "nada", com talvez uma adição de "mas eu costumo usar esse").

mjfgates
fonte
2

Suas alternativas não são tão ruins quanto você imagina. Um bom código deve:

  1. Indique claramente com bons nomes de variáveis ​​/ comentários sob quais condições o loop deve ser encerrado. (A condição de quebra não deve estar oculta)

  2. Indique claramente qual é a ordem das operações. É aqui que colocar a terceira operação opcional ( DoSomethingElseTo(input)) dentro da if é uma boa ideia.

Dito isto, é fácil deixar instintos perfeccionistas e polidores de latão consumirem muito tempo. Eu apenas ajustaria o if como mencionado acima; comente o código e siga em frente.

Pat
fonte
Penso que instintos perfeccionistas e polidores de latão economizam tempo a longo prazo. Esperemos que, com a prática, você desenvolva esses hábitos para que seu código seja perfeito e seu polimento seja polido sem consumir muito tempo. Mas, diferentemente da construção física, o software pode durar para sempre e ser usado em um número infinito de lugares. A economia de código que é até 0,00000001% mais rápida, mais confiável, utilizável e sustentável pode ser grande. (Claro, a maioria do meu código muito polido está em línguas longa obsoletos ou fazer dinheiro para alguém nos dias de hoje, mas que me ajudar a desenvolver bons hábitos.)
RalphChapin
Bem, não posso chamá-lo errado :-) Esta é uma pergunta de opinião e se boas habilidades surgiram do polimento de metais: ótimo.
Pat
Eu acho que é uma possibilidade de considerar, de qualquer maneira. Eu certamente não gostaria de oferecer nenhuma garantia. Faz-me sentir melhor pensar que meu polimento de latão melhorou minhas habilidades, para que eu possa ter preconceito nesse ponto.
RalphChapin
2

As pessoas dizem que é uma má prática usar while (true)e na maioria das vezes elas estão certas. Se a sua whiledeclaração contiver muitos códigos complicados e não for claro quando você sair do if, será difícil manter o código e um bug simples poderá causar um loop infinito.

No seu exemplo, você está correto de usar while(true)porque seu código é claro e fácil de entender. Não faz sentido criar código ineficiente e difícil de ler, simplesmente porque algumas pessoas consideram sempre uma má prática usar while(true).

Fui confrontado com um problema semelhante:

$i = 0
$key = $str.'0';
$key1 = $str1.'0';
while (isset($array][$key] || isset($array][$key1])
{
    if (isset($array][$key]))
    {
        // Code
    }
    else
    {
        // Code
    }
    $key = $str.++$i;
    $key1 = $str1.$i;
}

O uso do ... while(true)evita isset($array][$key]desnecessariamente ser chamado duas vezes, o código é mais curto, mais limpo e mais fácil de ler. Não há absolutamente nenhum risco de um bug de loop infinito ser introduzido else break;no final.

$i = -1;
do
{
    $key = $str.++$i;
    $key1 = $str1.$i;
    if (isset($array[$key]))
    {
        // Code
    }
    elseif (isset($array[$key1]))
    {
        // Code
    }
    else
        break;
} while (true);

É bom seguir boas práticas e evitar más práticas, mas sempre há exceções e é importante manter a mente aberta e perceber essas exceções.

Dan Bray
fonte
0

Se um determinado fluxo de controle é naturalmente expresso como uma declaração de interrupção, essencialmente nunca é uma escolha melhor usar outros mecanismos de controle de fluxo para emular uma declaração de interrupção.

(observe que isso inclui desenrolar parcialmente o loop e deslizar o corpo do loop para baixo, para que o loop comece coincidindo com o teste condicional)

Se você pode reorganizar seu loop para que o fluxo de controle seja naturalmente expresso com uma verificação condicional no início do loop, ótimo. Pode até valer a pena gastar algum tempo pensando se isso é possível. Mas não tente não encaixar um pino quadrado em um buraco redondo.


fonte
0

Você também pode ir com isso:

public void doSomething(int input)
{
   while(TransformInSomeWay(input), !ProcessingComplete(input))
   {
      DoSomethingElseTo(input);
   }
}

Ou menos confuso:

bool TransformAndCheckCompletion(int &input)
{
   TransformInSomeWay(input);
   return ProcessingComplete(input))
}

public void doSomething(int input)
{
   while(TransformAndCheckCompletion(input))
   {
      DoSomethingElseTo(input);
   }
}
Eclipse
fonte
1
Enquanto algumas pessoas gostam disso, afirmo que a condição do loop geralmente deve ser reservada para testes condicionais, em vez de declarações que fazem coisas.
5
Expressão de vírgula em uma condição de loop ... Isso é horrível. Você é inteligente demais. E só funciona nesse caso trivial em que TransformInSomeWay pode ser expresso como uma expressão.
gnasher729
0

Quando a complexidade da lógica se torna pesada, o uso de um loop ilimitado com interrupções no meio do loop para controle de fluxo faz realmente mais sentido. Eu tenho um projeto com algum código que se parece com o seguinte. (Observe a exceção no final do loop.)

L: for (;;) {
    if (someBool) {
        someOtherBool = doSomeWork();
        if (someOtherBool) {
            doFinalWork();
            break L;
        }
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
            break L;
        }
        someOtherBool3 = doSomeWork3();
        if (someOtherBool3) {
            doFinalWork3();
            break L;
        }
    }
    bitOfData = getTheData();
    throw new SomeException("Unable to do something for some reason.", bitOfData);
}
progressOntoOtherThings();

Para fazer essa lógica sem quebras de loop ilimitadas, eu terminaria com algo como o seguinte:

void method() {
    if (!someBool) {
        throwingMethod();
    }
    someOtherBool = doSomeWork();
    if (someOtherBool) {
        doFinalWork();
    } else {
        someOtherBool2 = doSomeWork2();
        if (someOtherBool2) {
            doFinalWork2();
        } else {
            someOtherBool3 = doSomeWork3();
            if (someOtherBool3) {
                doFinalWork3();
            } else {
                throwingMethod();
            }
        }
    }
    progressOntoOtherThings();
}
void throwingMethod() throws SomeException {
        bitOfData = getTheData();
        throw new SomeException("Unable to do something for some reason.", bitOfData);
}

Portanto, a exceção agora é lançada em um método auxiliar. Também tem bastante if ... elsenidificação. Não estou satisfeito com essa abordagem. Portanto, acho que o primeiro padrão é o melhor.

intrepidis
fonte
Na verdade, eu fiz esta pergunta no SO e foi abatido em chamas ... stackoverflow.com/questions/47253486/...
intrepidis