# 前言

匿名函数平时用得太常见了,平时都知道说用多了不好。

为什么不好?原因呢?

特别是涉及到造成 闭包 的匿名函数,其原理是什么?闭包 抓住 的的局部变量怎么处理的?

众所周知 匿名函数 其实是 C# 提供的一种 语法糖 ,最终还是会由编译器生成具体的函数。

不过之前一直没有深入去研究,这次带着这个疑问,我决定利用反编译的 IL 代码详细研究下,对比各种匿名函数写法,看看最后究竟生成了什么。

# 分类

C# 匿名函数中,个人觉得从使用方式上主要可以分为以下几类:

  • 基本匿名函数:指没有抓住任何局部变量或成员变量,单纯执行自己的逻辑
  • 调用成员变量的匿名函数:引用了类中的成员变量
  • 带闭包的匿名函数:引用了调用方法的局部变量
    • 仅一个匿名函数所使用
    • 多个匿名函数共同使用同一个局部变量
    • 多个匿名函数使用不同的局部变量
    • 缓存 for 循环 index 的闭包

我也就大概就以这几类为例,观察其编译为 IL 代码后变成了什么样子

# 测试

首先,为了对比,定义的一个最基本的测试类如下:

public class MyClass
{
    public MyClass()
    {
    }
}

然后查看什么都没有的情况下,这个空类有什么东西:

.class public auto ansi beforefieldinit MyClass
	extends [mscorlib]System.Object
{
	// Methods
	.method public hidebysig specialname rtspecialname
		instance void .ctor () cil managed
	{
		// Method begins at RVA 0x2050
		// Header size: 1
		// Code size: 9 (0x9)
		.maxstack 8
		IL_0000: ldarg.0
		IL_0001: call instance void [mscorlib]System.Object::.ctor()
		IL_0006: nop
		IL_0007: nop
		IL_0008: ret
	} // end of method MyClass::.ctor
} // end of class MyClass

# 普通成员方法回调

为了对比,先测试一下普通的回调:指自己在类中手动定义成员函数作为回调传入。

public MyClass()
{
    DoAction(Action);
}
private void DoAction(Action callback)
{
    callback();
}
private void Action()
{
    Debug.Log("DO");
}

生成 IL 代码如下:

.class public auto ansi beforefieldinit MyClass
	extends [mscorlib]System.Object
{
	// Methods
	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// Method begins at RVA 0x2050
		// Header size: 1
		// Code size: 28 (0x1c)
		.maxstack 8
		IL_0000: ldarg.0
		IL_0001: call instance void [mscorlib]System.Object::.ctor()
		IL_0006: nop
		IL_0007: nop
		IL_0008: ldarg.0
		IL_0009: ldarg.0
		IL_000a: ldftn instance void MyClass::Action()
		IL_0010: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
		IL_0015: call instance void MyClass::DoAction(class [mscorlib]System.Action)
		IL_001a: nop
		IL_001b: ret
	} // end of method MyClass::.ctor
	.method private hidebysig 
		instance void DoAction (
			class [mscorlib]System.Action callback
		) cil managed 
	{
		// Method begins at RVA 0x206d
		// Header size: 1
		// Code size: 9 (0x9)
		.maxstack 8
		IL_0000: nop
		IL_0001: ldarg.1
		IL_0002: callvirt instance void [mscorlib]System.Action::Invoke()
		IL_0007: nop
		IL_0008: ret
	} // end of method MyClass::DoAction
	.method private hidebysig 
		instance void Action () cil managed 
	{
		// Method begins at RVA 0x2077
		// Header size: 1
		// Code size: 13 (0xd)
		.maxstack 8
		IL_0000: nop
		IL_0001: ldstr "DO"
		IL_0006: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)
		IL_000b: nop
		IL_000c: ret
	} // end of method MyClass::Action
} // end of class MyClass

可以看出,生成的 IL 代码中也是单纯的由 构造函数 + DoAction + Action 三个成员函数过程,没有什么特别的问题。

# 基本匿名函数

接着,就轮到普通匿名函数了。

将上面传参修改为匿名函数方式:

DoAction(() => Debug.Log("Log: DoAction"));

