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 è continuare a leggere fino a quando non arrivi alla fine.

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

  1. C'è sempre true che indica che ci deve essere un'istruzione break o return da qualche parte all'interno del blocco.

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
    
  2. C'è il double read for method method.

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

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

Mi chiedo quale sia l'approccio meno probabile per introdurre un bug, più leggibile e comunemente usato.

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

    
posta cgTag 25.12.2013 - 18:08
fonte

4 risposte

50

Vorrei fare un passo indietro qui. Ti stai concentrando sui dettagli schizzinosi del codice ma 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++;
}

Che cos'è il significato di questo codice? Il significato è "fai un po 'di lavoro per ogni record in un file". Ma non è quello che il codice assomiglia a . Il codice sembra come "mantenere uno scostamento, aprire un file, inserire un ciclo senza condizioni di fine, leggere un record, verificare la nullità". Tutto questo prima di arrivare al lavoro! La domanda che dovresti porre è " come posso rendere l'aspetto di questo codice corrispondente alla sua semantica? " Questo codice dovrebbe essere:

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

Ora il codice si legge come la sua intenzione. Separa i tuoi meccanismi dalla tua semantica . Nel tuo codice originale mescoli il meccanismo - i dettagli del ciclo - con la semantica - il lavoro svolto su ogni record.

Ora dobbiamo implementare RecordsFromFile() . Qual è il modo migliore di implementarlo? A chi importa? Questo non è il codice che qualcuno sta guardando. È il codice del meccanismo di base e le sue dieci righe. Scrivilo come vuoi. Che ne dici 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 calcolata pigramente tutti i tipi di scenari diventano possibili:

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 si legge come un meccanismo o come il significato del codice?" Se la risposta è "come un meccanismo", prova a spostare quel meccanismo sul proprio metodo e scrivi il codice per renderlo più visibile.

    
risposta data 26.12.2013 - 23:01
fonte
19

Non hai bisogno di un ciclo infinito. Non dovresti mai aver bisogno di uno in scenari di lettura C #. Questo è il mio approccio preferito, assumendo che tu abbia davvero bisogno di 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 impostazione per il lettore, quindi ci sono due chiamate al metodo di lettura. La condizione while è nella parte superiore del ciclo, nel caso in cui non ci siano dati nel lettore.

    
risposta data 25.12.2013 - 18:15
fonte
6

Beh, dipende dalla tua situazione. Ma una delle soluzioni più "C # -ish" che posso pensare è quella di utilizzare l'interfaccia IEnumerable integrata e un ciclo foreach. L'interfaccia per IEnumerator chiama MoveNext solo con vero o falso, quindi la dimensione può essere sconosciuta. Quindi la logica di terminazione viene scritta una sola volta nell'enumeratore e non è necessario ripetere più di un punto.

MSDN fornisce un esempio di IEnumerator < T > . Dovrai anche creare un oggetto IEnumerable < T > per restituire l'IEnumerator < T >.

    
risposta data 25.12.2013 - 21:56
fonte
2

Quando ho operazioni di inizializzazione, di condizionamento e di incremento mi piace usare il ciclo for di cicli come C, C ++ e C #. In questo modo:

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!
}
    
risposta data 26.12.2013 - 03:34
fonte

Leggi altre domande sui tag