Який спосіб припинити цикл читання є кращим підходом?


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
    }

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

Кожного разу, коли мені доводиться писати одне із таких, я думаю, що "повинен бути кращий шлях" .


2
Чому ви підтримуєте компенсацію? Чи не більшість читачів потоку дозволяють вам просто "читати далі?"
Роберт Харві

@RobertHarvey в моїй теперішній потребі у читача є базовий SQL-запит, який використовує зміщення для створення результатів. Я не знаю, скільки тривають результати запиту, поки він не поверне порожній результат. Але, для питання це насправді не є вимогою.
Реакційний

3
Ви здаєтесь заплутаним - заголовок питання стосується нескінченних циклів, але текст питання стосується закінчення циклів. Класичне рішення (з часів структурованого програмування) - зробити попереднє читання, цикл, поки у вас є дані, і прочитати ще раз, як остання дія в циклі. Це просте (задоволення вимоги про помилку), найпоширеніше (оскільки воно пишеться протягом 50 років). Найбільш читабельна - це питання думки.
andy256

@ andy256 плутати - це стан перед кавою для мене.
Реакційний

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 нарешті розумна відповідь. Це саме те, що я зроблю. Спасибі.
Реакційний

1
"спробувати перенести цей механізм на власний метод" - це схоже на те, що метод рефакторингу Extract Method чи не так? Msgstr "Перетворіть фрагмент у метод, ім'я якого пояснює призначення методу."
гнат

2
@gnat: Те, що я пропоную, є дещо більше, ніж "метод вилучення", який я вважаю просто переміщенням одного коду в інше місце. Методи вилучення, безумовно, є хорошим кроком для того, щоб зробити код читати більше як його семантику. Я пропоную витяг методу продумати продумано, з огляду на збереження відокремленої політики та механізмів.
Ерік Ліпперт

1
@gnat: Саме так! У цьому випадку деталь, яку я хочу витягти, - це механізм, за допомогою якого всі записи читаються з файлу, зберігаючи політику. Політика полягає в тому, що "ми повинні робити певну роботу над кожним записом".
Ерік Ліпперт

1
Я бачу. Таким чином, простіше читати та підтримувати. Вивчаючи цей код, я можу окремо зосередитися на політиці та механізмі, він не змушує мене розділити увагу
gnat

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 лише з істинним або хибним, тому розмір може бути невідомим. Тоді ваша логіка закінчення записується один раз - у перелік - і вам не доведеться повторюватись більш ніж в одному місці.

MSDN надає приклад IEnumerator <T> . Вам також потрібно створити IEnumerable <T>, щоб повернути IEnumerator <T>.


Чи можете ви навести приклад коду.
Реакційний

дякую, це я вирішив зробити. Хоча я не думаю, що створення нових класів щоразу, коли у вас є нескінченний цикл, вирішує питання.
Реакційний

Так, я знаю, що ти маєш на увазі - багато накладних витрат. Справжній геній / проблема ітераторів полягає в тому, що вони, безумовно, створені так, щоб мати змогу одночасно повторювати дві речі над колекцією. Так багато разів вам не потрібна ця функціональність, щоб ви могли просто обернути навколо об'єктів реалізацію як IEnumerable, так і IEnumerator. Інший спосіб поглянути на те, що те, що лежить в основі колекції речей, які ви повторюєте, не було розроблено з урахуванням прийнятої парадигми C # на увазі. І це нормально. Додатковим бонусом ітераторів є те, що ви можете отримати всі паралельні речі LINQ безкоштовно!
J Trana

2

Коли у мене є операції ініціалізації, умови та збільшення, я люблю використовувати для циклів таких мов, як 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!
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.