编译后 IL 代码如下:

.class public auto ansi beforefieldinit MyClass
	extends [mscorlib]System.Object
{
	// Nested Types
	.class nested private auto ansi sealed serializable beforefieldinit '<>c'
		extends [mscorlib]System.Object
	{
		.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
			01 00 00 00
		)
		// Fields
		.field public static initonly class MyClass/'<>c' '<>9'
		.field public static class [mscorlib]System.Action '<>9__0_0'
		// Methods
		.method private hidebysig specialname rtspecialname static 
			void .cctor () cil managed 
		{
			// Method begins at RVA 0x2139
			// Header size: 1
			// Code size: 11 (0xb)
			.maxstack 8
			IL_0000: newobj instance void MyClass/'<>c'::.ctor()
			IL_0005: stsfld class MyClass/'<>c' MyClass/'<>c'::'<>9'
			IL_000a: ret
		} // end of method '<>c'::.cctor
		.method public hidebysig specialname rtspecialname 
			instance void .ctor () cil managed 
		{
			// Method begins at RVA 0x2145
			// Header size: 1
			// Code size: 8 (0x8)
			.maxstack 8
			IL_0000: ldarg.0
			IL_0001: call instance void [mscorlib]System.Object::.ctor()
			IL_0006: nop
			IL_0007: ret
		} // end of method '<>c'::.ctor
		.method assembly hidebysig 
			instance void '<.ctor>b__0_0' () cil managed 
		{
			// Method begins at RVA 0x214e
			// Header size: 1
			// Code size: 12 (0xc)
			.maxstack 8
			IL_0000: ldstr "Log: DoAction"
			IL_0005: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)
			IL_000a: nop
			IL_000b: ret
		} // end of method '<>c'::'<.ctor>b__0_0'
	} // end of class <>c
	// Methods
	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// Method begins at RVA 0x2050
		// Header size: 1
		// Code size: 47 (0x2f)
		.maxstack 8
		IL_0000: ldarg.0
		IL_0001: call instance void [mscorlib]System.Object::.ctor()
		IL_0006: nop
		IL_0007: nop
		IL_0008: ldarg.0
		IL_0009: ldsfld class [mscorlib]System.Action MyClass/'<>c'::'<>9__0_0'
		IL_000e: dup
		IL_000f: brtrue.s IL_0028
		IL_0011: pop
		IL_0012: ldsfld class MyClass/'<>c' MyClass/'<>c'::'<>9'
		IL_0017: ldftn instance void MyClass/'<>c'::'<.ctor>b__0_0'()
		IL_001d: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
		IL_0022: dup
		IL_0023: stsfld class [mscorlib]System.Action MyClass/'<>c'::'<>9__0_0'
		IL_0028: call instance void MyClass::DoAction(class [mscorlib]System.Action)
		IL_002d: nop
		IL_002e: ret
	} // end of method MyClass::.ctor
	.method private hidebysig 
		instance void DoAction (
			class [mscorlib]System.Action callback
		) cil managed
	{
		// Method begins at RVA 0x2080
		// Header size: 1
		// Code size: 9 (0x9)
		.maxstack 8
		IL_0000: nop
		IL_0001: ldarg.1
		IL_0002: callvirt instance void [mscorlib]System.Action::Invoke()
		IL_0007: nop
		IL_0008: ret
	} // end of method MyClass::DoAction
} // end of class MyClass

# 分析

这时候,在原有的类中已经多了一个名为 <>c 的 Nested Types 的新类。

一步一步来看。

首先是包括的两个公共的静态字段:

// Fields
// 这个该是自己类的实例
.field public static initonly class MyClass/'<>c' '<>9'
// 绑定的委托实例
.field public static class [mscorlib]System.Action '<>9__0_0'

在构造方法中有对这个名为 <>c 的内嵌类做初始化:

IL_0000: newobj instance void MyClass/'<>c'::.ctor()
IL_0005: stsfld class MyClass/'<>c' MyClass/'<>c'::'<>9'

也就是说,一定程度上可以认为这是一个单例(虽然似乎并不标准?)。

