Como estruturar um loop que se repete até o sucesso e lida com falhas

8

Eu sou um programador autodidata. Comecei a programar cerca de 1,5 anos atrás. Agora comecei a ter aulas de programação na escola. Tivemos aulas de programação por 1/2 ano e agora temos outras 1/2.

Nas aulas, estamos aprendendo a programar em C ++ (que é uma linguagem que eu já sabia usar bastante antes de começarmos).

Não tive dificuldades durante esta aula, mas há um problema recorrente para o qual não consegui encontrar uma solução clara.

O problema é o seguinte (em Pseudocódigo):

 do something
 if something failed:
     handle the error
     try something (line 1) again
 else:
     we are done!

Aqui está um exemplo em C ++

O código solicita que o usuário insira um número e o faz até que a entrada seja válida. Ele usa cin.fail()para verificar se a entrada é inválida. Quando cin.fail()é que trueeu tenho que ligar cin.clear()e cin.ignore()poder continuar a receber informações do fluxo.

Estou ciente de que esse código não faz check-in EOF. Os programas que escrevemos não devem fazer isso.

Aqui está como eu escrevi o código em uma das minhas tarefas na escola:

for (;;) {
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        continue;
    }
    break;
}

Meu professor disse foi que eu não deveria estar usando breake continuecomo esta. Ele sugeriu que eu deveria usar um regular whileou do ... whileloop.

Parece-me que usar breake continueé a maneira mais simples de representar esse tipo de loop. Na verdade, eu pensei sobre isso por um bom tempo, mas não encontrei uma solução mais clara.

Eu acho que ele queria que eu fizesse algo assim:

do {
    cout << ": ";
    cin >> input;
    bool fail = cin.fail();
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (fail);

Para mim, esta versão parece muito mais complexa, pois agora também temos chamadas de variáveis failpara acompanhar e a verificação de falha de entrada é feita duas vezes em vez de apenas uma vez.

Também imaginei que posso escrever o código dessa maneira (abusando da avaliação de curto-circuito):

do {
    cout << ": ";
    cin >> input;
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (cin.fail() && (cin.clear(), cin.ignore(512, '\n', true);

Esta versão funciona exatamente como as outras. Ele não usa breakou continuee o cin.fail()teste é feito apenas uma vez. Contudo, não me parece correto abusar da "regra de avaliação de curto-circuito" como esta. Eu também não acho que meu professor gostaria.

Esse problema não se aplica apenas à cin.fail()verificação. Eu usei breake continueassim para muitos outros casos que envolvem a repetição de um conjunto de códigos até que uma condição seja atendida, onde algo também deve ser feito se a condição não for atendida (como chamar cin.clear()e a cin.ignore(...)partir do cin.fail()exemplo).

Eu continuei usando breake continueao longo do curso e agora meu professor parou de reclamar.

Quais são as suas opiniões sobre isso?

Você acha que meu professor está certo?

Você conhece uma maneira melhor de representar esse tipo de problema?

wefwefa3
fonte
6
Você não está repetindo para sempre, então por que você está usando algo que implica que está repetindo para sempre? Um loop para sempre nem sempre é uma má escolha (embora geralmente seja); nesse caso, é obviamente uma má escolha porque comunica a intenção incorreta do código. O segundo método está mais próximo da intenção do código, por isso é melhor. O código no segundo trecho pode ficar um pouco mais organizado e fácil de ler com uma reorganização mínima.
Dunk
3
@ Dunk Isso parece um pouco dogmático. Na verdade, é muito raro precisar fazer um loop para sempre, além do nível superior de programas interativos. Vejo while (true)e imediatamente supor que há um break, returnou throwem algum lugar lá. A introdução de uma variável extra que precisa ser monitorada apenas para evitar uma breakou continueé contraproducente. Eu argumentaria que o problema com o código inicial é que o loop nunca termina uma iteração normalmente e que você precisa saber que há um continuedetalhe interno ifpara saber breakque não será utilizado.
Doval
1
É muito cedo para você aprender a lidar com exceções?
Mawg diz que restabelece Monica
2
@ Dunk Você pode saber imediatamente o que foi planejado , mas terá que fazer uma pausa e pensar mais para verificar se está correto , o que é pior. dataIsValidé mutável, para saber que o loop termina corretamente, preciso verificar se está definido quando os dados são válidos e também se não está desabilitado em nenhum momento posteriormente. Em um loop do formulário, do { ... if (!expr) { break; } ... } while (true);eu sei que, se for expravaliado false, o loop será interrompido sem ter que olhar para o resto do corpo do loop.
Doval
1
@Doval: Eu também não sei como você lê o código, mas certamente não leio todas as linhas quando estou tentando descobrir o que está fazendo ou onde pode estar o erro que estou procurando. Se eu vi o loop while, é imediatamente aparente (do meu exemplo) que o loop está obtendo dados válidos. Não me importo com o que está fazendo, posso pular todo o resto do loop, a menos que aconteça que, por algum motivo, esteja obtendo dados inválidos. Então eu olhava o código dentro do loop. Sua ideia exige que eu analise todo o código do loop para descobrir que não é do meu interesse.
Dunk

Respostas:

15

Eu escreveria a instrução if um pouco diferente, por isso é usada quando a entrada é bem-sucedida.

for (;;) {
    cout << ": ";
    if (cin >> input)
        break;
    cin.clear();
    cin.ignore(512, '\n');
}

Também é mais curto.

O que sugere uma maneira mais curta que pode ser apreciada por seu professor:

cout << ": ";
while (!(cin >> input)) {
    cin.clear();
    cin.ignore(512, '\n');
    cout << ": ";
}
Sjoerd
fonte
É uma boa ideia reverter os condicionais assim, para que eu não exija um breakno final. Isso por si só torna o código muito mais fácil de ler. Também não sabia que isso !(cin >> input)era válido. Obrigado por me avisar!
precisa saber é o seguinte
8
O problema com a segunda solução é que ela duplica a linha cout << ": ";, violando o princípio DRY. Para o código do mundo real, isso seria inútil, uma vez que se torna propenso a erros (quando você precisar alterar essa parte posteriormente, precisará garantir que não se esqueça de alterar duas linhas da mesma maneira agora). Neverthelss dei-lhe um +1 para a sua primeira versão, que é um exemplo de como usá-lo breakcorretamente.
Doc Brown
2
Parece que você está mexendo nos cabelos, considerando que a linha duplicada é obviamente um pouco de interface do usuário comum. Você poderia remover o primeiro cout << ": "totalmente e nada quebraria.
Jdevlin
2
@codingthewheel: você está correto, este exemplo é muito trivial para demonstrar os riscos potenciais em ignorar o princípio DRY. Mas imagine essa situação no código do mundo real, onde a introdução de bugs acidentalmente pode ter consequências reais - então você pode pensar diferente sobre isso. É por isso que, após três décadas de programação, adquiri o hábito de nunca resolver um problema de saída de loop duplicando a primeira parte apenas "porque um whilecomando parece mais agradável".
Doc Brown
1
@DocBrown A duplicação do cout é principalmente um artefato do exemplo. Na prática, as duas instruções cout serão diferentes, pois a segunda conterá uma mensagem de erro. Por exemplo, "Digite <foo>:" e "Erro! Digite novamente <foo>:" .
precisa saber é o seguinte
15

O que você precisa se esforçar é evitar loops crus .

Mova a lógica complexa para uma função auxiliar e, de repente, as coisas ficam muito mais claras:

bool getValidUserInput(string & input)
{
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        return false;
    }
    return true;
}

int main() {
    string input;
    while (!getValidUserInput(input)) { 
        // We wait for a valid user input...
    }
}
glampert
fonte
1
Isso é basicamente o que eu ia sugerir. Refatorar para separar a obtenção da entrada da decisão de obter a entrada.
Rob K
4
Concordo que esta é uma boa solução. Torna-se a alternativa muito superior quando getValidUserInputé chamada muitas vezes e não apenas uma vez. Dou +1 para isso, mas aceitarei a resposta de Sjoerd, porque acho que ela responde melhor à minha pergunta.
wefwefa3
@ user3787875: exatamente o que eu pensei quando li as duas respostas - boa escolha.
Doc Brown
Este é o próximo passo lógico após a reescrita na minha resposta.
precisa saber é o seguinte
9

Não é tão for(;;)ruim assim. Não é tão claro quanto padrões como:

while (cin.fail()) {
    ...
}

Ou como Sjoerd colocou:

while (!(cin >> input)) {
    ...
}

Vamos considerar o público principal desse material como sendo seus colegas programadores, incluindo a versão futura de si mesmo que não se lembra mais do porquê você parou o intervalo no final dessa maneira ou pulou o intervalo com a continuação. Este padrão do seu exemplo:

for (;;) {
    ...
    if (blah) {
        continue;
    }
    break;
}

... requer alguns segundos de reflexão extra para simular versus outros padrões. Não é uma regra rígida e rápida, mas saltar a instrução break com a continuação parece confuso ou inteligente sem ser útil, e colocar a instrução break no final do ciclo, mesmo que funcione, é incomum. Normalmente ambos breake continuesão usados ​​para evacuar prematuramente o loop ou a iteração do loop, portanto, ver qualquer um deles no final parece um pouco estranho, mesmo que não seja.

Fora isso, coisas boas!

jdevlin
fonte
Sua resposta parece boa - mas apenas à primeira vista. De fato, ele oculta o problema inerente que podemos ver no @Sjoerd s answer: using a while` loop desta maneira pode levar à duplicação desnecessária de código para o exemplo dado. E esse é um problema que é IMHO pelo menos tão importante quanto a legibilidade geral do loop.
Doc Brown
Relaxe, doutor. O OP pediu alguns conselhos / feedback básicos sobre sua estrutura de loop, eu dei a ele a interpretação padrão. Você pode argumentar em favor de outras interpretações - a abordagem padrão nem sempre é a melhor ou a mais matematicamente rigorosa -, mas principalmente porque seu próprio instrutor se inclinou para o uso da while, acho que a parte "apenas à primeira vista" é injustificada .
Jdevlin
Não me interpretem mal, isso não tem nada a ver com ser "matematicamente rigoroso" - trata-se de escrever código evolutivo - código em que adicionar novos recursos ou alterar os recursos existentes tem o menor risco possível de introdução de bugs. No meu trabalho, isso realmente não é um problema acadêmico.
Doc Brown
Gostaria de acrescentar que concordo principalmente com a sua resposta. Eu só queria salientar que sua simplificação no começo pode esconder um problema frequente. Quando tiver a chance de usar "while" sem duplicação de código, não hesitarei em usá-lo.
Doc Brown
1
Concordado e, às vezes, uma ou duas linhas desnecessárias antes do loop é o preço que pagamos por essa whilesintaxe declarativa de carregamento antecipado . Eu não gosto de fazer algo antes do loop que também é feito dentro do loop; então, novamente, eu também não gosto de um loop que se liga a nós tentando refatorar o prefixo. Então, um pouco de um ato de equilíbrio de qualquer maneira.
jdevlin
3

Meu instrutor de lógica na escola sempre dizia e metia no meu cérebro que só deveria haver uma entrada e uma saída para os loops. Caso contrário, você começará a obter o código espaguete e não saberá para onde o código está indo durante a depuração.

Na vida real, acho que a legibilidade do código é realmente importante, de modo que, se alguém precisar corrigir ou depurar um problema com seu código, é fácil para eles.

Além disso, se for um problema de desempenho, porque você está executando esse visual milhões de vezes o loop for pode ser mais rápido. O teste responderia a essa pergunta.

Não conheço C ++, mas entendi completamente os dois primeiros exemplos de código. Estou assumindo que continue vai até o final do loop for e depois o executa novamente. O exemplo while eu entendi completamente, mas para mim foi mais fácil entender do que o seu exemplo (;;). O terceiro, eu teria que fazer algum trabalho de reflexão para descobrir isso.

Então, passando por mim o que era mais fácil de ler (sem saber c ++) e o que meu instrutor de lógica disse que eu usaria o loop while.

Jaydel Gluckie
fonte
4
Então, seu instrutor é contra o uso de exceções? E retorno antecipado? E coisas semelhantes? Devidamente empregados, eles todo o código make mais legível ...
Deduplicator
1
A frase-chave na sua frase elipsizada é "empregada adequadamente". Minha experiência amarga parece-me indicar que malditamente perto de NINGUÉM nesta raquete louca sabe como empregar essas coisas "adequadamente", para quase qualquer definição razoável de "corretamente" (ou "legível").
John R. Strohm
2
Lembre-se de que estamos falando de uma aula em que os alunos são iniciantes. Certamente, treiná-los para evitar essas coisas que causam problemas comuns enquanto iniciantes (antes que eles entendam as implicações e possam raciocinar sobre o que seria uma exceção à regra) é uma boa idéia.
precisa saber é o seguinte
2
O dogma "uma entrada - uma vez saída" é IMHO um ponto de vista antiquado, provavelmente "inventado" por Edward Dijkstra em uma época em que as pessoas brincavam com os GOTOs em idiomas como Basic, Pascal e COBOL. Concordo plenamente com o que o Deduplicator escreveu acima. Além disso, os dois primeiros exemplos do OP realmente contêm apenas uma entrada e uma saída, mas a legibilidade é medíocre.
Doc Brown
4
Concorde com @DocBrown. "Uma entrada, uma saída" nas bases de código de produção modernas tende a ser uma receita para o código de fragmento e incentiva o assentamento profundo das estruturas de controle.
jdevlin
-2

para mim, a tradução C mais direta do pseudocódigo é

do
    {
    success = something();
    if (success == FALSE)
        {
        handle_the_error();
        }
    } while (success == FALSE)
\\ moving on ...

Não entendo por que essa tradução óbvia é um problema.

talvez isto:

while (!something())
    {
    handle_the_error();
    }

isso parece mais simples.

Robert Bristow-Johnson
fonte