C#编译器是否可以在普通类型上使用带有foreach的鸭子类型?

时间:2019-03-06 16:08:01

标签: c# generics duck-typing

已经确定,编译器可以在迭代List或Array(请参阅Duck typing in the C# compiler)时执行鸭子类型处理,以消除一些开销,因为这些类型将其IEnumerator实现为堆栈分配的结构。

即使类型是通用类型,但必须限制实现IEnumerable时也是如此吗?

要提供更多的特异性,选项B的运行开销是否可以比A少?

A:

public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> collection)
{
    foreach (var subCollection in collection)
        foreach (var element in subCollection)
            yield return element;
}

B:

public static IEnumerable<T> Flatten<TList, T>(this TList collection)
    where TList : IEnumerable<IEnumerable<T>>
{
    foreach (var subCollection in collection)
        foreach (var element in subCollection)
            yield return element;
}

2 个答案:

答案 0 :(得分:6)

不,基本上。 “ B”的唯一用法是TList 本身实际上是struct时;然后,IL可以使用“受约束的呼叫”来呼叫原始的GetEnumerator() 而无需将原始struct TList值放入框内的任何部分。

但是:调用GetEnumerator()之后,您将回到IEnumerator<T>领域,并且不会使用自定义迭代器。

在这种情况下,所有这些都几乎没有意义,因为迭代器块也是 相当“可分配的”。因此...如果您担心避免TList装箱,那么您大概会迷恋分配:在这种情况下,您也不会以这种方式编写迭代器块。

答案 1 :(得分:1)

正如另一个答案所表明的那样,在TList版本中被调用的方法将始终为IEnumerable<T>.GetEnumerator,即使该方法被隐藏在TList和另一个GetEnumerator中是可见的。 因此,即使TList恰好是List<T>,版本B也无法利用List<T>.Enumerator GetEnumerator(),并且枚举器结构将被装在对IEnumerator<T> IEnumerable<T>.GetEnumerator()的调用中。

我们可以通过向后兼容的方式升级IEnumerable,如下所示:

interface IEnumerable<out T, out TEnumerator> : IEnumerable<T>
  where TEnumerator : IEnumerator<T>
{
  new TEnumerator GetEnumerator();
}

// In an imagined upgrade, the compiler should transform the iterator block
// to return IEnumerable<T, IEnumerator<T>>, allowing this to chain.
static IEnumerable<T> Flatten<T, TOuterEnumerator, TInnerEnumerator>
  (this IEnumerable<IEnumerable<T, TInnerEnumerator>, TOuterEnumerator> collection)
  // C# compiler needs to be reminded of these constraints,
  // or foreach will not compile.
  where TOuterEnumerator : IEnumerator<IEnumerable<T, TInnerEnumerator>>
  where TInnerEnumerator : IEnumerator<T>
{
  foreach (var subcoll in collection)
    foreach (var elem in subcoll)
      yield return elem;
}

IEnumerable<T, IEnumerator<T>>将是IEnumerable<T>的新自我,就像IEnumerable<object>IEnumerable的新自我一样。 在此想象的升级中,List<T>应该实现IEnumerable<T, List<T>.Enumerator>

编译器将展开foreach以将TOuterEnumeratorTInnerEnumerator用作枚举器的静态类型,因此,如果碰巧是结构,则不会发生装箱。

请注意,即使枚举器类型将其隐藏并具有另一个可见版本,编译器仍将始终选择IEnumerator<...>.MoveNextIEnumerator<...>.Current。这与非通用方法不同,后者会选择可见的版本,无论是IEnumerator<...>还是特定的类型。

这不会引起任何理智的枚举器的正确性问题(实际上,我不知道任何枚举器显式实现IEnumerator<...>)。 这也不应该引起性能问题,因为编译器将使用枚举器的静态类型的知识来限制调用。 因此,如果枚举数是sealed class或结构,则接口(虚拟)调用将消失,并被直接实例调用替换。


无耻的自我广告:我有a blog entry on this