而原本我们的匿名函数则被编译为新类中名为 b__0_0 的方法:

.method assembly hidebysig
instance void '<.ctor>b__0_0' () cil managed
{
	// Method begins at RVA 0x214e
	// Header size: 1
	// Code size: 12 (0xc)
	.maxstack 8
	IL_0000: ldstr "Log: DoAction"
	IL_0005: call void [UnityEngine.CoreModule] UnityEngine.Debug::Log(object)
    IL_000a: nop
    IL_000b: ret
} // end of method '<>c'::'<.ctor>b__0_0'

根据代码逻辑来看,字段 <>9__0_0(委托) 用于存放 b__0_0(原本匿名方法) 方法的绑定,其类型就是我们常用的内置 Action 委托。

若下次调用时这一项判断有值,就会直接调用 <>9__0_0(委托) ,否则创建委托并将 b__0_0(原本匿名方法)<>9__0_0(委托) 进行初始化绑定。

如下代码所示:

IL_0008: ldarg.0
IL_0009: ldsfld class [mscorlib]System.Action MyClass/'<>c'::'<>9__0_0'
IL_000e: dup
IL_000f: brtrue.s IL_0028
IL_0011: pop
IL_0012: ldsfld class MyClass/'<>c' MyClass/'<>c'::'<>9'
IL_0017: ldftn instance void MyClass/'<>c'::'<.ctor>b__0_0'()
IL_001d: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
IL_0022: dup
IL_0023: stsfld class [mscorlib]System.Action MyClass/'<>c'::'<>9__0_0'
IL_0028: call instance void MyClass::DoAction(class [mscorlib]System.Action)

使用静态字段配合类似单例的模式,应该是为了优化性能,这样如果实例化了多个 MyClass 的话,就只会存在有一份委托实例。

但是这样的话,为什么不直接编译为成员函数?

之前个人其实一直猜测:普通的匿名函数可能是直接编译为本类的成员函数的。

而在这里的事实 推翻了 猜测。

# 其它

使用单独方法调用的匿名函数,也是一样的情况,委托会在调用之处进行判断及绑定
如下单独成员函数中编译成的 IL 代码与上面一致:

public void DoTest()
{
	DoAction(() => Debug.Log("Log: DoAction"));
}

多个这种类型的匿名函数也是一样,只是对应增加了 Action 类型的委托字段,比如再加一个同类匿名函数:

// Fields
.field public static initonly class MyClass/'<>c' '<>9'
.field public static class [mscorlib]System.Action '<>9__0_0'
.field public static class [mscorlib]System.Action '<>9__1_0'

# 调用成员变量的匿名函数

测试的 C# 代码如下:

public class MyClass
{
    private int _myValue;
    public MyClass()
    {
        DoAction(() => Debug.Log("Log Value:" + _myValue));
    }
    private void DoAction(Action callback)
    {
        callback();
    }
}

编译后 IL 代码:

.class public auto ansi beforefieldinit MyClass
	extends [mscorlib]System.Object
{
	// Fields
	.field private int32 _myValue
	// Methods
	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// Method begins at RVA 0x2050
		// Header size: 1
		// Code size: 28 (0x1c)
		.maxstack 8
		IL_0000: ldarg.0
		IL_0001: call instance void [mscorlib]System.Object::.ctor()
		IL_0006: nop
		IL_0007: nop
		IL_0008: ldarg.0
		IL_0009: ldarg.0
		IL_000a: ldftn instance void MyClass::'<.ctor>b__1_0'()
		IL_0010: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
		IL_0015: call instance void MyClass::DoAction(class [mscorlib]System.Action)
		IL_001a: nop
		IL_001b: ret
	} // end of method MyClass::.ctor
	.method private hidebysig 
		instance void DoAction (
			class [mscorlib]System.Action callback
		) cil managed 
	{
		// Method begins at RVA 0x206d
		// Header size: 1
		// Code size: 9 (0x9)
		.maxstack 8
		IL_0000: nop
		IL_0001: ldarg.1
		IL_0002: callvirt instance void [mscorlib]System.Action::Invoke()
		IL_0007: nop
		IL_0008: ret
	} // end of method MyClass::DoAction
	.method private hidebysig 
		instance void '<.ctor>b__1_0' () cil managed 
	{
		.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
			01 00 00 00
		)
		// Method begins at RVA 0x2077
		// Header size: 1
		// Code size: 28 (0x1c)
		.maxstack 8
		IL_0000: ldstr "Log Value:"
		IL_0005: ldarg.0
		IL_0006: ldflda int32 MyClass::_myValue
		IL_000b: call instance string [mscorlib]System.Int32::ToString()
		IL_0010: call string [mscorlib]System.String::Concat(string, string)
		IL_0015: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)
		IL_001a: nop
		IL_001b: ret
	} // end of method MyClass::'<.ctor>b__1_0'
} // end of class MyClass

