为啥局部函数生成的 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 表达式?的主要内容,如果未能解决你的问题,请参考以下文章

JAVA学习之局部内部类,匿名内部类,静态内部类

1.匿名函数

匿名函数

为啥 Flux.zip 接受预定义函数而不接受匿名函数?

Python函数-2 匿名函数

函数--内置函数匿名函数