# 前言
匿名函数平时用得太常见了,平时都知道说用多了不好。
为什么不好?原因呢?
特别是涉及到造成 闭包
的匿名函数,其原理是什么?闭包 抓住
的的局部变量怎么处理的?
众所周知 匿名函数
其实是 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,每一个循环都会造成一次实例化。—— 当然,闭包确实也是一个很方便的特性... 必要时可以极大减少代码量,不要滥用就好。