Я изучаю внутреннюю механику методов итератор и заметил странное различие в поведении между IEnumerator<T>
, полученным итератором, и IEnumerator<T>
, полученным с помощьюметод LINQ. Если во время перечисления возникает исключение, то:
- Перечислитель LINQ остается активным. Он пропускает элемент, но продолжает производить больше.
- Перечислитель итератора завершает работу. Он не производит больше предметов.
Пример. 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 он, вероятно, реализован иначе, потому что в исключительных случаях он продолжает работать.