【问题标题】:Why does Enumerable.Empty() return an empty array?为什么 Enumerable.Empty() 返回一个空数组?
【发布时间】:2014-11-12 13:06:10
【问题描述】:

我希望 Enumerable.Empty() 的实现是这样的:

public static IEnumerable<TResult> Empty<TResult>()
{
    yield break;
}

但实现是这样的:

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;
        }
    }
}

为什么实现比一行代码更复杂?返回缓存数组而不(yield)不返回任何元素是否有优势?

注意:我永远不会依赖方法的实现细节,但我只是好奇。

【问题讨论】:

  • yield break 是 C# 中的一行代码,但如果您查看生成的 IL 代码,它会复杂得多。

标签: c# linq linq-to-objects


【解决方案1】:

编译(使用启用优化的 LINQpad)

public static IEnumerable<TResult> MyEmpty<TResult>()
{
    yield break;
}

导致相当多的代码。

它将创建一个实现IEnumerable 接口的状态机。每次调用MyEmpty 时,它都会创建该类的一个新实例。返回空数组的相同实例非常便宜。

EmptyEnumerable 的 IL 代码是:

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

对于MyEmpty 方法,它是:

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         

【讨论】:

  • 如果您还添加了缓存所需的 IL 代码,这将如何比较?
  • @Dirk 关于这一点的重要说明 - 尽管它产生更多的 IL,但这并不是它的性能不如空数组的原因。主要的性能差异是创建的对象数量。这与必须生成多少代码无关。
  • @mot 每次调用 MyEmpty 都会创建该类的新实例。
  • @Dirk True,但我没有另外说明。 yield 产生大量代码的事实并不是它表现不佳的原因,而是该流程中实例化的对象数量。
  • @YuvalItzchakov 是的,那完全是我的误解。我仍然坚持 IL 的数量对这里的性能没有任何意义,我怀疑这是选择静态空数组的原因。内存分配考虑更有意义。
【解决方案2】:

这样做是有意义的,因为在这种情况下,您将为所有相同类型的空实例创建一个数组,这将需要更少的内存。这就是为什么单个数组实例是静态的。

由于没有元素的数组无法更改,因此它不会被任何代码弄脏。

【讨论】:

  • 数组是不可变的?从什么时候开始?
  • 特定类型的所有空实例的特定数组。
  • 具体来说,由于它是一个 empty 数组,因此您不必担心它会变脏,因为没有要变异的元素。数组不是不可变的,但它们的长度是不可变的,因此零元素数组实际上是不可变的。
  • @SriramSakthivel 抱歉,我的意思是空数组,已编辑。
  • @AlexSiepman 因为它会为每次调用创建新实例,所以确实如此。
猜你喜欢
  • 2012-08-26
  • 2021-11-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-25
  • 2022-01-09
  • 1970-01-01
相关资源
最近更新 更多