SkillsTecnológicas
Menu
Conteúdo da trilha

Condições de parada e laços infinitos

Projete condições de término alcançáveis, acompanhe o progresso do estado e diagnostique atualizações que fazem um laço repetir para sempre.

Conteúdo 32 de 39

Duas pistas desenhadas a lápis comparam blocos que diminuem até um portão de saída e blocos inalterados que percorrem um circuito fechado

Um laço termina quando a saída é alcançável

Uma condição de parada define quando uma repetição deve acabar. Em laços como while, ela costuma aparecer de forma invertida: o corpo continua enquanto a condição é verdadeira e para quando ela se torna falsa.

ENQUANTO condição de continuação FAÇA
  ações
FIM ENQUANTO

Um laço infinito ocorre quando a execução continua sem alcançar uma saída. O erro nem sempre está na expressão da condição. O estado pode não mudar, mudar na direção errada, saltar sobre o valor esperado ou seguir um caminho que ignora a atualização.

Para avaliar se um laço termina, procure três elementos:

  1. estado observado: quais valores a condição lê;
  2. saída: qual situação torna a condição falsa ou aciona uma interrupção;
  3. progresso: o que aproxima o estado dessa situação em toda passagem que repete.

Uma condição legível descreve a porta. O progresso garante que o algoritmo caminhe até ela.

Escreva um contrato de término antes do laço

Antes da sintaxe, responda quatro perguntas:

PerguntaExemplo de resposta
O que mantém a repetição ativa?ainda existem tarefas pendentes
Qual estado representa isso?restante > 0
O que muda a cada passagem?processar entre 1 e 5 tarefas
Por que a saída será alcançada?restante diminui e não fica abaixo de zero

Esse é um contrato de término: uma explicação curta que conecta intenção, condição e atualização. Se uma das respostas depende de “provavelmente mudará”, ainda existe uma lacuna.

Considere 23 tarefas, processadas em lotes de até cinco:

restante <- 23

ENQUANTO restante > 0 FAÇA
  processadas <- MÍNIMO(5, restante)
  restante <- restante - processadas
FIM ENQUANTO

O uso de MÍNIMO(5, restante) evita processar mais tarefas do que existem no último lote. Enquanto o laço estiver ativo, processadas ficará entre 1 e 5; portanto, restante obrigatoriamente diminuirá.

A condição envia o fluxo ao corpo quando ainda existem tarefas; o corpo reduz o estado antes de um novo teste, enquanto a condição falsa conduz à saída
Condição e atualização formam um ciclo; a saída só é confiável quando cada volta produz progresso.

Meça a distância que falta até a saída

Uma forma rigorosa de justificar o término é escolher uma medida de progresso, também chamada de medida de término. Ela deve:

  • possuir um limite que impeça redução indefinida;
  • diminuir estritamente em toda iteração que continuará o laço.

No exemplo, a medida é o próprio restante. Ela começa em 23, nunca é negativa e diminui pelo menos uma unidade em cada passagem:

23 → 18 → 13 → 8 → 3 → 0

Quando chega a zero, restante > 0 se torna falsa. O argumento não depende de observar que “funcionou uma vez”; ele explica por que qualquer quantidade inicial não negativa termina, desde que todo lote ativo processe ao menos uma tarefa.

Em uma contagem crescente, a variável aumenta, mas a distância até o limite diminui. Para i < limite, uma medida possível é:

limite - i

Se i cresce uma unidade e limite permanece fixo, essa distância diminui até a condição ficar falsa. A documentação do verificador Dafny formaliza a mesma ideia: uma medida de término deve diminuir estritamente e ser limitada.

Essa medida é diferente das invariantes vistas em contadores e acumuladores. A invariante descreve algo que continua verdadeiro; a medida descreve algo que se aproxima do fim.

Compare três rastros antes de corrigir o código

O histórico do estado revela classes diferentes de defeito:

