在C#中实现defer

前言

在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));
// do something with 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
// class版本的Defer
public class DeferClass : IDisposable
{
private Action _onLeave;

public DeferClass(Action onLeave)
{
_onLeave = onLeave;
}

public void Dispose()
{
_onLeave?.Invoke();
_onLeave = null;
}
}

// 先转换成接口类型再using
void ExampleWithCast()
{
using var _ = (IDisposable)new Defer(() => Debug.Log("Deferred With Cast"));

Debug.Log("In block With Cast");
}

// 使用class版本的Defer
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
// Defer defer = new Defer(delegate
// {
// Debug.Log((object)"Deferred");
// });
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

// Debug.Log((object)"In block");
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
} // end .try
finally
{
// ((IDisposable)defer).Dispose();
IL_0034: ldloca.s 0
IL_0036: constrained. Defer /////////////////// 约束操作 ////////////////////
IL_003c: callvirt instance void [netstandard]System.IDisposable::Dispose()
// }
IL_0041: nop
IL_0042: endfinally
} // end handler

// (no C# code)
IL_0043: ret
} // end of method DeferTest::Example

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
// using ((object)new Defer(delegate
// {
// Debug.Log((object)"Deferred With Cast");
// }))
IL_0001: ldsfld class [netstandard]System.Action DeferTest/'<>c'::'<>9__1_0'
IL_0006: dup
IL_0007: brtrue.s IL_0020

// (no C# code)
IL_0009: pop
// Debug.Log((object)"In block With Cast");
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
} // end .try
finally
{
// (no C# code)
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
} // end handler

IL_0043: ret
} // end of method DeferTest::ExampleWithCast

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
// using (new DeferClass(delegate
// {
// Debug.Log((object)"Deferred Class");
// }))
IL_0001: ldsfld class [netstandard]System.Action DeferTest/'<>c'::'<>9__2_0'
IL_0006: dup
IL_0007: brtrue.s IL_0020

// (no C# code)
IL_0009: pop
// Debug.Log((object)"In block Class");
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) /////////////////// newobj ////////////////////
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
} // end .try
finally
{
// (no C# code)
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
} // end handler

IL_003e: ret
} // end of method DeferTest::ExampleClass
  1. 对比Example()ExampleWithCast(),可以看到两点区别:
    1. 后者有一个明显的装箱操作,而前者没有。

      1
      2
      IL_0020: newobj instance void Defer::.ctor(class [netstandard]System.Action)
      IL_0025: box Defer /////////////////// 装箱操作 ////////////////////

    2. 前者在通过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方法,因此不会装箱。

  2. 对比Example()ExampleClass(),可以看到后者没有装箱,但也是直接newobj

因此可以判断得出,第一种实现和用法中的Defer不会产生GC。

委托优化

从上面的il代码可以看到,方法调用中存在委托示例的创建,但是这个委托是被缓存起来的。相当于如下代码:

1
2
// <>c是编译器自动生成的一个类,<>9__0_0是它的一个静态字段
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
/// <summary>
/// 用于离开代码块后自动执行一些操作
/// </summary>
public struct Defer : IDisposable
{
private Action _onLeave;

public Defer(Action onLeave)
{
_onLeave = onLeave;
}

public void Dispose()
{
_onLeave?.Invoke();
_onLeave = null;
}

/// <summary>
/// 当离开代码块时执行。当存在多个时,按照与定义相反的顺序从下到上执行。
/// </summary>
/// <remarks><example>用法示例:
/// <code>
/// {
/// using var _ = Defer.OnLeave(() => { /* 离开后自动执行的代码 */ });
/// }
/// </code>
/// </example></remarks>
/// <param name="onLeave">执行函数</param>
/// <returns>需要using此返回值</returns>
public static Defer OnLeave(Action onLeave) => new (onLeave);

/// <summary>
/// 当离开代码块时执行。当存在多个时,按照与定义相反的顺序从下到上执行。
/// </summary>
/// <remarks><example>用法示例:
/// <code>
/// {
/// using var _ = Defer.OnLeave(para, para => { /* 离开后自动执行的代码 */ });
/// }
/// </code>
/// </example></remarks>
/// <param name="para">捕获第一个参数</param>
/// <param name="onLeave">执行函数</param>
/// <returns>需要using此返回值</returns>
public static Defer<T> OnLeave<T>(T para, Action<T> onLeave) => new(para, onLeave);

/// <summary>
/// 当离开代码块时执行。当存在多个时,按照与定义相反的顺序从下到上执行。
/// </summary>
/// <remarks><example>用法示例:
/// <code>
/// {
/// using var _ = Defer.OnLeave(para1, para2, (para1, para2) => { /* 离开后自动执行的代码 */ });
/// }
/// </code>
/// </example></remarks>
/// <param name="para1">捕获第一个参数</param>
/// <param name="para2">捕获第二个参数</param>
/// <param name="onLeave">执行函数</param>
/// <returns>需要using此返回值</returns>
public static Defer<T1, T2> OnLeave<T1, T2>(T1 para1, T2 para2, Action<T1, T2> onLeave) => new(para1, para2, onLeave);
}

/// <summary>
/// 用于离开代码块后自动执行一些操作
/// </summary>
/// <typeparam name="T"></typeparam>
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;
}
}

/// <summary>
/// 用于离开代码块后自动执行一些操作
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
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;
}
}