Qual a maneira de finalizar o loop de leitura é a abordagem preferida?

13

Quando você precisa repetir um leitor em que o número de itens a ler é desconhecido, e a única maneira de fazer isso é continuar lendo até chegar ao fim.

Geralmente, é o lugar em que você precisa de um loop sem fim.

  1. Há sempre o trueque indica que deve haver uma declaração breakou em algum lugar dentro do bloco.return

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
  2. Existe o método de leitura dupla para loop.

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
  3. Existe o único loop while de leitura. Nem todos os idiomas suportam esse método .

    int offset = 0;
    Record r = null;
    while((r = Read(++offset)) != null)
    {
        // do work
    }

Eu estou querendo saber qual abordagem é a menos provável de introduzir um bug, mais legível e comumente usada.

Toda vez que tenho que escrever um desses, penso que "deve haver uma maneira melhor" .

Reactgular
fonte
2
Por que você está mantendo um deslocamento? A maioria dos leitores de fluxo não permite que você simplesmente "leia a seguir?"
Robert Harvey
@RobertHarvey, na minha necessidade atual, o leitor tem uma consulta SQL subjacente que usa o deslocamento para paginar os resultados. Não sei quanto tempo os resultados da consulta são até que retorne um resultado vazio. Mas, a questão não é realmente um requisito.
Reactgular
3
Você parece confuso - o título da pergunta é sobre loops infinitos, mas o texto da pergunta é sobre loops de finalização. A solução clássica (desde os dias da programação estruturada) é fazer um loop de pré-leitura enquanto você tem dados e ler novamente como a última ação no loop. É simples (atendendo ao requisito do bug), o mais comum (desde que foi escrito por 50 anos). O mais legível é uma questão de opinião.
andy256
@ andy256 confused é condição pré-café para mim.
Reactgular
1
Ah, então o procedimento correto é 1) Beba café enquanto evita o teclado, 2) Comece o ciclo de codificação.
andy256

Respostas:

49

Eu daria um passo para trás aqui. Você está se concentrando nos detalhes exigentes do código, mas está perdendo a imagem maior. Vamos dar uma olhada em um dos seus exemplos de loops:

int offset = 0;
while(true)
{
    Record r = Read(offset);
    if(r == null)
    {
        break;
    }
    // do work
    offset++;
}

Qual é o significado desse código? O significado é "faça algum trabalho para cada registro em um arquivo". Mas não é assim que o código se parece . O código parece "manter um deslocamento. Abra um arquivo. Insira um loop sem condição final. Leia um registro. Teste a nulidade". Tudo isso antes de começarmos o trabalho! A pergunta que você deve fazer é " como posso fazer a aparência desse código corresponder à sua semântica? " Esse código deve ser:

foreach(Record record in RecordsFromFile())
    DoWork(record);

Agora o código parece com a sua intenção. Separe seus mecanismos da sua semântica . No seu código original, você mistura o mecanismo - os detalhes do loop - com a semântica - o trabalho realizado em cada registro.

Agora temos que implementar RecordsFromFile(). Qual é a melhor maneira de implementar isso? Quem se importa? Esse não é o código que alguém vai ver. É o código básico do mecanismo e suas dez linhas. Escreva como quiser. Que tal agora?

public IEnumerable<Record> RecordsFromFile()
{
    int offset = 0;
    while(true)
    {
        Record record = Read(offset);
        if (record == null) yield break;
        yield return record;
        offset += 1;
    }
}

Agora que estamos manipulando uma sequência de registros lenta e computada, todos os tipos de cenários se tornam possíveis:

foreach(Record record in RecordsFromFile().Take(10))
    DoWork(record);

foreach(Record record in RecordsFromFile().OrderBy(r=>r.LastName))
    DoWork(record);

foreach(Record record in RecordsFromFile().Where(r=>r.City == "London")
    DoWork(record);

E assim por diante.

Sempre que você escreve um loop, pergunte a si mesmo "esse loop é como um mecanismo ou como o significado do código?" Se a resposta for "como um mecanismo", tente mover esse mecanismo para seu próprio método e escreva o código para tornar o significado mais visível.

Eric Lippert
fonte
3
+1 finalmente uma resposta sensata. Isto é exatamente o que eu farei. Obrigado.
Reactgular
1
"tente mover esse mecanismo para seu próprio método" - parece muito com a refatoração do método de extração , não é? "Transforme o fragmento em um método cujo nome explica a finalidade do método."
precisa
2
@gnat: O que estou sugerindo é um pouco mais envolvido do que o "método de extração", que considero apenas mover um pedaço de código para outro lugar. A extração de métodos é definitivamente um bom passo para tornar o código mais parecido com sua semântica. Estou sugerindo que a extração do método seja feita cuidadosamente, com o objetivo de manter políticas e mecanismos separados.
precisa
1
@gnat: Exatamente! Nesse caso, o detalhe que quero extrair é o mecanismo pelo qual todos os registros são lidos no arquivo, mantendo a política. A política é "nós temos que fazer algum trabalho em cada registro".
precisa
1
Entendo. Dessa forma, é mais fácil ler e manter. Estudando este código, posso me concentrar em separado sobre a política e mecanismo, não me forçar a atenção dividida
mosquito
19

Você não precisa de um loop sem fim. Você nunca deve precisar de um em cenários de leitura de C #. Esta é minha abordagem preferida, supondo que você realmente precise manter um deslocamento:

Record r = Read(0);
offset=1;
while(r != null)
{
    // Do work
    r = Read(offset);
    offset++
}

Essa abordagem reconhece o fato de que há uma etapa de configuração para o leitor, portanto, há duas chamadas de método de leitura. A whilecondição está no topo do loop, apenas no caso de não haver dados no leitor.

Robert Harvey
fonte
6

Bem, isso depende da sua situação. Mas uma das soluções "c # -ish" em que consigo pensar é usar a interface IEnumerable interna e um loop foreach. A interface do IEnumerator apenas chama MoveNext com true ou false, portanto o tamanho pode ser desconhecido. Então sua lógica de terminação é escrita uma vez - no enumerador - e você não precisa repetir em mais de um ponto.

O MSDN fornece um exemplo de IEnumerator <T> . Você também precisará criar um IEnumerable <T> para retornar o IEnumerator <T>.

J Trana
fonte
Você pode dar um exemplo de código.
Reactgular
obrigado, é isso que eu decidi fazer. Embora eu não ache que criar novas classes cada vez que você tenha um loop infinito, resolva a questão.
Reactgular
Sim, eu sei o que você quer dizer - muita sobrecarga. O verdadeiro gênio / problema dos iteradores é que eles são definitivamente configurados para poderem fazer duas coisas repetirem uma coleção ao mesmo tempo. Tantas vezes você não precisa que a funcionalidade para que você poderia apenas ter o invólucro ao redor dos objetos implementar tanto IEnumerable e IEnumerator. Outra maneira de ver é que qualquer coleção subjacente de coisas nas quais você está interagindo não foi projetada com o paradigma C # aceito em mente. E tudo bem. Um bônus adicional de iteradores é que você pode obter todo o material paralelo do LINQ de graça!
J Trana
2

Quando tenho operações de inicialização, condição e incremento, gosto de usar loops de linguagens como C, C ++ e C #. Como isso:

for (int offset = 0, Record r = Read(offset); r != null; r = Read(++offset)){
    // loop here!
}

Ou isso, se você acha que isso é mais legível. Eu pessoalmente prefiro o primeiro.

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
Trylks
fonte