C# 编译器可以在泛型类型上使用带有 foreach 的鸭子类型吗?
Can the C# compiler use duck typing with foreach over a generic type?
已经确定编译器可以执行鸭子类型以在遍历列表或数组时消除一些开销(请参阅 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;
}
基本上没有。 "B" 的唯一用途是当 TList
本身 实际上是 struct
时;然后 IL 可以使用 "constrained call" 调用原始 GetEnumerator()
而无需 任何部分必须装箱原始 struct TList
值。
但是:调用 GetEnumerator()
后,您又回到了 IEnumerator<T>
领域,它将 不 使用自定义迭代器。
在这种情况下,所有这些基本上都没有实际意义,因为迭代器块也相当"allocatey"。所以...如果避免装箱 TList
是您的顾虑,那么您可能对分配很着迷:在这种情况下,您也不会以这种方式编写迭代器块。
正如其他答案所指出的,在 TList
版本中调用的方法将始终是 IEnumerable<T>.GetEnumerator
,即使它隐藏在 TList
和另一个 [=14] 中=] 可见。
因此,即使 TList
恰好是 List<T>
,版本 B 也无法利用 List<T>.Enumerator GetEnumerator()
并且枚举数结构将在对 IEnumerator<T> IEnumerable<T>.GetEnumerator()
.[=36= 的调用中被装箱]
我们可以向后兼容方式升级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>>
will be the new self of IEnumerable<T>
, just like how IEnumerable<object>
is the new self of IEnumerable
.
In this imagined upgrade, List<T>
should implement IEnumerable<T, List<T>.Enumerator>
.
编译器将扩展 foreach
以使用 TOuterEnumerator
和 TInnerEnumerator
作为枚举器的静态类型,因此如果它们恰好是结构,则不会发生装箱。
请注意,编译器将始终选择 IEnumerator<...>.MoveNext
和 IEnumerator<...>.Current
,即使枚举器类型隐藏它们并有另一个可见版本。这与 non-generic 方法不同,后者将选择可见版本,无论是 IEnumerator<...>
还是特定类型。
这不会导致任何理智的枚举器出现正确性问题(事实上,我不知道任何显式实现 IEnumerator<...>
的枚举器)。
这也不应该导致性能问题,因为编译器将使用枚举器的静态类型知识来约束调用。
因此,如果枚举器是 sealed class
或结构,则接口(虚拟)调用将消失并被直接实例调用所取代。
不要脸self-advertising:我有a blog entry on this.
已经确定编译器可以执行鸭子类型以在遍历列表或数组时消除一些开销(请参阅 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;
}
基本上没有。 "B" 的唯一用途是当 TList
本身 实际上是 struct
时;然后 IL 可以使用 "constrained call" 调用原始 GetEnumerator()
而无需 任何部分必须装箱原始 struct TList
值。
但是:调用 GetEnumerator()
后,您又回到了 IEnumerator<T>
领域,它将 不 使用自定义迭代器。
在这种情况下,所有这些基本上都没有实际意义,因为迭代器块也相当"allocatey"。所以...如果避免装箱 TList
是您的顾虑,那么您可能对分配很着迷:在这种情况下,您也不会以这种方式编写迭代器块。
正如其他答案所指出的,在 TList
版本中调用的方法将始终是 IEnumerable<T>.GetEnumerator
,即使它隐藏在 TList
和另一个 [=14] 中=] 可见。
因此,即使 TList
恰好是 List<T>
,版本 B 也无法利用 List<T>.Enumerator GetEnumerator()
并且枚举数结构将在对 IEnumerator<T> IEnumerable<T>.GetEnumerator()
.[=36= 的调用中被装箱]
我们可以向后兼容方式升级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>>
will be the new self ofIEnumerable<T>
, just like howIEnumerable<object>
is the new self ofIEnumerable
. In this imagined upgrade,List<T>
should implementIEnumerable<T, List<T>.Enumerator>
.
编译器将扩展 foreach
以使用 TOuterEnumerator
和 TInnerEnumerator
作为枚举器的静态类型,因此如果它们恰好是结构,则不会发生装箱。
请注意,编译器将始终选择 IEnumerator<...>.MoveNext
和 IEnumerator<...>.Current
,即使枚举器类型隐藏它们并有另一个可见版本。这与 non-generic 方法不同,后者将选择可见版本,无论是 IEnumerator<...>
还是特定类型。
这不会导致任何理智的枚举器出现正确性问题(事实上,我不知道任何显式实现 IEnumerator<...>
的枚举器)。
这也不应该导致性能问题,因为编译器将使用枚举器的静态类型知识来约束调用。
因此,如果枚举器是 sealed class
或结构,则接口(虚拟)调用将消失并被直接实例调用所取代。
不要脸self-advertising:我有a blog entry on this.