前言 在GO语言中,defer
语句用于在函数返回之前执行一些清理操作,比如关闭文件、释放资源等。
1 defer fmt.Println("Deferred" )
在C++中,则可以使用RAII(Resource Acquisition Is Initialization)的方式来实现类似的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Defer { std ::function<void ()> func; Defer(std ::function<void ()> f) : func(f) {} ~Defer() { func(); } }; #define CONCATENATE_DETAIL(x, y) x##y #define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y) #define defer(code) Defer CONCATENATE(_defer_, __COUNTER__)([&]() { code; }) void example () { FILE* f = fopen("file.txt" , "r" ); defer(fclose(f)); }
但是C#的结构体并不能像C++那样在出作用域时自动调用析构函数,因此无法直接使用RAII的方式来实现defer功能,那么该如何实现呢?
实现 C#提供了using
语句,可以在代码块结束时自动调用IDisposable
接口的Dispose
方法,从而可以这样实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public struct Defer : IDisposable{ private Action _onLeave; public Defer (Action onLeave ) { _onLeave = onLeave; } public void Dispose ( ) { _onLeave?.Invoke(); _onLeave = null ; } } void Example ( ) { using var _ = new Defer(() => Debug.Log("Deferred" )); Debug.Log("In block" ); }
using的原理和执行顺序 需要知道的是,using
语句在编译时会被转换成try...finally
语句,因此上面的代码实际上等价于:
1 2 3 4 5 6 7 8 9 10 11 12 13 void Example ( ) { Defer _ = new Defer(() => Debug.Log("Deferred" )); try { Debug.Log("In block" ); } finally { ((IDisposable) _).Dispose(); } }
当同时存在多个using
语句时,Dispose
的调用顺序是与using
语句的顺序相反的,也就是后进先出 。因为要先执行内部的finally块,再执行外部的finally块。
Defer的GC问题 注意到Defer
是一个struct
,而不是class
,是为了避免在堆上分配内存,从而减少GC的压力。但是可能会有人问,结构体转换成接口引用,不还是会装箱吗?
答案是:这里不会。我们可以通过查看反编译后的il代码来确认。为了方便比较,这里又实现了另外两种Defer的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class DeferClass : IDisposable { private Action _onLeave; public DeferClass (Action onLeave ) { _onLeave = onLeave; } public void Dispose ( ) { _onLeave?.Invoke(); _onLeave = null ; } } void ExampleWithCast ( ) { using var _ = (IDisposable)new Defer(() => Debug.Log("Deferred With Cast" )); Debug.Log("In block With Cast" ); } void ExampleClass ( ) { using var _ = new DeferClass(() => Debug.Log("Deferred Class" )); Debug.Log("In block Class" ); }
我们使用ilspy查看反编译后的il代码:
Example()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 .method private hidebysig instance void Example ( ) cil managed { .maxstack 3 .locals init ( [0 ] valuetype Defer _ ) IL_0000: nop IL_0001: ldloca.s 0 IL_0003: ldsfld class [netstandard]System.Action DeferTest/'<>c'::'<>9__0_0' IL_0008: dup IL_0009: brtrue.s IL_0022 IL_000b: pop IL_000c: ldsfld class DeferTest/'<>c' DeferTest/'<>c'::'<>9' IL_0011: ldftn instance void DeferTest/'<>c'::'<Example>b__0_0'( ) IL_0017: newobj instance void [netstandard]System.Action::.ctor (object , native int ) IL_001c: dup IL_001d: stsfld class [netstandard]System.Action DeferTest/'<>c'::'<>9__0_0' IL_0022: call instance void Defer::.ctor (class [netstandard]System.Action ) .try { IL_0027: ldstr "In block" IL_002c: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object) IL_0031: nop IL_0032: leave.s IL_0043 } finally { IL_0034: ldloca.s 0 IL_0036: constrained. Defer IL_003c: callvirt instance void [netstandard]System.IDisposable::Dispose() IL_0041: nop IL_0042: endfinally } IL_0043: ret }
ExampleWithCast()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 .method private hidebysig instance void ExampleWithCast ( ) cil managed { .maxstack 2 .locals init ( [0 ] class [netstandard]System.IDisposable _ ) IL_0000: nop IL_0001: ldsfld class [netstandard]System.Action DeferTest/'<>c'::'<>9__1_0' IL_0006: dup IL_0007: brtrue.s IL_0020 IL_0009: pop IL_000a: ldsfld class DeferTest/'<>c' DeferTest/'<>c'::'<>9' IL_000f: ldftn instance void DeferTest/'<>c'::'<ExampleWithCast>b__1_0'( ) IL_0015: newobj instance void [netstandard]System.Action::.ctor (object , native int ) IL_001a: dup IL_001b: stsfld class [netstandard]System.Action DeferTest/'<>c'::'<>9__1_0' IL_0020: newobj instance void Defer::.ctor (class [netstandard]System.Action ) IL_0025: box Defer IL_002a: stloc.0 .try { IL_002b: ldstr "In block With Cast" IL_0030: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object) IL_0035: nop IL_0036: leave.s IL_0043 } finally { IL_0038: ldloc.0 IL_0039: brfalse.s IL_0042 IL_003b: ldloc.0 IL_003c: callvirt instance void [netstandard]System.IDisposable::Dispose() IL_0041: nop IL_0042: endfinally } IL_0043: ret }
ExampleClass()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 .method private hidebysig instance void ExampleClass ( ) cil managed { .maxstack 2 .locals init ( [0 ] class DeferClass _ ) IL_0000: nop IL_0001: ldsfld class [netstandard]System.Action DeferTest/'<>c'::'<>9__2_0' IL_0006: dup IL_0007: brtrue.s IL_0020 IL_0009: pop IL_000a: ldsfld class DeferTest/'<>c' DeferTest/'<>c'::'<>9' IL_000f: ldftn instance void DeferTest/'<>c'::'<ExampleClass>b__2_0'( ) IL_0015: newobj instance void [netstandard]System.Action::.ctor (object , native int ) IL_001a: dup IL_001b: stsfld class [netstandard]System.Action DeferTest/'<>c'::'<>9__2_0' IL_0020: newobj instance void DeferClass::.ctor (class [netstandard]System.Action ) IL_0025: stloc.0 .try { IL_0026: ldstr "In block Class" IL_002b: call void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object) IL_0030: nop IL_0031: leave.s IL_003e } finally { IL_0033: ldloc.0 IL_0034: brfalse.s IL_003d IL_0036: ldloc.0 IL_0037: callvirt instance void [netstandard]System.IDisposable::Dispose() IL_003c: nop IL_003d: endfinally } IL_003e: ret }
对比Example()
和ExampleWithCast()
,可以看到两点区别:后者有一个明显的装箱操作,而前者没有。
1 2 IL_0020: newobj instance void Defer::.ctor(class [netstandard]System.Action) IL_0025: box Defer
前者在通过callvirt
调用Dispose
前使用了constrained.
指令。
1 2 IL_0036: constrained. Defer IL_003c: callvirt instance void [netstandard]System.IDisposable::Dispose()
查看MSDN 上关于constrained.
的描述:
When a callvirt method instruction has been prefixed by constrained thisType, the instruction is executed as follows:
If thisType is a reference type (as opposed to a value type) then ptr is dereferenced and passed as the 'this' pointer to the callvirt of method.
If thisType is a value type and thisType implements method then ptr is passed unmodified as the 'this' pointer to a call method instruction, for the implementation of method by thisType.
If thisType is a value type and thisType does not implement method then ptr is dereferenced, boxed, and passed as the 'this' pointer to the callvirt method instruction.
This last case can occur only when method was defined on Object, ValueType, or Enum and not overridden by thisType. In this case, the boxing causes a copy of the original object to be made. However, because none of the methods of Object, ValueType, and Enum modify the state of the object, this fact cannot be detected.
也就是说,constrained.
指令会根据值类型是否定义了该方法来决定是否装箱,而Defer
实现了IDisposable
接口,实现了Dispose
方法,因此不会装箱。
对比Example()
和ExampleClass()
,可以看到后者没有装箱,但也是直接newobj
因此可以判断得出,第一种实现和用法中的Defer
不会产生GC。
委托优化 从上面的il代码可以看到,方法调用中存在委托示例的创建,但是这个委托是被缓存起来的。相当于如下代码:
1 2 Defer _ = new Defer(<>c.<>9 __0_0 ?? (<>c.<>9 __0_0 = new Action(<>c.<>9. <Example>b__0_0)));
上面这种缓存优化只有在委托没有捕获变量时可以生效,如果捕获了变量,每次都需要创建新的实例来保存变量,比如下面的代码:
1 2 3 4 5 6 7 void ExampleWithCapture ( ) { int x = 42 ; using var _ = new Defer(() => Debug.Log("Deferred Capture: " + x)); Debug.Log("In block Capture" ); }
会被编译成:
1 2 3 4 5 6 7 8 9 private void ExampleWithCapture ( ) { <>c__DisplayClass2_0 <>c__DisplayClass2_ = new <>c__DisplayClass2_0(); <>c__DisplayClass2_.x = 42 ; using (new Defer(new Action(<>c__DisplayClass2_.<ExampleWithCapture>b__0))) { Debug.Log((object )"In block Capture" ); } }
为了尽量减少这样的闭包,可以想到的一种优化方法是在
Defer
中直接传递参数,而不是通过闭包捕获变量。比如一个参数的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public struct Defer<T> : IDisposable{ private Action<T> _onLeave; private T _para; public Defer (T para, Action<T> onLeave ) { _para = para; _onLeave = onLeave; } public void Dispose ( ) { _onLeave?.Invoke(_para); _onLeave = null ; _para = default ; } } using var _ = new Defer<int >(x, x => Debug.Log("Deferred Capture with Param: " + x));
除了一个参数当然可以扩展到多个参数的情况。需要注意的是,这里参数都是按值捕获的,不过对于绝大多数情况来说是够用了。
完整代码 Defer.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 public struct Defer : IDisposable{ private Action _onLeave; public Defer (Action onLeave ) { _onLeave = onLeave; } public void Dispose ( ) { _onLeave?.Invoke(); _onLeave = null ; } public static Defer OnLeave (Action onLeave ) => new (onLeave); public static Defer <T > OnLeave <T >(T para, Action<T> onLeave ) => new (para, onLeave); public static Defer <T1 , T2 > OnLeave <T1 , T2 >(T1 para1, T2 para2, Action<T1, T2> onLeave ) => new (para1, para2, onLeave); } public struct Defer<T> : IDisposable{ private Action<T> _onLeave; private T _para; public Defer (T para, Action<T> onLeave ) { _para = para; _onLeave = onLeave; } public void Dispose ( ) { _onLeave?.Invoke(_para); _onLeave = null ; _para = default ; } } public struct Defer<T1, T2> : IDisposable{ private Action<T1, T2> _onLeave; private T1 _para1; private T2 _para2; public Defer (T1 para1, T2 para2, Action<T1, T2> onLeave ) { _para1 = para1; _para2 = para2; _onLeave = onLeave; } public void Dispose ( ) { _onLeave?.Invoke(_para1, _para2); _onLeave = null ; _para1 = default ; _para2 = default ; } }