为什么 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
)。
存储在委托中的匿名方法可以由任何代码调用,甚至是用不同语言编写的代码,这些代码在 C# 7 出现之前几年编译,并且编译器生成的 CIL 需要对所有可能的用途都有效。这意味着在您的情况下,在 CIL 级别,该方法不得采用任何参数。
本地方法只能由同一个 C# 项目调用(更具体地说,来自包含方法),因此还将处理编译该方法的同一编译器以编译对它的所有调用。因此,不存在匿名方法的兼容性问题。任何产生相同效果的 CIL 都可以在这里工作,因此选择最有效的方法都是有意义的。在这种情况下,编译器重写以启用值类型而不是引用类型可防止不必要的分配。
匿名方法(和 lambda 表达式)的主要用途是能够将它们传递给消费方法以指定过滤器、谓词或方法所需的任何内容。它们不是特别适合从定义它们的同一方法调用,并且只有在稍后使用System.Action委托时才考虑这种能力。
另一方面,局部方法恰恰相反 - 它们的主要目的是从相同的方法调用,例如使用局部变量。
匿名方法可以从原始方法中调用,但它们是在 C# 2 中实现的,并且未考虑此特定用法。
因此,本地方法可以传递给其他方法,但它们的实现细节的设计方式更适合它们的目的。毕竟,您观察到的差异是简单的优化。他们本可以以这种方式优化匿名方法,但他们没有,现在添加这种优化可能会破坏现有的程序(尽管我们都知道依赖实现细节是一个坏主意)。
因此,让我们看看优化在哪里。最重要的变化是结构而不是类。匿名方法需要一种方法来访问外部局部变量,即使在原始方法返回后也是如此。这称为闭包,DisplayClass
是实现它的东西。C 函数指针和 C# 委托之间的主要区别在于,委托也可以选择携带目标对象,只需用作this
(内部的第一个参数)。该方法绑定到目标对象,每次调用委托时,对象都会传递给该方法(内部作为第一个参数,绑定实际上甚至适用于静态方法)。
但是,目标对象是...一个object
.可以将方法绑定到值类型,但在此之前需要对其进行装箱。现在您可以了解为什么在匿名方法的情况下DisplayClass
需要是引用类型,因为值类型将是一种负担,而不是优化,需要额外的装箱。
使用本地方法消除了将方法绑定到对象的需要,也无需考虑将方法传递给外部代码。我们可以纯粹在堆栈上分配DisplayClass
(因为它应该用于本地数据),或者通常与原始局部变量位于同一位置,不会给 GC 带来负担。现在开发人员有两个选择——要么将LocalFunc
作为实例方法并将其移动到DisplayClass
,要么将其设为静态并将DisplayClass
作为其第一个(ref
)参数。调用该方法的这两个选项之间没有区别,所以我认为选择只是任意的。他们本可以做出其他决定,没有任何区别。
但是,请注意,一旦此优化可能变成性能问题,就会以多快的速度下降。对代码的简单添加(如Action a = DoIt;
)会立即更改LocalFunc
方法。结果立即恢复到匿名方法的情况,因为DisplayClass
需要装箱等。