为什么本地函数生成与匿名方法和Lambda表达式不同的IL?

时间:2017-07-26 21:45:45

标签: c# lambda cil anonymous-methods c#-7.0

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

例如, C#代码(匿名方法)

WITH tempdata(TPID, UserID) AS
(
   SELECT tc.Testcases_ProjectsID, tc.AssignedTo 
   FROM dbo.TestCases_Cycles tc 
   INNER JOIN dbo.TestCases_Projects tp 
   ON tp.TestCases_ProjectsID = tc.Testcases_ProjectsID 
   AND tp.UserID<>tc.AssignedTo
   AND tc.CycleID=6733 AND tp.ProjectID=10500
)
UPDATE dbo.TestCases_Projects 
SET UserID=(SELECT tempdata.UserID 
            FROM tempdata 
            WHERE dbo.TestCases_Projects.TestCases_ProjectsID=tempdata.TPID), 
    AssignedOn=GETDATE()
WHERE ProjectID=10500 
AND Testcases_ProjectsID IN (SELECT Testcases_ProjectsID FROM tempdata)

将产生 IL代码(匿名方法)类似于:

internal class AnonymousMethod_Example
{
    public void MyFunc(string[] args)
    {
        var x = 5;
        Action act = delegate ()
        {
            Console.WriteLine(x);
        };
        act();
    }
}

这时, C#代码(本地功能)

.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)
            ...
        }
        ...
    }
...

将生成 IL代码(本地函数),类似于:

internal class LocalFunction_Example
{
    public void MyFunc(string[] args)
    {
        var x = 5;
        void DoIt()
        {
            Console.WriteLine(x);
        };
        DoIt();
    }
}

请注意,.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不一样)。

2 个答案:

答案 0 :(得分:11)

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

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

答案 1 :(得分:3)

匿名方法(和lambda表达式)的主要用法是能够将它们传递给使用方法以指定过滤器,谓词或方法所需的任何内容。它们并不特别适合从定义它们的相同方法调用,并且该功能仅在稍后使用 System.Action 委托进行考虑。

另一方面,本地方法恰恰相反 - 它们的主要目的是从同一方法调用,如使用局部变量。

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

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

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

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

使用本地方法不需要将方法绑定到对象,并考虑将方法传递给外部代码。我们可以纯粹在堆栈上分配DisplayClass(因为它应该用于本地数据),不会给GC带来负担。现在开发人员有两个选择 - 要么创建LocalFunc实例并将其移动到DisplayClass,要么使其成为静态并使DisplayClass成为它的第一个(ref)参数。调用方法没有区别,所以我认为选择只是随意的。他们可以做出其他决定,没有任何区别。

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