Który sposób zakończenia pętli odczytu jest preferowanym podejściem?

13

Gdy musisz powtórzyć czytnik, w którym liczba pozycji do odczytania jest nieznana, a jedynym sposobem jest kontynuowanie czytania, aż do końca.

Często jest to miejsce, w którym potrzebujesz niekończącej się pętli.

  1. Zawsze jest to, trueco oznacza, że gdzieś wewnątrz bloku musi być instrukcja breaklubreturn

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
  2. Istnieje metoda podwójnego odczytu dla pętli.

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
  3. Jest jedna pętla odczytu podczas odczytu. Nie wszystkie języki obsługują tę metodę .

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

Zastanawiam się, które podejście jest najmniej prawdopodobne, aby wprowadzić błąd, najbardziej czytelny i powszechnie używany.

Za każdym razem, gdy muszę napisać jedną z nich, myślę „musi być lepszy sposób” .

Reactgular
źródło
2
Dlaczego utrzymujesz offset? Czy większość czytników strumieniowych nie pozwala po prostu „czytać dalej?”
Robert Harvey,
@RobertHarvey w mojej obecnej potrzebie czytelnik ma podstawowe zapytanie SQL, które wykorzystuje przesunięcie do dzielenia wyników na strony. Nie wiem, jak długo trwają wyniki zapytania, dopóki nie zwróci pustego wyniku. Ale pytanie nie jest tak naprawdę wymogiem.
Reactgular,
3
Wydajesz się zdezorientowany - tytuł pytania dotyczy niekończących się pętli, ale tekst pytania dotyczy zakończenia pętli. Klasycznym rozwiązaniem (od dni programowania strukturalnego) jest wykonanie odczytu wstępnego, zapętlenie, gdy masz dane, i ponowne odczytanie jako ostatnia czynność w pętli. To proste (spełniające wymagania dotyczące błędów), najczęstsze (ponieważ jest pisane od 50 lat). Najbardziej czytelny jest kwestia opinii.
andy256
@ andy256 mylić to warunek przed kawą dla mnie.
Reactgular,
1
Ach, więc poprawna procedura to 1) Pij kawę unikając klawiatury, 2) Rozpocznij pętlę kodowania.
andy256

Odpowiedzi:

49

Cofnąłbym się tutaj. Koncentrujesz się na wybrednych szczegółach kodu, ale brakuje większego obrazu. Rzućmy okiem na jedną z twoich przykładowych pętli:

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

Jakie jest znaczenie tego kodu? Oznacza to: „popracuj nad każdym rekordem w pliku”. Ale nie tak wygląda kod . Kod wygląda jak „zachowaj przesunięcie. Otwórz plik. Wejdź w pętlę bez warunku zakończenia. Przeczytaj rekord. Testuj pod kątem nieważności”. Wszystko to zanim przejdziemy do pracy! Pytanie, które powinieneś zadać, brzmi: „w jaki sposób mogę dopasować wygląd tego kodu do semantyki? ” Ten kod powinien być:

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

Teraz kod brzmi jak jego intencja. Oddziel swoje mechanizmy od semantyki . W swoim oryginalnym kodzie łączysz mechanizm - szczegóły pętli - z semantyką - pracą wykonaną dla każdego rekordu.

Teraz musimy wdrożyć RecordsFromFile(). Jaki jest najlepszy sposób na wdrożenie tego? Kogo to obchodzi? To nie jest kod, na który ktoś będzie patrzył. Jest to podstawowy kod mechanizmu i jego dziesięć linii. Napisz, jak chcesz. Co powiesz na to?

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

Teraz, gdy manipulujemy leniwie obliczoną sekwencją rekordów, możliwe są wszelkiego rodzaju scenariusze:

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);

I tak dalej.

Za każdym razem, gdy piszesz pętlę, zadaj sobie pytanie: „czy ta pętla czyta się jak mechanizm czy jak znaczenie kodu?” Jeśli odpowiedź brzmi „jak mechanizm”, spróbuj przenieść ten mechanizm do własnej metody i napisz kod, aby znaczenie było bardziej widoczne.

