São 'quebrar' e 'continuar' más práticas de programação?

179

Meu chefe continua mencionando indiferentemente que programadores ruins usam break e continue em loops.

Eu os uso o tempo todo porque eles fazem sentido; deixe-me mostrar-lhe a inspiração:

function verify(object) {
    if (object->value < 0) return false;
    if (object->value > object->max_value) return false;
    if (object->name == "") return false;
    ...
}

O ponto aqui é que primeiro a função verifica se as condições estão corretas e, em seguida, executa a funcionalidade real. IMO mesmo se aplica com loops:

while (primary_condition) {
    if (loop_count > 1000) break;
    if (time_exect > 3600) break;
    if (this->data == "undefined") continue;
    if (this->skip == true) continue;
    ...
}

Acho que isso facilita a leitura de & depurar; mas também não vejo uma desvantagem.

    
por Mikhail 16.08.2017 / 19:16
fonte

20 respostas

228

Quando usados no início de um bloco, conforme as primeiras verificações são feitas, eles agem como pré-condições, por isso é bom.

Quando usados no meio do bloco, com algum código por perto, eles agem como armadilhas ocultas, então é ruim.

    
por 15.03.2011 / 16:08
fonte
83

Você poderia ler o artigo de 1974 de Donald Knuth Programação estruturada com às Declarações , nas quais ele discute vários usos do go to que são estruturalmente desejáveis. Eles incluem o equivalente das declarações break e continue (muitos dos usos de go to foram desenvolvidos em construções mais limitadas). Seu chefe é o tipo de chamar Knuth de mau programador?

(Os exemplos que me interessam. Geralmente, break e continue não são apreciados por pessoas que gostam de uma entrada e uma saída de qualquer código e esse tipo de pessoa também desaprova várias declarações return . )

    
por 15.03.2011 / 16:12
fonte
40

Eu não acredito que eles sejam ruins. A ideia de que eles são ruins vem dos dias de programação estruturada. Está relacionado com a noção de que uma função deve ter um único ponto de entrada e um único ponto de saída, i. e. apenas um return por função.

Isso faz algum sentido se sua função for longa e se você tiver vários loops aninhados. No entanto, suas funções devem ser curtas e você deve envolver os loops e seus corpos em pequenas funções próprias. Geralmente, forçar uma função a ter um único ponto de saída pode resultar em uma lógica muito complicada.

Se a sua função for muito curta, se você tiver um único loop ou, na pior das hipóteses, dois loops aninhados, e se o corpo do loop for muito curto, é muito claro o que faz break ou continue . Também está claro o que várias declarações return fazem.

Estas questões são abordadas em "Código Limpo" por Robert C. Martin e em "Refatoração" por Martin Fowler.

    
por 15.03.2011 / 16:11
fonte
32

Bad programadores falam em absolutos (assim como Sith). Bons programadores usam a solução mais clara possível ( todas as outras coisas sendo iguais ).

Usar a quebra e continuar com frequência torna o código difícil de seguir. Mas se substituí-los torna o código ainda mais difícil de seguir, então essa é uma mudança ruim.

O exemplo que você deu é definitivamente uma situação em que os intervalos e continuações devem ser substituídos por algo mais elegante.

    
por 06.10.2011 / 00:34
fonte
22

A maioria das pessoas acha que é uma má ideia porque o comportamento não é facilmente previsível. Se você está lendo o código e você vê while(x < 1000){} , você assume que vai rodar até x > = 1000 ... Mas se houver intervalos no meio, então isso não é verdade, então você pode realmente confie em seu looping ...

É a mesma razão pela qual as pessoas não gostam do GOTO: claro, ele pode ser usado bem, mas também pode levar a um espantoso código de espaguete, onde o código salta aleatoriamente de seção para seção. / p>

Para mim, se eu fosse fazer um loop que quebrasse em mais de uma condição, eu faria while(x){} e então alternaria X para false quando eu precisasse sair. O resultado final seria o mesmo, e qualquer um que ler o código saberia examinar mais de perto as coisas que mudaram o valor de X.

    
por 15.03.2011 / 16:07
fonte
14

Sim, você pode [re] escrever programas sem instruções de quebra (ou retorna do meio dos loops, que fazem a mesma coisa). Mas você pode ter que introduzir variáveis adicionais e / ou duplicação de código, o que normalmente torna o programa mais difícil de entender. Pascal (a linguagem de programação) foi muito ruim, especialmente para programadores iniciantes por esse motivo. Seu chefe basicamente quer que você programe nas estruturas de controle de Pascal. Se Linus Torvalds estivesse no seu lugar, ele provavelmente mostraria ao seu chefe o dedo do meio!