!!!!!!

看来我关于 普通的匿名函数可能是直接编译为本类的成员函数 的猜测也不全是错误的,原来得对本类的 成员变量 发生了访问,才会直接编译为 成员函数

从上面代码可以看出,之前普通匿名函数会额外生成的内嵌类已经不在了,只多了一个名为 b__1_0 的成员函数。

# (闭包) 仅一个匿名函数所使用

测试代码:

public class MyClass
{
    public MyClass()
    {
        int val = 100;
        DoAction(() => Debug.Log("Log Val:" + val));
    }
    private void DoAction(Action callback)
    {
        callback();
    }
}

编译后的 IL 代码:

.class public auto ansi beforefieldinit MyClass
	extends [mscorlib]System.Object
{
	// Nested Types
	.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0'
		extends [mscorlib]System.Object
	{
		.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
			01 00 00 00
		)
		// Fields
		.field public int32 val
		// Methods
		.method public hidebysig specialname rtspecialname 
			instance void .ctor () cil managed 
		{
			// Method begins at RVA 0x213d
			// Header size: 1
			// Code size: 8 (0x8)
			.maxstack 8
			IL_0000: ldarg.0
			IL_0001: call instance void [mscorlib]System.Object::.ctor()
			IL_0006: nop
			IL_0007: ret
		} // end of method '<>c__DisplayClass0_0'::.ctor
		.method assembly hidebysig 
			instance void '<.ctor>b__0' () cil managed 
		{
			// Method begins at RVA 0x2146
			// Header size: 1
			// Code size: 28 (0x1c)
			.maxstack 8
			IL_0000: ldstr "Log Val:"
			IL_0005: ldarg.0
			IL_0006: ldflda int32 MyClass/'<>c__DisplayClass0_0'::val
			IL_000b: call instance string [mscorlib]System.Int32::ToString()
			IL_0010: call string [mscorlib]System.String::Concat(string, string)
			IL_0015: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)
			IL_001a: nop
			IL_001b: ret
		} // end of method '<>c__DisplayClass0_0'::'<.ctor>b__0'
	} // end of class <>c__DisplayClass0_0
	// Methods
	.method public hidebysig specialname rtspecialname 
		instance void .ctor () cil managed 
	{
		// Method begins at RVA 0x2050
		// Header size: 12
		// Code size: 42 (0x2a)
		.maxstack 3
		.locals init (
			[0] class MyClass/'<>c__DisplayClass0_0' 'CS$<>8__locals0'
		)
		IL_0000: ldarg.0
		IL_0001: call instance void [mscorlib]System.Object::.ctor()
		IL_0006: nop
		IL_0007: newobj instance void MyClass/'<>c__DisplayClass0_0'::.ctor()
		IL_000c: stloc.0
		IL_000d: nop
		IL_000e: ldloc.0
		IL_000f: ldc.i4.s 100
		IL_0011: stfld int32 MyClass/'<>c__DisplayClass0_0'::val
		IL_0016: ldarg.0
		IL_0017: ldloc.0
		IL_0018: ldftn instance void MyClass/'<>c__DisplayClass0_0'::'<.ctor>b__0'()
		IL_001e: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
		IL_0023: call instance void MyClass::DoAction(class [mscorlib]System.Action)
		IL_0028: nop
		IL_0029: ret
	} // end of method MyClass::.ctor
	.method private hidebysig 
		instance void DoAction (
			class [mscorlib]System.Action callback
		) cil managed 
	{
		// Method begins at RVA 0x2086
		// Header size: 1
		// Code size: 9 (0x9)
		.maxstack 8
		IL_0000: nop
		IL_0001: ldarg.1
		IL_0002: callvirt instance void [mscorlib]System.Action::Invoke()
		IL_0007: nop
		IL_0008: ret
	} // end of method MyClass::DoAction
} // end of class MyClass

