M
Marcel Müller
The code below causes an infinite loop in method Foo. I think the
problem is that EnumeratorHelper.Next is invoked on a temporary rather
that on the instance en. This causes any changes to be lost and
therefore the loop will not terminate. Depending on the kind of data
source this will either cause an infinite loop or an
InvalidOperationException.
..method private hidebysig newslot virtual final instance bool MoveNext()
cil managed
{
.override [mscorlib]System.Collections.IEnumerator::MoveNext
.maxstack 4
.locals init (
[0] bool flag,
[1] int32 num,
[2] bool flag2,
[3] valuetype Test/EnumeratorHelper<!T, !K> helper)
....
L_009e: ldarg.0
L_009f: ldfld valuetype Test/EnumeratorHelper<!0, !1>
Test/<Foo>d__0<!T, !K, !R>::<en>5__1
L_00a4: stloc.3
L_00a5: ldloca.s helper
L_00a7: call instance void Test/EnumeratorHelper<!T, !K>::Next()
....
I know the code makes not too much sense in /this/ small example. My
real use case is the synchronized enumeration of several ordered data
sources of different types and a common key (something like a join). It
make the code really unreadable if I need to expand the number of
variables and the code in EnumeratorHelper inline.
using System;
using System.Collections.Generic;
public class Test
{
public static IEnumerable<R> Foo<T,K,R>(IEnumerable<T> source,
Func<T,K> selector, Func<T,R> result)
{ using (var en = new EnumeratorHelper<T,K>(source, selector))
{ while (true)
{ // Do some logic with en and en.Key
if (!en.Valid)
yield break;
yield return result(en.En.Current);
en.Next();
}
}
}
public static void Bar<T,K>(IEnumerable<T> source, Func<T,K> selector,
Action<T> action)
{ using (var en = new EnumeratorHelper<T,K>(source, selector))
{ while (true)
{ // Do some logic with en and en.Key
if (!en.Valid)
return;
action(en.En.Current);
en.Next(); // Operates on a temporary copy!!!
}
}
}
private struct EnumeratorHelper<T,K> : IDisposable
{ private readonly Func<T,K> Selector;
public readonly IEnumerator<T> En;
public bool Valid;
public K Key;
public EnumeratorHelper(IEnumerable<T> list, Func<T,K> selector)
{ Selector = selector;
En = list.GetEnumerator();
Key = (Valid = En.MoveNext()) ? Selector(En.Current) : default(K);
}
public void Next()
{ Key = (Valid = En.MoveNext()) ? Selector(En.Current) : default(K);
}
public void Dispose()
{ if (En != null)
En.Dispose();
}
}
static void Main()
{
var data = new int[] { 1,3,6,7,8 };
// OK
Bar(data, x => x, x => Console.WriteLine(x));
// Fails...
foreach(var item in Foo(data, x => x, x => x))
Console.WriteLine(item);
}
}
problem is that EnumeratorHelper.Next is invoked on a temporary rather
that on the instance en. This causes any changes to be lost and
therefore the loop will not terminate. Depending on the kind of data
source this will either cause an infinite loop or an
InvalidOperationException.
..method private hidebysig newslot virtual final instance bool MoveNext()
cil managed
{
.override [mscorlib]System.Collections.IEnumerator::MoveNext
.maxstack 4
.locals init (
[0] bool flag,
[1] int32 num,
[2] bool flag2,
[3] valuetype Test/EnumeratorHelper<!T, !K> helper)
....
L_009e: ldarg.0
L_009f: ldfld valuetype Test/EnumeratorHelper<!0, !1>
Test/<Foo>d__0<!T, !K, !R>::<en>5__1
L_00a4: stloc.3
L_00a5: ldloca.s helper
L_00a7: call instance void Test/EnumeratorHelper<!T, !K>::Next()
....
I know the code makes not too much sense in /this/ small example. My
real use case is the synchronized enumeration of several ordered data
sources of different types and a common key (something like a join). It
make the code really unreadable if I need to expand the number of
variables and the code in EnumeratorHelper inline.
using System;
using System.Collections.Generic;
public class Test
{
public static IEnumerable<R> Foo<T,K,R>(IEnumerable<T> source,
Func<T,K> selector, Func<T,R> result)
{ using (var en = new EnumeratorHelper<T,K>(source, selector))
{ while (true)
{ // Do some logic with en and en.Key
if (!en.Valid)
yield break;
yield return result(en.En.Current);
en.Next();
}
}
}
public static void Bar<T,K>(IEnumerable<T> source, Func<T,K> selector,
Action<T> action)
{ using (var en = new EnumeratorHelper<T,K>(source, selector))
{ while (true)
{ // Do some logic with en and en.Key
if (!en.Valid)
return;
action(en.En.Current);
en.Next(); // Operates on a temporary copy!!!
}
}
}
private struct EnumeratorHelper<T,K> : IDisposable
{ private readonly Func<T,K> Selector;
public readonly IEnumerator<T> En;
public bool Valid;
public K Key;
public EnumeratorHelper(IEnumerable<T> list, Func<T,K> selector)
{ Selector = selector;
En = list.GetEnumerator();
Key = (Valid = En.MoveNext()) ? Selector(En.Current) : default(K);
}
public void Next()
{ Key = (Valid = En.MoveNext()) ? Selector(En.Current) : default(K);
}
public void Dispose()
{ if (En != null)
En.Dispose();
}
}
static void Main()
{
var data = new int[] { 1,3,6,7,8 };
// OK
Bar(data, x => x, x => Console.WriteLine(x));
// Fails...
foreach(var item in Foo(data, x => x, x => x))
Console.WriteLine(item);
}
}