【问题标题】:Why is the compiler-generated enumerator for "yield" not a struct?为什么编译器生成的“yield”枚举器不是结构?
【发布时间】:2023-04-01 21:49:01
【问题描述】:

IEnumerator / IEnumerablecompiler-generated implementation 用于 yield 方法和 getter 似乎是一个类,因此分配在堆上。但是,其他 .NET 类型(例如 List<T>)专门返回 struct 枚举器以避免无用的内存分配。通过对C# In Depth 帖子的快速概述,我认为这里没有理由不这样做。

我错过了什么吗?

【问题讨论】:

  • 我刚刚意识到,由于返回类型是一个接口(@98​​7654330@ 或IEnumerator),所以无论如何它是would get boxed,对吗?在这种情况下,不能将方法更改为返回显式类型的枚举器(如List<T> does)吗?由于它实现了接口,因此应保留对它的所有代码引用。 (IIRC,这是因为foreachdetected by pattern)。

标签: c# struct heap-memory ienumerable yield


【解决方案1】:

Servy 正确回答了您的问题——您在评论中回答了自己的问题:

我刚刚意识到,由于返回类型是一个接口,所以无论如何它都会被装箱,对吗?

没错。您的后续问题是:

不能将方法更改为返回显式类型的枚举器(如 List&lt;T&gt; 那样)吗?

所以你的想法是用户写:

IEnumerable<int> Blah() ...

并且编译器实际上生成了一个返回BlahEnumerable的方法,它是一个实现IEnumerable&lt;int&gt;的结构,但具有适当的GetEnumerator等方法和属性,允许foreach的“模式匹配”功能省略拳击。

虽然这是一个合理的想法,但当您开始对方法的返回类型撒谎时,就会遇到严重的困难。 特别是当谎言涉及改变方法是返回结构还是引用类型时。想想所有出错的事情:

  • 假设方法是虚拟的。它怎么能被覆盖?虚拟覆盖方法的返回类型必须与被覆盖的方法完全匹配。 (同样对于:方法覆盖另一个方法,方法实现接口的方法,等等。)

  • 假设方法被制作成委托Func&lt;IEnumerable&lt;int&gt;&gt;Func&lt;T&gt;T 中是协变的,但协变仅适用于引用类型的类型参数。该代码看起来返回一个IEnumerable&lt;T&gt;,但实际上它返回的值类型与IEnumerable&lt;T&gt; 不协方差兼容,仅赋值兼容

  • 假设我们有void M&lt;T&gt;(T t) where T : class,我们调用M(Blah())。我们期望推导出TIEnumerable&lt;int&gt;,它通过了约束检查,但是结构类型没有通过约束检查。

等等。你很快就出现在 Three's Company 的一集中(男孩我在这里约会自己),一个小小的谎言最终变成了一场巨大的灾难。这一切都是为了节省少量的采集压力。不值得。

我注意到编译器创建的实现确实以一种有趣的方式节省了收集压力。在返回的枚举上调用GetEnumerator第一次 次,枚举将自身 变成一个枚举数。第二次当然状态不同,所以它分配了一个新对象。由于 99.99% 的可能情况是给定序列仅枚举一次,因此可以大大节省收集压力。

【讨论】:

  • 感谢您的详尽回答!我在收集压力上吹毛求疵,因为我正在使用 Unity 糟糕的早期 Mono 运行时非分代 GC,所以我正在密切关注分配非常
【解决方案2】:

该类将通过接口使用。如果它是一个结构,它将 100% 的时间被装箱,这使得它比使用类效率低

您不能将其装箱,因为根据定义,在编译时不可能使用该类型,因为当您开始编译时它不存在代码。

在编写IEnumerator 的自定义实现时,您可以在编译代码之前公开实际的底层类型,这样就可以在不装箱的情况下使用它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-02-19
    • 1970-01-01
    • 2017-07-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多