可以发现,与 普通匿名函数 一样,这里生成了一个名为 <>c__DisplayClass0_0 新的内嵌类,且我们的匿名方法被编译为内嵌类的成员函数。局部变量则作为新类的 成员变量

但与之不同的是:该类字段并非静态的。

再仔细看看调用的地方:

IL_0007: newobj instance void MyClass/'<>c__DisplayClass0_0'::.ctor()
IL_000c: stloc.0
IL_000d: nop
IL_000e: ldloc.0
IL_000f: ldc.i4.s 100
IL_0011: stfld int32 MyClass/'<>c__DisplayClass0_0'::val
IL_0016: ldarg.0
IL_0017: ldloc.0
IL_0018: ldftn instance void MyClass/'<>c__DisplayClass0_0'::'<.ctor>b__0'()
IL_001e: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
IL_0023: call instance void MyClass::DoAction(class [mscorlib]System.Action)

可以明显看出不同:这里直接暴力创建了新类,然后将 局部变量 赋值后调用 —— 不存在缓存,也没什么特殊的优化!

相当于存在闭包的匿名函数:每次走到匿名函数创建的地方,都会创建新的类型。

# (闭包) 多个匿名函数共同使用同一个局部变量

与前一项类似,只多增加几个调用:

public MyClass()
{
    int val = 100;
    DoAction(() => Debug.Log("Log Val:" + val));
    DoAction(() => Debug.Log("Log2 Val:" + val));
    DoAction(() => Debug.Log("Log3 Val:" + val));
}

具体的 IL 代码就不贴了,生成的 IL 是差不多的,三个匿名函数被编译为同一个类的成员函数,共享 val 变量。

看一下调用时:

IL_0007: newobj instance void MyClass/'<>c__DisplayClass0_0'::.ctor()
IL_000c: stloc.0
IL_000d: nop
IL_000e: ldloc.0
IL_000f: ldc.i4.s 100
IL_0011: stfld int32 MyClass/'<>c__DisplayClass0_0'::val
IL_0016: ldarg.0
IL_0017: ldloc.0
IL_0018: ldftn instance void MyClass/'<>c__DisplayClass0_0'::'<.ctor>b__0'()
IL_001e: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
IL_0023: call instance void MyClass::DoAction(class [mscorlib]System.Action)
IL_0028: nop
IL_0029: ldarg.0
IL_002a: ldloc.0
IL_002b: ldftn instance void MyClass/'<>c__DisplayClass0_0'::'<.ctor>b__1'()
IL_0031: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
IL_0036: call instance void MyClass::DoAction(class [mscorlib]System.Action)
IL_003b: nop
IL_003c: ldarg.0
IL_003d: ldloc.0
IL_003e: ldftn instance void MyClass/'<>c__DisplayClass0_0'::'<.ctor>b__2'()
IL_0044: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
IL_0049: call instance void MyClass::DoAction(class [mscorlib]System.Action)

# (闭包) 多个匿名函数使用不同的局部变量

处理方式与前者一致。

public MyClass()
{
    int val = 100;
    int val2 = 12345;
    DoAction(() => Debug.Log("Log Val:" + val));
    DoAction(() => Debug.Log("Log2 Val:" + val2));
}

调用时:

