【问题标题】:Can the C# compiler use duck typing with foreach over a generic type?C# 编译器可以在泛型类型上使用带有 foreach 的鸭子类型吗?
【发布时间】:2023-03-11 11:35:01
【问题描述】:

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

即使类型是泛型的,但被限制为实现 IEnumerable,是否也是这种情况?

为了更具体,选项 B 的运行开销是否比 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;
}

乙:

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

【问题讨论】:

    标签: c# generics duck-typing


    【解决方案1】:

    不,基本上。 “B”的唯一用途是TList 本身实际上是struct;然后,IL 可以使用“约束调用”来调用原始 GetEnumerator()而无需任何部分都必须将原始 struct TList 值装箱。

    但是:一旦您调用了GetEnumerator(),您就会回到IEnumerator&lt;T&gt; 领域,并且它将使用自定义迭代器。

    在这种情况下,所有这些都没有实际意义,因为迭代器块相当“分配”。所以...如果避免装箱 TList 是您关心的问题,那么您可能对分配很着迷:在这种情况下,您也不会以这种方式编写迭代器块。

    【讨论】:

      【解决方案2】:

      正如另一个答案所示,在TList 版本中调用的方法将始终为IEnumerable&lt;T&gt;.GetEnumerator,即使它隐藏在TList 中并且另一个GetEnumerator 是可见的。 因此,即使TList 恰好是List&lt;T&gt;,版本B 也无法利用List&lt;T&gt;.Enumerator GetEnumerator(),并且枚举器结构将被装箱在对IEnumerator&lt;T&gt; IEnumerable&lt;T&gt;.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&lt;T, IEnumerator&lt;T&gt;&gt; 将是IEnumerable&lt;T&gt; 的新自我,就像IEnumerable&lt;object&gt;IEnumerable 的新自我一样。 在这个想象的升级中,List&lt;T&gt; 应该实现IEnumerable&lt;T, List&lt;T&gt;.Enumerator&gt;

      编译器将扩​​展foreach 以使用TOuterEnumeratorTInnerEnumerator 作为枚举数的静态类型,因此如果它们恰好是结构,则不会发生装箱。

      请注意,编译器将始终选择IEnumerator&lt;...&gt;.MoveNextIEnumerator&lt;...&gt;.Current,即使枚举器类型将它们隐藏并具有另一个可见版本。这与非泛型方法不同,后者将选择可见版本,无论是IEnumerator&lt;...&gt; 还是特定类型。

      这不会对任何理智的枚举器造成正确性问题(事实上,我不知道有任何枚举器显式地实现了IEnumerator&lt;...&gt;)。 这也不应该导致性能问题,因为编译器将使用枚举器的静态类型的知识来限制调用。 因此,如果枚举数是sealed class 或结构,则接口(虚拟)调用消失并由直接实例调用代替。


      无耻的自我宣传:我有a blog entry on this

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-11-21
        • 2011-03-23
        • 1970-01-01
        相关资源
        最近更新 更多