I am studying the internal mechanics of the iterator methods, and I noticed a strange difference in behavior between the IEnumerator<T>
obtained by an iterator and the IEnumerator<T>
obtained by a LINQ method. If an exception happens during the enumeration, then:
- The LINQ enumerator remains active. It skips an item but continues producing more.
- The iterator enumerator becomes finished. It does not produce any more items.
Example. An IEnumerator<int>
is enumerated stubbornly until it completes:
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}");
}
}
}
}
Let's try enumerating a LINQ enumerator that throws on every 3rd item:
var linqEnumerable = Enumerable.Range(1, 10).Select(i =>
{
if (i % 3 == 0) throw new Exception("Oops!");
return i;
});
StubbornEnumeration(linqEnumerable.GetEnumerator());
Output:
1
2
Exception: Oops!
4
5
Exception: Oops!
7
8
Exception: Oops!
10
Finished
Now let's try the same with an iterator that throws on every 3rd item:
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;
}
}
Output:
1
2
Exception: Oops!
Finished
My question is: what is the reason for this inconsistency? And which behavior is more useful for practical applications?
Note: This observation was made following an answer by Dennis1679 in another iterator-related question.
Update: I made some more observations. Not all LINQ methods behave the same. For example the Take
method is implemented internally as a TakeIterator
on .NET Framework, so it behaves like an iterator (on exception completes immediately). But on .NET Core it's probably implemented differently because on exception it keeps going.
The yield
syntax in the second example makes the difference. When you use it, the compiler generates a state machine that manages a real enumerator under the hood. Throwing the exception exits the function and therefore terminates the state machine.
来源:https://stackoverflow.com/questions/58257033/why-do-iterators-behave-differently-on-exception-than-linq-enumerables