RastroDiagnóstico provável
23, 18, 13, 8, 3, 0progresso em direção à saída
23, 23, 23, 23...atualização ausente ou sem efeito
0, 3, 6, 9, 12... com saída em valor == 10atualização salta sobre o alvo e continua se afastando
Três linhas de estados mostram uma sequência que alcança zero, outra que permanece em 23 e uma terceira que avança de três em três sem atingir o alvo dez
O valor repetido indica estagnação; ultrapassar um alvo testado por igualdade revela uma saída inalcançável.

No terceiro caso, este laço não termina:

let valor = 0;

while (valor !== 10) {
  valor += 3;
}

A sequência pula de 9 para 12. Depois disso, continua crescendo e nunca volta a 10. Trocar a condição por valor < 10 pode ser correto se a regra real for “repetir enquanto estiver abaixo do limite”. Se o resultado precisa ser exatamente 10, mudar apenas o operador esconderia um problema no passo; o contrato do domínio deve decidir a correção.

Investigue as causas por categoria

O estado não muda

let tentativas = 0;

while (tentativas < 3) {
  console.log("Tentando novamente");
}

tentativas continua zero. A condição permanece verdadeira porque nenhuma instrução altera o valor que ela observa.

O estado muda na direção oposta

let restante = 5;

while (restante > 0) {
  restante += 1;
}

A condição espera que restante chegue a zero ou menos, mas a atualização aumenta a distância. Nomear a medida — aqui, restante — torna a contradição evidente.

A saída depende de uma igualdade que pode ser pulada

Passos maiores que uma unidade, arredondamentos ou valores externos podem nunca produzir o valor exato. Compare a regra do problema antes de trocar != por < ou >.

Apenas alguns caminhos atualizam o estado

Uma estrutura condicional pode criar uma rota que volta ao teste sem avançar. O caso mais discreto costuma envolver continue.

A mudança depende de algo externo

Um laço pode aguardar entrada, mensagem ou resposta que talvez nunca chegue. Nesse cenário, não existe prova de término baseada apenas no estado local. Cancelamento, prazo máximo, encerramento do canal ou outra política explícita precisa fazer parte do contrato do sistema.

Verifique se continue pula a atualização

continue encerra a passagem atual e transfere o fluxo para o próximo teste. Isso pode ignorar instruções colocadas depois dele:

const itens = ["A", null, "B"];
let i = 0;

while (i < itens.length) {
  if (itens[i] === null) {
    continue;
  }

  console.log(itens[i]);
  i += 1;
}

Quando i vale 1, o item é null. O continue volta à condição sem executar i += 1; a posição permanece 1 para sempre.

Uma correção é garantir o avanço antes de qualquer desvio:

const itens = ["A", null, "B"];
let i = 0;

while (i < itens.length) {
  const item = itens[i];
  i += 1;

  if (item === null) {
    continue;
  }

  console.log(item);
}

Agora a medida itens.length - i diminui em todas as passagens. Um for também pode tornar a atualização estruturalmente mais visível quando a progressão é previsível.

Dois fluxos com continue são comparados: no primeiro o desvio retorna ao teste antes do incremento; no segundo a posição avança antes do desvio
Todo caminho que repete precisa preservar o argumento de progresso, inclusive os caminhos de desvio.

Use break para uma saída legítima, não para esconder o defeito

break encerra imediatamente o laço mais interno. Ele é adequado quando o evento de saída nasce durante o processamento, como localizar um item:

let encontrado = null;

for (const codigo of [14, 27, 31, 42]) {
  if (codigo === 31) {
    encontrado = codigo;
    break;
  }
}

Nesse caso existem duas formas de término:

  • a coleção acaba;
  • o valor procurado é encontrado e break executa.

Adicionar break a uma condição arbitrária apenas para impedir o travamento pode mascarar uma atualização errada. Para ser parte do contrato, o evento que dispara a interrupção também precisa ser alcançável ou a repetição precisa possuir outra saída.

while (true) não é automaticamente um erro. Ele torna explícito que a saída estará no corpo ou fora do processo:

ENQUANTO verdadeiro FAÇA
  mensagem <- RECEBER()
  SE mensagem indica encerramento ENTÃO
    INTERROMPER
  FIM SE
  PROCESSAR(mensagem)
FIM ENQUANTO

