Почему итераторы ведут себя иначе, чем перечисляемые в LINQ? - PullRequest
2 голосов
/ 06 октября 2019

Я изучаю внутреннюю механику методов итератор и заметил странное различие в поведении между IEnumerator<T>, полученным итератором, и IEnumerator<T>, полученным с помощьюметод LINQ. Если во время перечисления возникает исключение, то:

  1. Перечислитель LINQ остается активным. Он пропускает элемент, но продолжает производить больше.
  2. Перечислитель итератора завершает работу. Он не производит больше предметов.

Пример. IEnumerator<int> перечисляется упорно, пока не завершится:

private static void StubbornEnumeration(IEnumerator<int> enumerator)
{
    using (enumerator)
    {
        while (true)
        {
            try
            {
                while (enumerator.MoveNext())
                {
                    Console.WriteLine(enumerator.Current);
                }
                Console.WriteLine("Finished");
                return;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Exception: {ex.Message}");
            }
        }
    }
}

Попытаемся перечислив LINQ нумератор, генерирующий на каждом 3-й пункт:

var linqEnumerable = Enumerable.Range(1, 10).Select(i =>
{
    if (i % 3 == 0) throw new Exception("Oops!");
    return i;
});
StubbornEnumeration(linqEnumerable.GetEnumerator());

Выход:

1023 *

1
2
Исключение: Упс!
4
5
Исключение: Упс!
7
8
Исключение: Упс!
10
Завершено

Теперь давайте попробуем то же самое с итератором, который выбрасывает каждый 3-й элемент:

StubbornEnumeration(MyIterator().GetEnumerator());

static IEnumerable<int> MyIterator()
{
    for (int i = 1; i <= 10; i++)
    {
        if (i % 3 == 0) throw new Exception("Oops!");
        yield return i;
    }
}

Вывод:

1
2
Исключение: Ой!
Закончено

Мой вопрос: в чем причина этого несоответствия? И какое поведение более полезно для практических приложений?

Примечание: Это наблюдение было сделано после ответа Дениса1679 в другом вопросе, связанном с итератором.


Обновление : я сделал еще несколько замечаний. Не все методы LINQ ведут себя одинаково. Например, метод Take реализован внутри как TakeIterator в .NET Framework, поэтому он ведет себя как итератор (исключение завершается немедленно). Но в .NET Core он, вероятно, реализован иначе, потому что в исключительных случаях он продолжает работать.

1 Ответ

1 голос
/ 06 октября 2019

Синтаксис yield во втором примере имеет значение. Когда вы используете его, компилятор генерирует конечный автомат, который управляет реальным перечислителем. Вызов исключения выходит из функции и, следовательно, завершает работу конечного автомата.

...