为什么局部函数生成的 IL 不同于匿名方法和 Lambda 表达式?

Why Local Functions generate IL different from Anonymous Methods and Lambda Expressions?

为什么 C# 7 编译器将局部函数转换为父函数所在的相同 class 中的方法。而对于匿名方法(和 Lambda 表达式),编译器会为每个父函数生成一个嵌套的 class,它将包含其所有匿名方法作为实例方法 ?

例如,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 函数已在与其父函数相同的 class 中变为静态函数。 此外,封闭变量 x 已变成嵌套 struct 中的字段(不像匿名方法示例中那样嵌套 class)。

存储在委托中的匿名方法可以被任何代码调用,甚至是用不同语言编写的代码,在 C# 7 出现之前编译多年,编译器生成的 CIL 需要对所有可能的用途有效。这意味着在您的情况下,在 CIL 级别,方法 必须 不带参数。

本地方法只能由同一个 C# 项目调用(从包含方法,更具体地说),因此编译该方法的同一个编译器也将被处理以编译对它的所有调用。因此不存在关于匿名方法的兼容性问题。任何产生相同效果的 CIL 都可以在这里工作,因此选择最有效的方法是有意义的。在这种情况下,编译器重新编写以启用值类型而不是引用类型可以防止不必要的分配。

匿名方法(和 lambda 表达式)的主要用途是能够将它们传递给使用方法以指定过滤器、谓词或方法需要的任何内容。它们并不特别适合从定义它们的同一方法中调用,并且这种能力只是在以后才被考虑,使用 System.Action 委托。

另一方面,局部方法恰恰相反 - 它们的主要目的是从同一个方法中调用,就像使用局部变量一样。

可以从原始方法中调用匿名方法,但它们是在 C# 2 中实现的,因此未考虑这种特定用法。

因此可以将本地方法传递给其他方法,但它们的实现细节的设计方式更适合它们的目的。毕竟,您观察到的差异是一个简单的优化。他们本来可以在当天优化匿名方法,但他们没有,现在添加这样的优化可能会破坏现有程序(尽管我们都知道依赖实现细节是一个坏主意)。

那么让我们看看优化在哪里。最重要的变化是结构而不是 class。好吧,即使在原始方法 returns 之后,匿名方法也需要一种访问外部局部变量的方法。这称为闭包,"DisplayClass" 是它的实现。 C 函数指针和 C# 委托之间的主要区别在于,委托还可以选择性地携带一个目标对象,简单地用作 this(内部第一个参数)。该方法绑定到目标对象,每次调用委托时都会将对象传递给方法(在内部作为第一个参数,绑定实际上适用于静态方法)。

然而,目标对象是……好吧,object。您可以将方法绑定到值类型,但需要在此之前对其进行装箱。现在您可以明白为什么在匿名方法的情况下 DisplayClass 需要是引用类型,因为值类型将是一种负担,而不是优化。

使用本地方法无需将方法绑定到对象,也无需考虑将方法传递给外部代码。我们可以完全在堆栈上分配 DisplayClass(对于本地数据来说应该如此),不会给 GC 带来任何负担。现在开发人员有两个选择——创建 LocalFunc 实例并将其移至 DisplayClass,或者将其设为静态并将 DisplayClass 作为其第一个 (ref) 参数。调用方法没有区别,所以我认为选择是随意的。他们本可以做出其他决定,没有任何区别。

但是,请注意这种优化一旦可能变成性能问题,就会以多快的速度被放弃。对您的代码进行简单的添加,例如 Action a = DoIt; 会立即破坏 LocalFunc 方法。然后该实现立即恢复为匿名方法之一,因为 DisplayClass 需要装箱等。