IL_0007: newobj instance void MyClass/'<>c__DisplayClass0_0'::.ctor()
IL_000c: stloc.0
IL_000d: nop
IL_000e: ldloc.0
IL_000f: ldc.i4.s 100
IL_0011: stfld int32 MyClass/'<>c__DisplayClass0_0'::val
IL_0016: ldloc.0
IL_0017: ldc.i4 12345
IL_001c: stfld int32 MyClass/'<>c__DisplayClass0_0'::val2
IL_0021: ldarg.0
IL_0022: ldloc.0
IL_0023: ldftn instance void MyClass/'<>c__DisplayClass0_0'::'<.ctor>b__0'()
IL_0029: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
IL_002e: call instance void MyClass::DoAction(class [mscorlib]System.Action)
IL_0033: nop
IL_0034: ldarg.0
IL_0035: ldloc.0
IL_0036: ldftn instance void MyClass/'<>c__DisplayClass0_0'::'<.ctor>b__1'()
IL_003c: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
IL_0041: call instance void MyClass::DoAction(class [mscorlib]System.Action)

# (闭包) 同时引用局部变量和成员变量

int val = 100;
public MyClass()
{
    int localVal = 0;
    DoAction(() => Debug.Log("Log" + val + localVal));
}

与闭包一样都是新的实例,区别在于会多定义一个 本类字段 将本类传入以便引用成员变量:

// Nested Types
.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass1_0'
    extends [mscorlib]System.Object
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Fields
	// 局部变量字段
    .field public int32 localVal
	// 引用类的字段,用于获取成员变量
    .field public class MyClass '<>4__this'
    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
		//=========== 省略 =================================
    } // end of method '<>c__DisplayClass1_0'::.ctor
    .method assembly hidebysig
        instance void '<.ctor>b__0' () cil managed
    {
        // Method begins at RVA 0x215a
        // Header size: 1
        // Code size: 44 (0x2c)
        .maxstack 8
        IL_0000: ldstr "Log"
        IL_0005: ldarg.0
        IL_0006: ldfld class MyClass MyClass/'<>c__DisplayClass1_0'::'<>4__this'
        IL_000b: ldflda int32 MyClass::val
        IL_0010: call instance string [mscorlib]System.Int32::ToString()
        IL_0015: ldarg.0
        IL_0016: ldflda int32 MyClass/'<>c__DisplayClass1_0'::localVal
        IL_001b: call instance string [mscorlib]System.Int32::ToString()
        IL_0020: call string [mscorlib]System.String::Concat(string, string, string)
        IL_0025: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)
        IL_002a: nop
        IL_002b: ret
    } // end of method '<>c__DisplayClass1_0'::'<.ctor>b__0'
} // end of class <>c__DisplayClass1_0
// Fields
.field private int32 val
// Methods
.method public hidebysig specialname rtspecialname 
    instance void .ctor () cil managed 
{
    // Method begins at RVA 0x2050
    // Header size: 12
    // Code size: 56 (0x38)
    .maxstack 3
    .locals init (
        [0] class MyClass/'<>c__DisplayClass1_0' 'CS$<>8__locals0'
    )
    IL_0000: ldarg.0
    IL_0001: ldc.i4.s 100
    IL_0003: stfld int32 MyClass::val
    IL_0008: ldarg.0
    IL_0009: call instance void [mscorlib]System.Object::.ctor()
    IL_000e: nop
    IL_000f: newobj instance void MyClass/'<>c__DisplayClass1_0'::.ctor()
    IL_0014: stloc.0
    IL_0015: ldloc.0
    IL_0016: ldarg.0
	// 将自身引用赋值进去
    IL_0017: stfld class MyClass MyClass/'<>c__DisplayClass1_0'::'<>4__this'
    IL_001c: nop
    IL_001d: ldloc.0
    IL_001e: ldc.i4.0
	// 赋值使用到的局部变量
    IL_001f: stfld int32 MyClass/'<>c__DisplayClass1_0'::localVal
    IL_0024: ldarg.0
    IL_0025: ldloc.0
    IL_0026: ldftn instance void MyClass/'<>c__DisplayClass1_0'::'<.ctor>b__0'()
    IL_002c: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
    IL_0031: call instance void MyClass::DoAction(class [mscorlib]System.Action)
    IL_0036: nop
    IL_0037: ret
} // end of method MyClass::.ctor

# (闭包) for 循环缓存

C# 代码:

public MyClass()
{
    for (int i = 0; i < 10; i++)
    {
        int index = i;
        DoAction(() => Debug.Log("Log index:" + index));
    }
}

