Quale modo di terminare il ciclo di lettura è l'approccio preferito?


13

Quando devi iterare un lettore in cui il numero di elementi da leggere è sconosciuto e l'unico modo per farlo è quello di continuare a leggere fino a raggiungere il traguardo.

Questo è spesso il posto in cui hai bisogno di un ciclo infinito.

  1. C'è sempre trueciò che indica che ci deve essere un'istruzione breako da qualche parte all'interno del blocco.return

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
    
  2. C'è il doppio metodo di lettura per il ciclo.

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
    
  3. C'è il singolo ciclo while letto. Non tutte le lingue supportano questo metodo .

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

Mi chiedo quale approccio abbia meno probabilità di introdurre un bug, più leggibile e comunemente usato.

Ogni volta che devo scrivere uno di questi penso "deve esserci un modo migliore" .


2
Perché stai mantenendo un offset? La maggior parte dei lettori di stream non ti consente semplicemente di "leggere dopo?"
Robert Harvey,

@RobertHarvey nella mia attuale esigenza il lettore ha una query SQL sottostante che utilizza l'offset per impaginare i risultati. Non so quanto siano lunghi i risultati della query fino a quando non restituisce un risultato vuoto. Ma, per la domanda non è davvero un requisito.
Reactgular il

3
Sembri confuso: il titolo della domanda riguarda i loop infiniti, ma il testo della domanda è tutto sulla terminazione dei loop. La soluzione classica (dai tempi della programmazione strutturata) è fare un ciclo di pre-lettura, mentre si hanno i dati, e rileggerlo come ultima azione nel ciclo. È semplice (soddisfare il requisito di bug), il più comune (poiché è stato scritto per 50 anni). Il più leggibile è una questione di opinione.
andy256,

@ andy256 confuso è una condizione pre-caffè per me.
Reactgular,

1
Ah, quindi la procedura corretta è 1) Bevi caffè evitando la tastiera, 2) Inizia il ciclo di codifica.
Andy256,

Risposte:


49

Vorrei fare un passo indietro qui. Ti stai concentrando sui dettagli esigenti del codice ma ti manca l'immagine più grande. Diamo un'occhiata a uno dei tuoi loop di esempio:

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

Qual è il significato di questo codice? Il significato è "fai un po 'di lavoro per ogni record in un file". Ma non è così che appare il codice . Il codice appare come "mantieni un offset. Apri un file. Inserisci un ciclo senza condizioni finali. Leggi un record. Verifica la nullità". Tutto ciò prima di arrivare al lavoro! La domanda che dovresti porre è " come posso fare in modo che l'aspetto di questo codice corrisponda alla sua semantica? " Questo codice dovrebbe essere:

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

Ora il codice si legge come la sua intenzione. Separare i meccanismi dalla semantica . Nel codice originale si confonde il meccanismo - i dettagli del ciclo - con la semantica - il lavoro svolto per ciascun record.

Ora dobbiamo implementare RecordsFromFile(). Qual è il modo migliore per implementarlo? Che importa? Questo non è il codice che qualcuno guarderà. È un codice meccanismo di base e le sue dieci righe. Scrivilo come vuoi. Cosa ne pensi di questo?

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

Ora che stiamo manipolando una sequenza di record calcolati pigramente diventano possibili tutti i tipi di scenari:

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 così via.

Ogni volta che scrivi un ciclo, chiediti "questo ciclo legge come un meccanismo o come il significato del codice?" Se la risposta è "come un meccanismo", prova a spostare quel meccanismo nel suo metodo e scrivi il codice per renderlo più visibile.


3
+1 finalmente una risposta sensata. Questo è esattamente quello che farò. Grazie.
Reactgular,

1
"prova a spostare quel meccanismo sul suo metodo" - sembra molto simile al refactoring del metodo Extract ? "Trasforma il frammento in un metodo il cui nome spiega lo scopo del metodo."
moscerino del

2
@gnat: Quello che sto suggerendo è leggermente più coinvolto del "metodo extract", che penso sia semplicemente spostare un pezzo di codice in un altro posto. L'estrazione di metodi è sicuramente un buon passo per rendere il codice più simile alla sua semantica. Sto suggerendo che l'estrazione del metodo venga eseguita in modo ponderato, con l'obiettivo di mantenere separate politiche e meccanismi.
Eric Lippert,

1
@gnat: esattamente! In questo caso il dettaglio che desidero estrarre è il meccanismo mediante il quale tutti i record vengono letti dal file, mantenendo la politica. La politica è "dobbiamo fare del lavoro su ogni disco".
Eric Lippert,

1
Vedo. In questo modo, è più facile da leggere e mantenere. Studiando questo codice, posso concentrarmi separatamente su politica e meccanismo, non mi costringe a distogliere l'attenzione
moscerino

19

Non hai bisogno di un ciclo infinito. Non dovresti mai averne bisogno in scenari di lettura in C #. Questo è il mio approccio preferito, supponendo che sia davvero necessario mantenere un offset:

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

Questo approccio riconosce il fatto che esiste una fase di installazione per il lettore, quindi ci sono due chiamate al metodo di lettura. La whilecondizione è all'inizio del ciclo, nel caso in cui non ci siano dati nel lettore.


6

Bene dipende dalla tua situazione. Ma una delle soluzioni "C # -ish" che mi viene in mente è quella di utilizzare l'interfaccia IEnumerable integrata e un ciclo foreach. L'interfaccia per IEnumerator chiama MoveNext solo con true o false, quindi le dimensioni possono essere sconosciute. Quindi la tua logica di terminazione viene scritta una volta - nell'enumeratore - e non devi ripetere in più di un punto.

MSDN fornisce un esempio di IEnumerator <T> . Sarà inoltre necessario creare un IEnumerable <T> per restituire IEnumerator <T>.


Puoi fare un esempio di codice.
Reactgular,

grazie, questo è quello che ho deciso di fare. Mentre non penso che creare nuove classi ogni volta che hai un ciclo infinito risolva la domanda.
Reactgular il

Sì, so cosa vuoi dire - un sacco di spese generali. Il vero genio / problema degli iteratori è che sono sicuramente configurati per essere in grado di far scorrere due cose su una collezione contemporaneamente. Tante volte non è necessario che la funzionalità in modo da potrebbe semplicemente avere l'involucro intorno agli oggetti implementare sia IEnumerable e IEnumerator. Un altro modo di guardare è che qualunque raccolta sottostante di cose su cui stai ripetendo non è stata progettata pensando al paradigma C # accettato. E va bene. Un ulteriore vantaggio degli iteratori è che puoi ottenere gratuitamente tutte le cose parallele di LINQ!
J Trana,

2

Quando ho operazioni di inizializzazione, condizione e incremento mi piace usare i cicli for di linguaggi come C, C ++ e C #. Come questo:

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

O questo se pensi che questo sia più leggibile. Personalmente preferisco il primo.

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.