C#中内联Lambda函数的创建行为

7
如果我有一个像这样的方法:
void SomeMethod () {
    Func<A,B> f =  a => /*Some code*/;
    ...
    b = f (a);
}

每次调用 SomeMethod 时,f 都会被创建吗?我的意思是说,这行代码需要花费时间进行计算,还是编译器将函数保存在某个地方,在执行时跳过它?


2
一个 Func 是一个对象,其主体与普通方法一样被“存储”(因此不会“重新计算”)。或者这是关于在 SomeMethod 的后续调用中,f 是否“引用相等”的问题?(我认为它会,但是手头没有参考资料。) - user2864740
如果您将一个调用的 f 与稍后调用的 f 进行比较,它们将作为引用和实例而不同,因此 ReferenceEquals 将为 false,但根据 Equals 的相关重写,它们将是相等的。这个事实是 C# 规范所要求的。还要注意,这里 重载了运算符 == - Jeppe Stig Nielsen
2个回答

5
考虑这个简单的例子:
static void Main(string[] args)
{
     Test();
     Test();
}
static void Test()
{
     Func<int, int> f = a => a * a;
     int b = f(2);
     Console.WriteLine(b);
}

创建的方法是: enter image description here <Test>b__0 的 IL 代码为:
.method private hidebysig static int32  '<Test>b__0'(int32 a) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       8 (0x8)
  .maxstack  2
  .locals init ([0] int32 CS$1$0000)
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method Program::'<Test>b__0'

Test 方法的 IL 代码:

    .method private hidebysig static void  Test() cil managed
{
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Func`2<int32,int32> f,
           [1] int32 b)
  IL_0000:  nop
  IL_0001:  ldsfld     class [mscorlib]System.Func`2<int32,int32> ConsoleApplication10.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
  IL_0006:  brtrue.s   IL_001b
  IL_0008:  ldnull
  IL_0009:  ldftn      int32 ConsoleApplication10.Program::'<Test>b__0'(int32)
  IL_000f:  newobj     instance void class [mscorlib]System.Func`2<int32,int32>::.ctor(object,
                                                                                       native int)
  IL_0014:  stsfld     class [mscorlib]System.Func`2<int32,int32> ConsoleApplication10.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
  IL_0019:  br.s       IL_001b
  IL_001b:  ldsfld     class [mscorlib]System.Func`2<int32,int32> ConsoleApplication10.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
  IL_0020:  stloc.0
  IL_0021:  ldloc.0
  IL_0022:  ldc.i4.2
  IL_0023:  callvirt   instance !1 class [mscorlib]System.Func`2<int32,int32>::Invoke(!0)
  IL_0028:  stloc.1
  IL_0029:  ldloc.1
  IL_002a:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::Test

每次调用Test方法时,缓存的匿名委托(即Func<int,int>)将被加载,随后调用Invoke方法。
因此,答案是,编译器只会创建一次匿名方法(<Test>b__0),而不是每次调用函数都会创建。

确切地说,IL 中的静态方法 '<Test>b__0' 就是我在答案中称之为 StrangeName 的方法。 - Jeppe Stig Nielsen
如果一些变量被闭包所捕获,编译器会如何处理? - Cristian Garcia

4
C#编译器从lambda箭头中创建一个普通的(非匿名)方法,然后委托f将在其调用列表中具有该“生成的”方法。
每次运行SomeMethod时,都会创建一个新的Func<,>委托实例,该实例引用普通但生成的方法。
就像这样:
void SomeMethod () {
    Func<A, B> f = StrangeName;
    ...
    b = f(a);
}

static B StrangeName(A a) {
    /*Some code*/
}

1
如果一些变量被闭包捕获,编译器如何处理? - Cristian Garcia
@CristianGarcia 很好的评论。如果这些变量是字段,它们将通过\*一些代码*/进行访问。也许需要将一些实例引用传递给StrangeName,或者将StrangeName设置为非静态。如果这些变量是局部变量,编译器将把它们提升为字段,无论是在同一个类中的字段还是在新生成的类中的字段,我们称之为StrangeNameClass。具体细节取决于哪些变量被“捕获”,以及C#编译器作者的实现选择。 - Jeppe Stig Nielsen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接