Какой способ прервать цикл чтения является предпочтительным?

13

Когда вам нужно перебрать читателя, где количество элементов для чтения неизвестно, и единственный способ сделать это - продолжить чтение до конца.

Это часто место, где вам нужен бесконечный цикл.

  1. Существует Всегда trueчто указывает на то , должно быть , breakили returnзаявление где - то внутри блока.

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
  2. Существует метод двойного чтения для цикла.

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
  3. Существует один цикл чтения пока. Не все языки поддерживают этот метод .

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

Мне интересно, какой подход наименее вероятен для введения ошибки, наиболее читаемый и широко используемый.

Каждый раз, когда мне приходится писать один из них, я думаю, что «должен быть лучший путь» .

Reactgular
источник
2
Почему вы поддерживаете компенсацию? Разве большинство потоковых ридеров не позволяют вам просто «читать дальше»?
Роберт Харви
@RobertHarvey в моей текущей потребности, читатель имеет базовый SQL-запрос, который использует смещение для разбивки на страницы результатов. Я не знаю, как долго результаты запроса, пока он не возвращает пустой результат. Но, для вопроса, это не совсем требование.
Reactgular
3
Вы, кажется, смущены - заголовок вопроса о бесконечных циклах, но текст вопроса о завершающих циклах. Классическое решение (со времен структурированного программирования) состоит в том, чтобы выполнить предварительное чтение, цикл, пока у вас есть данные, и чтение снова как последнее действие в цикле. Это просто (соответствует требованию ошибки), наиболее распространенное (так как оно написано в течение 50 лет). Наиболее читаемым является вопрос мнения.
andy256 26.12.13
@ andy256 запутался, это состояние перед кофе для меня.
Reactgular
1
Ах, так правильная процедура: 1) Пейте кофе, избегая клавиатуры, 2) Начните цикл кодирования.
andy256

Ответы:

49

Я бы сделал шаг назад сюда. Вы концентрируетесь на придирчивых деталях кода, но упускаете общую картину. Давайте посмотрим на один из ваших примеров циклов:

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

В чем смысл этого кода? Смысл в том, чтобы «сделать некоторую работу с каждой записью в файле». Но это не то, на что похож код . Код выглядит как «поддерживать смещение. Откройте файл. Введите цикл без конечного условия. Прочитайте запись. Проверьте на ничтожность». Все это, прежде чем мы приступим к работе! Вопрос, который вы должны задать: « Как я могу сделать внешний вид этого кода соответствующим его семантике? » Этот код должен быть:

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

Теперь код читается как его намерение. Отделите свои механизмы от вашей семантики . В своем исходном коде вы смешиваете механизм - детали цикла - с семантикой - работу, проделанную с каждой записью.

Теперь мы должны реализовать RecordsFromFile(). Каков наилучший способ реализации этого? Какая разница? Это не тот код, на который будет смотреть кто-либо. Это базовый код механизма и его длина в десять строк. Напиши как хочешь. Как насчет этого?

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

Теперь, когда мы манипулируем лениво вычисляемой последовательностью записей, возможны все виды сценариев:

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

И так далее.

Каждый раз, когда вы пишете цикл, спрашивайте себя: «Этот цикл читается как механизм или как смысл кода?» Если ответ «как механизм», попробуйте переместить этот механизм в его собственный метод и напишите код, чтобы значение стало более видимым.

Эрик Липперт
источник
3
+1 наконец разумный ответ. Это именно то, что я сделаю. Благодарю.
Reactgular
1
«попытаться перенести этот механизм в свой собственный метод» - это очень похоже на рефакторинг Извлечения метода , не так ли? «Превратить фрагмент в метод, имя которого объясняет назначение метода».
Комнат
2
@gnat: То, что я предлагаю, немного сложнее, чем «метод извлечения», который я считаю простым перемещением одного куска кода в другое место. Извлечение методов, безусловно, является хорошим шагом в том, чтобы сделать код более похожим на его семантику. Я предлагаю, чтобы извлечение методов было сделано вдумчиво, с прицелом на разделение политики и механизмов.
Эрик Липперт
1
@gnat: Точно! В этом случае подробности, которые я хочу извлечь, - это механизм, с помощью которого все записи читаются из файла при сохранении политики. Политика заключается в том, что «мы должны делать какую-то работу над каждой записью».
Эрик Липперт
1
Понимаю. Таким образом, его легче читать и поддерживать. Изучая этот код, я могу отдельно сосредоточиться на политике и механизме, это не заставляет меня разделять внимание
комнат
19

Вам не нужен бесконечный цикл. Вы никогда не должны нуждаться в этом в сценариях чтения C #. Это мой предпочтительный подход, предполагающий, что вам действительно нужно поддерживать смещение:

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

Этот подход учитывает тот факт, что для читателя существует шаг настройки, поэтому существует два вызова метода чтения. whileСостояние находится в верхней части петли, только в случае , если нет данных на все в читателе.

Роберт Харви
источник
6

Ну, это зависит от вашей ситуации. Но одно из наиболее "C # -ish" решений, о которых я могу подумать, - это использовать встроенный интерфейс IEnumerable и цикл foreach. Интерфейс для IEnumerator вызывает MoveNext только с true или false, поэтому размер может быть неизвестен. Тогда ваша логика завершения записывается один раз - в счетчике - и вам не нужно повторять более чем в одном месте.

MSDN предоставляет пример IEnumerator <T> . Вам также потребуется создать IEnumerable <T>, чтобы вернуть IEnumerator <T>.

Дж Трана
источник
Можете ли вы привести пример кода.
Reactgular
спасибо, это то, что я решил сделать. Хотя я не думаю, что создание новых классов каждый раз, когда у вас есть бесконечный цикл, решает вопрос.
Reactgular
Да, я знаю, что вы имеете в виду - много накладных расходов. Настоящая гениальность / проблема итераторов заключается в том, что они определенно настроены так, чтобы иметь возможность выполнять две итерации по коллекции одновременно. Очень часто вам не нужны эти функциональные возможности, поэтому вы можете просто обернуть объекты вокруг объектов, реализующих IEnumerable и IEnumerator. Другой способ взглянуть на это - то, что любой основной материал, который вы перебираете, не был разработан с учетом принятой парадигмы C #. И это нормально. Дополнительный бонус итераторов заключается в том, что вы можете получить весь параллельный LINQ бесплатно!
J Trana
2

Когда у меня есть операции инициализации, условия и приращения, мне нравится использовать циклы for таких языков, как C, C ++ и C #. Как это:

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

Или это, если вы думаете, что это более читабельно. Я лично предпочитаю первый.

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
Trylks
источник