看了下与闭包匿名函数一样,被编译为新的内嵌类,不同之处在于使用的地方。

关键 IL 代码:

IL_0008: ldc.i4.0
IL_0009: stloc.0
IL_000a: br.s IL_0032
// loop start (head: IL_0032)
	IL_000c: newobj instance void MyClass/'<>c__DisplayClass0_0'::.ctor()
	IL_0011: stloc.1
	IL_0012: nop
	IL_0013: ldloc.1
	IL_0014: ldloc.0
	IL_0015: stfld int32 MyClass/'<>c__DisplayClass0_0'::index
	IL_001a: ldarg.0
	IL_001b: ldloc.1
	IL_001c: ldftn instance void MyClass/'<>c__DisplayClass0_0'::'<.ctor>b__0'()
	IL_0022: newobj instance void [mscorlib] System.Action::.ctor(object, native int)
	IL_0027: call instance void MyClass::DoAction(class [mscorlib] System.Action)
	IL_002c: nop
	IL_002d: nop
	IL_002e: ldloc.0
	IL_002f: ldc.i4.1
	IL_0030: add
	IL_0031: stloc.0
	IL_0032: ldloc.0
	IL_0033: ldc.i4.s 10
	IL_0035: clt
	IL_0037: stloc.2
	IL_0038: ldloc.2
	IL_0039: brtrue.s IL_000c
// end loop

每一次 循环中都创建了一个内嵌类 <>c__DisplayClass0_0 的实例!非常恐怖。

于是想 对比测试 下,我们通常说 for 循环 index 重复 ( 即始终取到最后一个index ) 的情景:

C# 代码如下:

for (int i = 0; i < 10; i++)
{
    //int index = i;
    DoAction(() => Debug.Log("Log index:" + i));
}

IL 代码:

IL_0008: newobj instance void MyClass/ '<>c__DisplayClass0_0'::.ctor()
IL_000d: stloc.0
IL_000e: ldloc.0
IL_000f: ldc.i4.0
IL_0010: stfld int32 MyClass / '<>c__DisplayClass0_0'::i
IL_0015: br.s IL_003c
// loop start (head: IL_003c)
	IL_0017: nop
	IL_0018: ldarg.0
	IL_0019: ldloc.0
	IL_001a: ldftn instance void MyClass/ '<>c__DisplayClass0_0'::'<.ctor>b__0'()
	IL_0020: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
	IL_0025: call instance void MyClass::DoAction(class [mscorlib] System.Action)
	IL_002a: nop
	IL_002b: nop
	IL_002c: ldloc.0
	IL_002d: ldfld int32 MyClass/'<>c__DisplayClass0_0'::i
	IL_0032: stloc.1
	IL_0033: ldloc.0
	IL_0034: ldloc.1
	IL_0035: ldc.i4.1
	IL_0036: add
	IL_0037: stfld int32 MyClass/'<>c__DisplayClass0_0'::i
	IL_003c: ldloc.0
	IL_003d: ldfld int32 MyClass/'<>c__DisplayClass0_0'::i
	IL_0042: ldc.i4.s 10
	IL_0044: clt
	IL_0046: stloc.2
	IL_0047: ldloc.2
	IL_0048: brtrue.s IL_0017
// end loop

这种模式就是在循环外创建的实例了,也就是整个 for 循环只会有一个委托内嵌类实例,因此字段值会被覆盖。

# 闭包优化 -- 传参

(2023.2.9 补充)

对于闭包的情况,有没有优化方式呢?

答案是有的,虽然有一定局限 —— 比如通过局部变量的 传参 的方式使用:

public MyClass()
{
    int val = 100;
    DoAction((pr) => Debug.Log("Log Val:" + pr), val);
}
private void DoAction<T>(Action<T> callback, T parm)
{
    callback(parm);
}

这种方式编译后,生成的 IL 代码与普通匿名函数一样,生成的是一个静态对象,不会造成使用时反复创建新的内嵌类的实例:

