Why does Enumerable.Empty() return an empty array?

1.6k views Asked by At

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.

2

There are 2 answers

10
Dirk On BEST ANSWER

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         
6
Moti Azu On

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.