Há um resultado da ciência da computação chamado hierarquia de estruturas de controle de Kosaraju, que remonta a 1973 e que é mencionado no famoso (mais) artigo de Knuth sobre gotos de 1974. (Este artigo de Knuth já foi recomendado acima por David Thornley, O que S. Rao Kosaraju provou em 1973 é que não é possível reescrever todos os programas que tenham intervalos de vários níveis de profundidade n em programas com profundidade de quebra menor que n sem introduzir variáveis extras. Mas digamos que é apenas um resultado puramente teórico. (Basta adicionar algumas variáveis extras ?! Certamente você pode fazer isso para agradar seu chefe ...)

O que é mais importante do ponto de vista da engenharia de software é um artigo mais recente, de 1995, escrito por Eric S. Roberts intitulado Loop Exits e Structured Programming: Reopening the Debate ( link ). Roberts resume vários estudos empíricos realizados por outros antes dele. Por exemplo, quando um grupo de alunos do tipo CS101 foi solicitado a escrever código para uma função implementando uma pesquisa sequencial em uma matriz, o autor do estudo disse o seguinte sobre os alunos que usaram um intervalo / retorno / goto para sair do o loop de pesquisa sequencial quando o elemento foi encontrado:

I have yet to find a single person who attempted a program using [this style] who produced an incorrect solution.

Roberts também diz que:

Students who attempted to solve the problem without using an explicit return from the for loop fared much less well: only seven of the 42 students attempting this strategy managed to generate correct solutions. That figure represents a success rate of less than 20%.

Sim, você pode ser mais experiente que os alunos do CS101, mas sem usar a instrução break (ou equivalentemente retornar / goto do meio dos loops), eventualmente você escreverá um código que, apesar de ser bem estruturado, é bastante peludo em termos de variáveis lógicas extras e duplicação de código que alguém, provavelmente você mesmo, colocará bugs lógicos enquanto tenta seguir o estilo de código do seu chefe.

Também vou dizer aqui que o artigo de Roberts é muito mais acessível ao programador médio, então é melhor ler primeiro que o de Knuth. Também é mais curto e cobre um tópico mais restrito. Você provavelmente poderia até recomendá-lo ao seu chefe, mesmo que ele seja o gerente e não o tipo CS.

    
por 15.07.2014 / 01:37
fonte
9

Eu não considero o uso de uma dessas práticas ruins, mas usá-las em demasia dentro do mesmo ciclo deve justificar repensar a lógica que está sendo usada no loop. Use-os com moderação.

    
por 15.03.2011 / 16:00
fonte
7

O exemplo que você deu não precisa de pausas nem continua:

while (primary-condition AND
       loop-count <= 1000 AND
       time-exec <= 3600) {
   when (data != "undefined" AND
           NOT skip)
      do-something-useful;
   }

Meu "problema" com as 4 linhas no seu exemplo é que elas estão todas no mesmo nível, mas fazem coisas diferentes: algumas quebram, outras continuam ... Você precisa ler cada linha.

Na minha abordagem aninhada, quanto mais você se aprofundar, mais "útil" se torna o código.

Mas, se no fundo você encontrar um motivo para interromper o loop (diferente da condição primária), um intervalo ou retorno teria seu uso. Eu prefiro o uso de um sinalizador extra que deve ser testado na condição de nível superior. O intervalo / retorno é mais direto; indica melhor a intenção do que definir outra variável.

    
por 16.03.2011 / 13:35
fonte
6

A "maldade" depende de como você os usa. Eu normalmente uso as interrupções em construções de loop SOMENTE quando isso me salvará de ciclos que não podem ser salvos por meio de uma refatoração de um algoritmo. Por exemplo, percorrendo uma coleção procurando por um item com um valor em uma propriedade específica definida como true. Se tudo que você precisa saber é que um dos itens tinha essa propriedade definida como true, uma vez que você alcança esse resultado, uma quebra é boa para finalizar o loop apropriadamente.

Se o uso de um intervalo não tornar o código especificamente mais fácil de ler, mais curto para executar ou salvar ciclos de processamento de maneira significativa, é melhor não usá-los. Eu tenho a tendência de codificar o "menor denominador comum" quando possível para ter certeza de que qualquer um que me siga possa facilmente olhar para o meu código e descobrir o que está acontecendo (eu nem sempre sou bem sucedido nisso). As quebras reduzem isso porque introduzem pontos de entrada / saída ímpares. Usados indevidamente, eles podem se comportar de maneira muito parecida com um extravagante "goto".

    
por 15.03.2011 / 16:08
fonte
4

Absolutamente não ... Sim, o uso de goto é ruim porque deteriora a estrutura do seu programa e também é muito difícil entender o fluxo de controle.

Mas o uso de declarações como break e continue é absolutamente necessário nos dias de hoje e não é considerado uma prática de programação ruim.

E também não é difícil entender o fluxo de controle em uso de break e continue . Em construções como switch , a instrução break é absolutamente necessária.

    
por 27.11.2013 / 15:09
fonte
3

A noção essencial vem da capacidade de analisar semanticamente o seu programa. Se você tiver uma única entrada e uma única saída, a matemática necessária para denotar estados possíveis é consideravelmente mais fácil do que se você tivesse que gerenciar caminhos de bifurcação.

Em parte, essa dificuldade se reflete em ser capaz de raciocinar conceitualmente sobre seu código.

Francamente, seu segundo código não é óbvio. O que isso está fazendo? Continuar 'continuar', ou 'próximo' o loop? Eu não faço ideia. Pelo menos o seu primeiro exemplo é claro.

    
por 15.03.2011 / 16:56
fonte
3

Eu substituiria seu segundo snippet de código por

while (primary_condition && (loop_count <= 1000 && time_exect <= 3600)) {
    if (this->data != "undefined" && this->skip != true) {
        ..
    }
}

não por qualquer motivo de terseness - eu realmente acho que isso é mais fácil de ler e que alguém entenda o que está acontecendo. De um modo geral, as condições para os seus laços devem estar contidas puramente dentro daquelas condições de ciclo não espalhadas por todo o corpo. No entanto, existem algumas situações em que break e continue podem ajudar na legibilidade. break mais do que continue devo acrescentar: D

    
por 13.06.2017 / 23:09
fonte
2

Eu não concordo com o seu chefe. Existem locais apropriados para break e continue a serem usados. Na verdade, o motivo pelo qual execeptions e manipulação de exceções foram introduzidas em linguagens de programação modernas é que você não pode resolver todos os problemas usando apenas structured techniques .

Em uma nota lateral Eu não quero começar uma discussão religiosa aqui, mas você pode reestruturar seu código para ficar ainda mais legível assim:

while (primary_condition) {
    if (loop_count > 1000) || (time_exect > 3600) {
        break;
    } else if ( ( this->data != "undefined") && ( !this->skip ) ) {
       ... // where the real work of the loop happens
    }
}

Em outro lado, note

Eu pessoalmente não gosto do uso de ( flag == true ) em condicionais porque se a variável já é booleana, então você está introduzindo uma comparação adicional que precisa acontecer quando o valor do booleano tem a resposta que você quer - a menos é claro É certo que o seu compilador otimizará essa comparação extra.

    
por 13.06.2017 / 23:08
fonte
1

Eu concordo com o seu chefe. Eles são ruins porque produzem métodos com alta complexidade ciclomática. Tais métodos são difíceis de ler e difíceis de testar. Felizmente há uma solução fácil. Extraia o corpo do loop em um método separado, onde o "continue" se torna "return". "Retorno" é melhor porque depois do "retorno" acabou - não há preocupações sobre o estado local.

Para "break", extraia o próprio loop em um método separado, substituindo "break" por "return".

Se os métodos extraídos exigirem um grande número de argumentos, isso é uma indicação para extrair uma classe - colete-os em um objeto de contexto.

    
por 16.08.2017 / 19:28
fonte
0

Eu acho que é apenas um problema quando aninhado profundamente em vários loops. É difícil saber em qual loop você está indo. Pode ser difícil seguir uma continuação também, mas acho que a verdadeira dor vem das pausas - a lógica pode ser difícil de seguir.

    
por 15.03.2011 / 15:54
fonte
0

Contanto que eles não sejam usados como goto disfarçado, como no exemplo a seguir:

do
{
      if (foo)
      {
             /*** code ***/
             break;
      }

      if (bar)
      {
             /*** code ***/
             break;
      }
} while (0);

Eu estou bem com eles. (Exemplo visto no código de produção, meh)

    
por 13.06.2017 / 23:09
fonte
0

Eu não gosto de nenhum desses estilos. Aqui está o que eu preferiria:

function verify(object)
{
    if not (object->value < 0) 
       and not(object->value > object->max_value)
       and not(object->name == "") 
       {
         do somethign important
       }
    else return false; //probably not necessary since this function doesn't even seem to be defined to return anything...?
}

Eu realmente não gosto de usar return para abortar uma função. Parece um abuso de return .

O uso de break nem sempre é claro para leitura.

Melhor ainda pode ser:

notdone := primarycondition    
while (notDone)
{
    if (loop_count > 1000) or (time_exect > 3600)
    {
       notDone := false; 
    }
    else
    { 
        skipCurrentIteration := (this->data == "undefined") or (this->skip == true) 

        if not skipCurrentIteration
        {
           do something
        } 
        ...
    }
}

menos aninhamento e as condições complexas são refatoradas em variáveis (em um programa real você precisaria ter nomes melhores, obviamente ...)

(Todo o código acima é pseudo-código)

    
por 13.06.2017 / 23:10
fonte
0

Não. É uma maneira de resolver um problema e existem outras maneiras de resolvê-lo.

Muitas linguagens mainstream atuais (Java, .NET (C # + VB), PHP, escrevem suas próprias) usam "break" e "continue" para pular loops. Ambos "estruturaram goto (s)" sentenças.

sem eles:

String myKey = "mars";

int i = 0; bool found = false;
while ((i < MyList.Count) && (not found)) {
  found = (MyList[i].key == myKey);
  i++;   
}
if (found)
  ShowMessage("Key is " + i.toString());
else
  ShowMessage("Not found.");

Com eles:

String myKey = "mars";

for (i = 0; i < MyList.Count; i++) {
  if (MyList[i].key == myKey)
    break;
}
ShowMessage("Key is " + i.toString());

Observe que o código "break" e "continue" é mais curto e geralmente transforma frases "while" em frases "for" ou "foreach".

Ambos os casos são uma questão de estilo de codificação. Eu prefiro não usá-los , porque o estilo detalhado me permite ter mais controle sobre o código.

Eu, na verdade, trabalho em alguns projetos, onde era obrigatório usar essas frases.

Alguns desenvolvedores podem pensar que eles não são precisos, mas hipotéticos, se precisarmos removê-los, temos que remover "while" e "do while" ("repetir até", vocês pascal) também; -)

Conclusão, mesmo que eu prefira não usá-los, acho que é uma opção, não uma prática de programação ruim.

    
por 13.06.2017 / 23:10
fonte
0

Eu não sou contra continue e break em princípio, mas eu acho que são construções de baixo nível que muitas vezes podem ser substituídas por algo ainda melhor.

Estou usando o C # como exemplo aqui, considere o caso de querer iterar sobre uma coleção, mas queremos apenas os elementos que preenchem algum predicado, e não queremos fazer mais do que um máximo de 100 iterações.

for (var i = 0; i < collection.Count; i++)
{
    if (!Predicate(item)) continue;
    if (i >= 100) break; // at first I used a > here which is a bug. another good thing about the more declarative style!

    DoStuff(item);
}

Isso parece razoavelmente limpo. Não é muito difícil de entender. Eu acho que poderia ganhar muito de ser mais declarativo. Compare com o seguinte:

foreach (var item in collection.Where(Predicate).Take(100))
    DoStuff(item);

Talvez as chamadas "Where and Take" nem estejam nesse método. Talvez essa filtragem deva ser feita ANTES que a coleção seja passada para esse método. De qualquer forma, ao nos afastarmos das coisas de baixo nível e nos concentrarmos mais na lógica comercial real, fica mais claro o que estamos REALMENTE interessados. Torna-se mais fácil separar nosso código em módulos coesos que aderem mais a boas práticas de projeto. em.

Coisas de baixo nível ainda existirão em algumas partes do código, mas queremos esconder isso o máximo possível, porque é preciso energia mental que poderíamos estar usando para raciocinar sobre os problemas de negócios.

    
por 13.06.2017 / 23:11
fonte
-1

O código completo tem uma seção interessante sobre como usar goto e vários retornos da rotina ou do loop.

Em geral, não é uma prática ruim. break ou continue informam exatamente o que acontece a seguir. E eu concordo com isso.

Steve McConnell (autor de Code Complete) usa quase os mesmos exemplos que você para mostrar as vantagens de usar várias instruções goto .

No entanto, o uso excessivo de break ou continue pode levar a softwares complexos e inatingíveis.

    
por 16.03.2011 / 16:34
fonte