C#之弱事件(Weak Event)的实现

  欢迎参与讨论,转载请注明出处。

前言

  最近使用C#开发项目时,发现一个会导致内存泄漏的陷阱——event里的成员并非弱引用,这样便会导致与event相关联的对象都不会被回收,从而导致内存泄漏。如此便很有必要实现一款弱事件(Weak Event)以解决此问题。

分析

  首先当然是找找是否存在现成的方案,答案是有的,不过很奇怪的是,该解决方案隶属于WPF,那么便没戏了。从网上来看也有不少各自的实现,不过个人对此都不算太满意,于是便打算自己造个轮子。
  实现弱事件自然需要用到弱引用,而弱引用的具体实现则是WeakReference,可以根据Delegate提供的Target作为弱引用对象,Method作为调用。
  剩下的问题便是Delegate的参数问题了,很可惜Delegate似乎不支持作为泛型,但是Delegate的参数还是支持的。但即便是支持,也不方便作为多个参数来进行了。那么只能选择继承EventArgs了,EventArgs本身是个空类,一般做法是继承它然后自定义,这也是微软官方所推荐的做法。

实现

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
using System;
using System.Reflection;
using System.Collections.Generic;
public class WeakEvent<TEventArgs> where TEventArgs : EventArgs {
public delegate void Func(TEventArgs e);
private static object[] ARGS = new object[1];
private class Unit {
private WeakReference reference;
private MethodInfo method;
private bool isStatic;
public bool IsDead {
get {
return !this.isStatic && !this.reference.IsAlive;
}
}
public Unit(Func callback) {
this.isStatic = callback.Target == null;
this.reference = new WeakReference(callback.Target);
this.method = callback.Method;
}
public bool Equals(Func callback) {
return this.reference.Target == callback.Target && this.method == callback.Method;
}
public void Invoke(object[] args) {
this.method.Invoke(this.reference.Target, args);
}
}
private List<Unit> list = new List<Unit>();
public int Count {
get {
return this.list.Count;
}
}
public void Add(Func callback) {
this.list.Add(new Unit(callback));
}
public void Remove(Func callback) {
for (int i = this.list.Count - 1; i > -1; i--) {
if (this.list[i].Equals(callback)) {
this.list.RemoveAt(i);
}
}
}
public void Invoke(TEventArgs args=null) {
ARGS[0] = args;
for (int i = this.list.Count - 1; i > -1; i--) {
if (this.list[i].IsDead) {
this.list.RemoveAt(i);
}
else {
this.list[i].Invoke(ARGS);
}
}
}
public void Clear() {
this.list.Clear();
}
}

  以上便是弱事件的实现代码了,其实原理与Caller基本一致。接下来是演示:

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
using System;
public class Obj {
public void Do(EventArgs e) {
Console.WriteLine("test");
}
public static void StaticDo(EventArgs e) {
Console.WriteLine("static");
}
public static void Main(string[] args) {
var a = new Obj();
var b = new Obj();
var weakEvent = new WeakEvent<EventArgs>();
weakEvent.Add(a.Do);
weakEvent.Add(b.Do);
weakEvent.Add(StaticDo);
weakEvent.Add((EventArgs e) => Console.WriteLine("lambda"));
a = null;
weakEvent.Remove(StaticDo);
GC.Collect();
weakEvent.Invoke();
Console.WriteLine(weakEvent.Count);
}
}

  输出结果为:

1
2
3
lambda
test
2

  以上分别演示了静态方法、实例方法、匿名方法,其中静态方法和匿名方法需要手动调用Remove将之移除,如演示一般那样匿名方法便无从回收了,这点需要注意。如此弱事件便完成了,当然它带来了一定的性能损耗,这是无可避免的。也并未经过长久实践的磨砺,可以说只是一个原型罢了。

后记

  类似这样的内存泄漏问题在开发过程中可有不少,尤其是有了GC的庇护下对此更为麻痹。一般需要定期使用专业工具进行检测,这也是优化的一环啊。