I expected the implementation of Enumerable.Empty() to be just this:
public static IEnumerable<TResult> Empty<TResult>()
{
yield break;
}
But the implementation is something like this:
public static IEnumerable<TResult> Empty<TResult>()
{
return EmptyEnumerable<TResult>.Instance;
}
internal class EmptyEnumerable<TElement>
{
private static volatile TElement[] instance;
public static IEnumerable<TElement> Instance
{
get
{
if (EmptyEnumerable<TElement>.instance == null)
EmptyEnumerable<TElement>.instance = new TElement[0];
return (IEnumerable<TElement>)EmptyEnumerable<TElement>.instance;
}
}
}
Why does the implementation is more complex than just one line of code? Is there an advantage to return a cached array and not (yield) return no elements?
Note: I will never rely on the implementation details of a method, but I am just curious.
Compiling (using LINQpad with optimizations enabled)
public static IEnumerable<TResult> MyEmpty<TResult>()
{
yield break;
}
results in quite a lot of code.
It will create a state machine that implements the IEnumerable
interface. Every time you call MyEmpty
it will create a new instance of that class. Returning the same instance of an empty array is quite cheap.
The IL code for EmptyEnumerable
is:
EmptyEnumerable`1.get_Instance:
IL_0000: volatile.
IL_0002: ldsfld 16 00 00 0A
IL_0007: brtrue.s IL_0016
IL_0009: ldc.i4.0
IL_000A: newarr 04 00 00 1B
IL_000F: volatile.
IL_0011: stsfld 16 00 00 0A
IL_0016: volatile.
IL_0018: ldsfld 16 00 00 0A
IL_001D: castclass 01 00 00 1B
IL_0022: ret
And for the MyEmpty
method it is:
MyEmpty:
IL_0000: ldc.i4.s FE
IL_0002: newobj 15 00 00 0A
IL_0007: stloc.0
IL_0008: ldloc.0
IL_0009: ret
<MyEmpty>d__0`1.System.Collections.Generic.IEnumerable<TResult>.GetEnumerator:
IL_0000: call System.Environment.get_CurrentManagedThreadId
IL_0005: ldarg.0
IL_0006: ldfld 0E 00 00 0A
IL_000B: bne.un.s IL_0022
IL_000D: ldarg.0
IL_000E: ldfld 0F 00 00 0A
IL_0013: ldc.i4.s FE
IL_0015: bne.un.s IL_0022
IL_0017: ldarg.0
IL_0018: ldc.i4.0
IL_0019: stfld 0F 00 00 0A
IL_001E: ldarg.0
IL_001F: stloc.0
IL_0020: br.s IL_0029
IL_0022: ldc.i4.0
IL_0023: newobj 10 00 00 0A
IL_0028: stloc.0
IL_0029: ldloc.0
IL_002A: ret
<MyEmpty>d__0`1.System.Collections.IEnumerable.GetEnumerator:
IL_0000: ldarg.0
IL_0001: call 11 00 00 0A
IL_0006: ret
<MyEmpty>d__0`1.MoveNext:
IL_0000: ldarg.0
IL_0001: ldfld 0F 00 00 0A
IL_0006: stloc.0 // CS$0$0000
IL_0007: ldloc.0 // CS$0$0000
IL_0008: ldc.i4.0
IL_0009: bne.un.s IL_0012
IL_000B: ldarg.0
IL_000C: ldc.i4.m1
IL_000D: stfld 0F 00 00 0A
IL_0012: ldc.i4.0
IL_0013: ret
<MyEmpty>d__0`1.System.Collections.Generic.IEnumerator<TResult>.get_Current:
IL_0000: ldarg.0
IL_0001: ldfld 12 00 00 0A
IL_0006: ret
<MyEmpty>d__0`1.System.Collections.IEnumerator.Reset:
IL_0000: newobj System.NotSupportedException..ctor
IL_0005: throw
<MyEmpty>d__0`1.System.IDisposable.Dispose:
IL_0000: ret
<MyEmpty>d__0`1.System.Collections.IEnumerator.get_Current:
IL_0000: ldarg.0
IL_0001: ldfld 12 00 00 0A
IL_0006: box 04 00 00 1B
IL_000B: ret
<MyEmpty>d__0`1..ctor:
IL_0000: ldarg.0
IL_0001: call System.Object..ctor
IL_0006: ldarg.0
IL_0007: ldarg.1
IL_0008: stfld 0F 00 00 0A
IL_000D: ldarg.0
IL_000E: call System.Environment.get_CurrentManagedThreadId
IL_0013: stfld 0E 00 00 0A
IL_0018: ret
It makes sense to do that because in that case you would one array for all empty instances of the same type, which will require less memory. That is why the single array instance is static.
Since an array with no elements can't be changed, it can't get dirty by any code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With