为啥局部函数生成的 IL 不同于匿名方法和 Lambda 表达式?
Posted
技术标签:
【中文标题】为啥局部函数生成的 IL 不同于匿名方法和 Lambda 表达式?【英文标题】:Why Local Functions generate IL different from Anonymous Methods and Lambda Expressions?为什么局部函数生成的 IL 不同于匿名方法和 Lambda 表达式? 【发布时间】:2018-01-02 10:15:21 【问题描述】:为什么 C# 7 编译器将本地函数转换为与其父函数所在的同一类中的方法。而对于匿名方法(和 Lambda 表达式),编译器会为每个父函数生成一个嵌套类,该类将包含其所有匿名方法作为实例方法?
例如C#代码(匿名方法):
internal class AnonymousMethod_Example
public void MyFunc(string[] args)
var x = 5;
Action act = delegate ()
Console.WriteLine(x);
;
act();
会产生IL代码(匿名方法)类似于:
.class private auto ansi beforefieldinit AnonymousMethod_Example
.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0'
.field public int32 x
.method assembly hidebysig instance void '<MyFunc>b__0' () cil managed
...
AnonymousMethod_Example/'<>c__DisplayClass0_0'::x
call void [mscorlib]System.Console::WriteLine(int32)
...
...
...
同时,C#代码(局部函数):
internal class LocalFunction_Example
public void MyFunc(string[] args)
var x = 5;
void DoIt()
Console.WriteLine(x);
;
DoIt();
会生成IL代码(局部函数)类似于:
.class private auto ansi beforefieldinit LocalFunction_Example
.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0' extends [mscorlib]System.ValueType
.field public int32 x
.method public hidebysig instance void MyFunc(string[] args) cil managed
...
ldc.i4.5
stfld int32 LocalFunction_Example/'<>c__DisplayClass1_0'::x
...
call void LocalFunction_Example::'<MyFunc>g__DoIt1_0'(valuetype LocalFunction_Example/'<>c__DisplayClass1_0'&)
.method assembly hidebysig static void '<MyFunc>g__DoIt0_0'(valuetype LocalFunction_Example/'<>c__DisplayClass0_0'& '') cil managed
...
LocalFunction_Example/'<>c__DisplayClass0_0'::x
call void [mscorlib]System.Console::WriteLine(int32)
...
请注意,DoIt
函数已变成与其父函数在同一类中的静态函数。
此外,封闭的变量 x
已变成嵌套 struct
中的字段(不像匿名方法示例中那样嵌套 class
)。
【问题讨论】:
尝试以实现本地方法的方式实现 lambda,看看会发生什么。 @Servy 你能告诉我怎么做吗? 你不能。这就是要点。 @Servy 这正是我要问的。为什么我(或编译器的人)不能像本地方法那样实现 lambda? 所以尝试使用与本地方法相同的方法来实现 lambda,看看为什么它不起作用。 【参考方案1】:任何代码都可以调用存储在委托中的匿名方法,甚至是用不同语言编写的代码,在 C# 7 出现之前编译多年,并且编译器生成的 CIL 需要对所有可能的用途都有效。这意味着在您的情况下,在 CIL 级别,方法必须不带参数。
本地方法只能由同一个 C# 项目调用(从包含方法,更具体地说),因此编译该方法的同一个编译器也将被处理以编译对它的所有调用。因此,不存在与匿名方法一样的兼容性问题。任何产生相同效果的 CIL 都可以在这里工作,因此选择最有效的方法是有意义的。在这种情况下,编译器重写以启用值类型而不是引用类型可以防止不必要的分配。
【讨论】:
即使委托可以被重写为接受参数的方法,它也行不通。这与与其他系统或早期代码的兼容性无关,而是在委托的情况下无法使用该方法解决问题。这两段代码做完全不同的事情。由于它们做的事情完全不同,因此它们的实现方式也不同,这是可以理解的。 @Servy 我并不反对,但我从不同的角度来看待它。如果编译器可以看到和修改委托的所有调用,则委托可以重写为接受不同类型参数的方法。但当然编译器无法查看和修改所有调用。 即使它可以修改所有调用,你仍然将无法解决问题,因为方法需要的值不会' t 存在在许多调用站点中传递,因此问题比这更深,更根本。 @Servy 即使在一般情况下,即使是值类型,这部分也是可能的:它可以通过绑定到值类型的盒装副本的静态方法来完成。当静态证明委托的生命周期没有超过方法时,编译器可以避免装箱。但不值得努力。 现在你说创建的闭包类型可能是一个值类型,是的,它可能是,尽管这样做不会有成效,正如你所提到的。这种方法与本地方法版本的做法仍然不同。【参考方案2】:匿名方法(和 lambda 表达式)的主要用途是能够将它们传递给消费方法以指定过滤器、谓词或任何方法所需的内容。它们并不特别适合从定义它们的同一方法中调用,并且该功能仅在以后才考虑使用 System.Action 委托。
另一方面,局部方法恰恰相反——它们的主要目的是从同一个方法中调用,就像使用局部变量一样。
可以从原始方法中调用匿名方法,但它们是在 C# 2 中实现的,并没有考虑到这种特定用法。
所以可以将本地方法传递给其他方法,但它们的实现细节的设计方式更适合它们的目的。毕竟,您观察到的差异是一个简单的优化。过去他们本可以优化匿名方法,但他们没有,现在添加这种优化可能会破坏现有程序(尽管我们都知道依赖实现细节是一个坏主意)。
那么让我们看看优化在哪里。最重要的变化是结构而不是类。好吧,即使在原始方法返回后,匿名方法也需要一种访问外部局部变量的方法。这称为闭包,“DisplayClass”就是实现它的对象。 C 函数指针和 C# 委托之间的主要区别在于,委托还可以选择携带目标对象,简单地用作 this
(内部的第一个参数)。方法绑定到目标对象,并且每次调用委托时都会将对象传递给方法(在内部作为第一个参数,并且绑定实际上对静态方法也有效)。
但是,目标对象是……嗯,object
。您可以将方法绑定到值类型,但在此之前需要对其进行装箱。现在您可以了解为什么在匿名方法的情况下 DisplayClass 需要是引用类型,因为值类型将是负担,而不是优化。
使用本地方法无需将方法绑定到对象,也无需考虑将方法传递给外部代码。我们可以纯粹在堆栈上分配 DisplayClass(因为它应该用于本地数据),不会给 GC 带来负担。现在开发人员有两个选择 - 要么创建 LocalFunc 实例并将其移动到 DisplayClass,要么将其设为静态并将 DisplayClass 作为其第一个 (ref
) 参数。调用方法没有区别,所以我认为选择只是任意的。他们本可以做出其他决定,没有任何区别。
但是,请注意,一旦这种优化变成性能问题,它就会以多快的速度被放弃。对您的代码进行简单的添加,例如 Action a = DoIt;
会立即破坏 LocalFunc 方法。然后实现立即恢复为匿名方法之一,因为 DisplayClass 需要装箱等。
【讨论】:
以上是关于为啥局部函数生成的 IL 不同于匿名方法和 Lambda 表达式?的主要内容,如果未能解决你的问题,请参考以下文章