Eric Lippert
źródło
3
+1 wreszcie rozsądna odpowiedź. Właśnie to zrobię. Dzięki.
Reactgular,
1
„spróbuj przenieść ten mechanizm na własną metodę” - brzmi to podobnie do refaktoryzacji metody ekstrakcji, prawda? „Zamień fragment w metodę, której nazwa wyjaśnia cel tej metody”.
komar
2
@gnat: To, co sugeruję, jest nieco bardziej zaangażowane niż „wyodrębnianie metody”, co uważam za przeniesienie jednego fragmentu kodu w inne miejsce. Wyodrębnianie metod jest zdecydowanie dobrym krokiem do uczynienia kodu bardziej podobnym do semantyki. Sugeruję, aby ekstrakcję metody przeprowadzić z rozwagą, z myślą o oddzieleniu polityk i mechanizmów.
Eric Lippert,
1
@gnat: Dokładnie! W tym przypadku szczegółem, który chcę wyodrębnić, jest mechanizm, za pomocą którego wszystkie rekordy są odczytywane z pliku, przy zachowaniu zasad. Polityka jest taka, że ​​„musimy popracować nad każdą płytą”.
Eric Lippert,
1
Widzę. W ten sposób łatwiej jest czytać i utrzymywać. Studiując ten kod, mogę skupić się osobno na polityce i mechanizmie, nie zmusza mnie to do podzielenia uwagi
gnat
19

Nie potrzebujesz niekończącej się pętli. Nigdy nie powinien być potrzebny w scenariuszach odczytu C #. To jest moje preferowane podejście, zakładając, że naprawdę musisz zachować przesunięcie:

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

Takie podejście potwierdza fakt, że istnieje etap konfiguracji czytnika, więc istnieją dwa wywołania metody odczytu. whileStan jest na początek pętli, tylko w przypadku braku danych w ogóle w czytniku.

Robert Harvey
źródło
6

To zależy od twojej sytuacji. Ale jednym z bardziej „C # -ishowych” rozwiązań, które mogę wymyślić, jest użycie wbudowanego interfejsu IEnumerable i pętli foreach. Interfejs IEnumerator wywołuje tylko MoveNext z prawdą lub fałszem, więc rozmiar może być nieznany. Następnie logika zakończenia jest zapisywana raz - w module wyliczającym - i nie trzeba powtarzać w więcej niż jednym miejscu.

MSDN stanowi przykład IEnumerator <T> . Musisz także utworzyć IEnumerable <T>, aby zwrócić IEnumerator <T>.

J Trana
źródło
Czy możesz podać przykładowy kod?
Reactgular,
dzięki, to właśnie postanowiłem zrobić. Chociaż nie sądzę, aby tworzenie nowych klas za każdym razem, gdy masz nieskończoną pętlę, rozwiązało to pytanie.
Reactgular,
Tak, wiem o co ci chodzi - dużo kosztów ogólnych. Prawdziwym geniuszem / problemem iteratorów jest to, że są one na pewno skonfigurowane tak, aby móc iterować dwie rzeczy w tym samym czasie w kolekcji. Tyle razy nie trzeba tę funkcjonalność, dzięki czemu mógłby po prostu mają owijkę wokół obiektów wdrożenia zarówno IEnumerable i IEnumerator. Innym sposobem patrzenia jest to, że cokolwiek leżącego u podstaw kolekcji rzeczy, które iterujesz, nie zostało zaprojektowane z uwzględnieniem przyjętego paradygmatu C #. I to jest w porządku. Dodatkową zaletą iteratorów jest to, że możesz uzyskać wszystkie równoległe rzeczy LINQ za darmo!
J Trana,
2

Kiedy mam operacje inicjalizacji, warunku i przyrostu, lubię używać pętli dla języków takich jak C, C ++ i C #. Lubię to:

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

Lub jeśli uważasz, że jest to bardziej czytelne. Ja osobiście wolę ten pierwszy.

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
Trylks
źródło