Serviços, consumidores de mensagens e interfaces orientadas a eventos podem ser deliberadamente contínuos. Ainda assim, precisam de política de cancelamento, tratamento de falhas e encerramento controlado. Um laço intencionalmente duradouro é uma decisão arquitetural; um laço infinito acidental é uma promessa de término quebrada.

Coloque limites operacionais sem confundi-los com correção

Durante a depuração, um limite de iterações evita que um teste defeituoso consuma recursos indefinidamente:

let iteracoes = 0;
const limiteDeDiagnostico = 1000;

while (condicao()) {
  iteracoes += 1;

  if (iteracoes > limiteDeDiagnostico) {
    throw new Error("Laço excedeu o limite de diagnóstico");
  }

  executarPasso();
}

Esse limite revela um sintoma e protege o ambiente de teste. Ele não demonstra que o algoritmo está correto: entradas válidas talvez exijam mais de mil passagens, ou o erro pode ocorrer antes desse ponto.

Em produção, timeouts, cancelamento e cotas podem ser requisitos legítimos. Seus valores devem vir do comportamento esperado do sistema, não de uma tentativa de compensar uma condição de parada mal definida.

Depure observando somente o estado relevante

Use este roteiro:

  1. interrompa a execução ou reproduza o caso com um limite seguro;
  2. escreva a condição em uma frase do domínio;
  3. liste todas as variáveis lidas pela condição;
  4. registre seus valores antes e depois de cada passagem;
  5. marque break, continue, return e exceções possíveis;
  6. identifique uma medida limitada que deveria se aproximar da saída;
  7. confira se ela progride em todos os caminhos que repetem.

Uma assinatura de estado é o conjunto dos valores que determina o próximo passo. Se um algoritmo determinístico volta à mesma assinatura, ele repetirá a mesma transição e entrou em um ciclo. No exemplo com continue, a assinatura relevante inclui i = 1 e o mesmo vetor; nada muda entre duas avaliações.

Logs precisam ser seletivos. Registrar apenas “executou” confirma repetição, mas não explica a causa. Registre os valores da condição, a medida de progresso e o caminho escolhido.

Faça um teste de mesa com poucas passagens antes de executar entradas grandes.

Teste as fronteiras e todos os caminhos de retorno

Para um laço que processa tarefas, teste:

  • estado inicial que já torna a condição falsa;
  • entrada que exige exatamente uma passagem;
  • quantidade divisível pelo tamanho do lote;
  • último lote menor que a capacidade;
  • menor e maior valores válidos;
  • caminho que executa continue;
  • caminho que executa break;
  • atualização zero, negativa ou maior que o alvo, quando puder chegar da entrada.

Não verifique apenas a saída. Confira também o número de iterações, o estado final e a propriedade de progresso. Um erro de limite pode terminar e ainda executar uma vez a mais; término e correção do resultado são obrigações relacionadas, mas diferentes.

Pratique com um estoque a separar

Um depósito possui 17 pedidos pendentes e separa até quatro por rodada.

  1. escreva o contrato de término em quatro respostas;
  2. implemente o laço sem deixar o estoque negativo;
  3. registre pendentes após cada rodada;
  4. indique uma medida de progresso e seu limite;
  5. teste entradas 0, 1, 4, 5 e 17;
  6. introduza propositalmente atualização ausente, direção invertida e passo zero;
  7. diagnostique cada defeito apenas pelo rastro do estado.

Depois, acrescente uma regra que ignore uma rodada cancelada. Garanta que esse caminho não impeça o avanço do estado responsável pelo término.

O que você deve guardar

Condição de parada, estado e progresso precisam concordar. Identifique uma saída alcançável e uma medida limitada que avance estritamente em toda passagem; depois teste cada caminho capaz de voltar à condição.

Estados repetidos revelam estagnação ou ciclos, alvos pulados denunciam igualdades frágeis e continue pode esconder uma atualização ausente. break, timeouts e limites são úteis quando fazem parte do contrato, mas não substituem a correção da lógica. Continue estudando como essas responsabilidades se combinam em estruturas de repetição aninhadas.

Referências