//======= 内嵌类字段是静态的,匿名方法被编译为对应带参数成员函数 =========
.method assembly hidebysig
    instance void '<.ctor>b__0_0' (
        int32 pr
    ) cil managed 
{
    // Method begins at RVA 0x2156
    // Header size: 1
    // Code size: 24 (0x18)
    .maxstack 8
    IL_0000: ldstr "Log Val:"
    IL_0005: ldarga.s pr
    IL_0007: call instance string [mscorlib]System.Int32::ToString()
    IL_000c: call string [mscorlib]System.String::Concat(string, string)
    IL_0011: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)
    IL_0016: nop
    IL_0017: ret
} // end of method '<>c'::'<.ctor>b__0_0'
//=== 创建和执行处,判断了是否有实例化绑定过委托 ====
IL_0008: ldc.i4.s 100
IL_000a: stloc.0
IL_000b: ldarg.0
IL_000c: ldsfld class [mscorlib]System.Action`1<int32> MyClass/'<>c'::'<>9__0_0'
IL_0011: dup
IL_0012: brtrue.s IL_002b
IL_0014: pop
IL_0015: ldsfld class MyClass/'<>c' MyClass/'<>c'::'<>9'
IL_001a: ldftn instance void MyClass/'<>c'::'<.ctor>b__0_0'(int32)
IL_0020: newobj instance void class [mscorlib]System.Action`1<int32>::.ctor(object, native int)
IL_0025: dup
IL_0026: stsfld class [mscorlib]System.Action`1<int32> MyClass/'<>c'::'<>9__0_0'
IL_002b: ldloc.0
IL_002c: call instance void MyClass::DoAction<int32>(class [mscorlib]System.Action`1<!!0>, !!0)

注:局部变量缓存的匿名委托,一样是会编译为内嵌类。

例如如下代码编译后与上述 IL 代码基本一致:

Action<int> callback = (pr) => Debug.Log("Log Val:");
callback.Invoke(val);

当然,这种优化方式也有局限性:那就是调用时就必须传参, 没法存储 局部变量 以待后续 的使用,要么就是绑定者自己直接就存储参数。

# 总结

  • 普通匿名函数 (没有对外部产生任何变量引用):被编译为一个内嵌类,匿名函数本身成为新类的一个成员函数,并使用静态字段绑定委托。保证调用时只会初始化一次。
  • 调用过成员变量的匿名函数:会被直接编译为成员函数
  • 带闭包匿名函数:编译为普通内嵌类,其中生成对应局部变量类型的字段,生成时会创建该类实例,然后将局部变量为其赋值。
    • 相当于每次走到匿名函数创建的地方,都会创建新的类型,评价是性能堪忧
    • 多个匿名函数共同使用同一个局部变量,被编译在同一个类中
    • 多个匿名函数使用不同的局部变量:被编译在同一个类中
    • for 循环缓存:
      • 若使用循环外的局部变量,则只有一个实例
      • 若是循环内部的局部变量,循环多少次,就会创建多少个实例 (!)

按照如上的情况来看:

  • 普通的匿名函数 应该没什么大问题,委托会被作为静态实例缓存,只会第一次运行到代码块时初始化一次(可能需要考虑由于静态字段原因,一旦创建就不会释放,不过觉得这点空间消耗应该不用过于担心)
  • 仅调用成员变量的匿名函数 它跟自己在类中定义一个成员函数没有区别(当然由于委托本身也是一个类,他与自己的成员函数一样,作为委托传递本身也有实例化消耗)
  • 带闭包的匿名函数 性能就比较糟糕了:每次运行到的时候都会创建新类型的实例,普通 for循环普通闭包匿名函数 一样。同时加上委托本身实例化消耗。
    • 同时引用局部变量和成员变量 的匿名函数,与闭包一样都是新的实例,区别在于会多定义一个 本类字段 将本类传入以便引用成员变量
    • 若实在需要局部变量作为参数,可以考虑是否可以使用传参的方式做优化 (例如 Task.TaskFactory.StartNew 就提供了传参的重载)

注意 特别最为需要避免的就是 for 循环中使用循环中定义的局部变量:例如缓存 index,每一个循环都会造成一次实例化。—— 当然,闭包确实也是一个很方便的特性... 必要时可以极大减少代码量,不要滥用就好。