在C#中,使用 Delegate d = Object.Method; 的方式創建一個委托,在實現上,這個委托對象內部持有了源對象的一個強引用(System.Object),如果使用者恰好有特殊需求,比如“要求源對象一旦在其他任何地方都不再使用,應該被及時回收。”,那麼,一旦委托對象的生命期足夠長,由於委托內部的強引用存在,源對象的銷毀將被延遲,與使用者預期不符,可能會導致Bug等問題。
比如,這裡有一個簡單的測試對象:
1 class ClassForDeclTest2 {3 public string GetString() { return "call GetString"; }4 public int AddInt(int a, int b) { return a + b; }5 public void PrintString(string s) { Print(s); }6 } 這個簡單對象雖然沒有成員數據,但各個方法也都生命為非Static的,因為在這裡我要測試的是和對象綁定的委托,即 public static Delegate CreateDelegate(Type type, object target, string method); 方式創建的委托。另外,AddInt方法中用到了我自定義的一個函數Print,功能如其名。
下面這段代碼,演示的是強引用委托,和最直接的弱引用委托方式:
1 Action<string> a = new ClassForDeclTest().PrintString; 2 a("abc"); 3 GC.Collect(); 4 a("abc"); 5 6 WeakReference weakRef = new WeakReference(new ClassForDeclTest()); 7 a = (s) => { object o = weakRef.Target; if (o != null) Print(s); }; 8 a("def"); 9 GC.Collect();10 a("def"); 輸出是:
abcabcdef "def”只被輸出了一次,是因為,第6行用new ClassForDeclTest()創建的對象並沒有被專門的變量保存下來,所以第9行的GC.Collect()會將這個對象給回收掉,第10行的委托Invoke將判斷WeakReferece.Target為null,於是第10行不輸出任何內容。
我這篇文章的主要目的,就是要將6、7行的弱委托創建過程提取成一個專門的工具模塊。
第一次嘗試:
1 class WeakDelegate 2 { 3 public WeakDelegate(object o, string methodName) : 4 this(o, o.GetType().GetMethod(methodName)) 5 { 6 } 7 8 public WeakDelegate(object o, MethodInfo method) 9 {10 m_target = new WeakReference(o);11 m_method = method;12 }13 14 public object Invoke(params object[] args)15 {16 object target = m_target.Target;17 if (target != null) return m_method.Invoke(target, args);18 else return null;19 }20 21 private WeakReference m_target;22 private MethodInfo m_method;23 }24 25 WeakDelegate d = new WeakDelegate(new ClassForDeclTest(), "PrintString");26 d.Invoke("abc");27 GC.Collect();28 d.Invoke("abc"); 這種方法很簡單,只要事件發送方管理一個WeakDelegate的容器,就能很方便的使用弱委托。一個明顯的缺點是,使用這個WeakDelegate類,對事件發送方是侵入性的,如果發送方是系統類型,不能修改,比如按鈕事件 public event EventHandler Click; 要求事件響應者必須是形如 public delegate void EventHandler(Object sender, EventArgs e); 的委托,所以WeakDelegate不能滿足需要。
第二次嘗試:
1 class WeakDelegate 2 { 3 public WeakDelegate(object o, string methodName): 4 this(o, o.GetType().GetMethod(methodName)) 5 { 6 } 7 8 public WeakDelegate(object o, MethodInfo method) 9 {10 m_target = new WeakReference(o);11 m_method = method;12 }13 14 public Delegate ToDelegate()15 {16 ParameterExpression[] parExps = null;17 {18 ParameterInfo[] parInfos = m_method.GetParameters();19 parExps = new ParameterExpression[parInfos.Length];20 for (int i = 0; i < parExps.Length; ++i)21 {22 parExps[i] = Expression.Parameter(parInfos[i].ParameterType, "p" + i);23 }24 }25 26 Expression target = Expression.Field(Expression.Constant(this), GetType().GetField("m_target", BindingFlags.Instance | BindingFlags.NonPublic));27 target = Expression.Convert(Expression.Property(target, "Target"), m_method.ReflectedType);28 29 Expression body = 30 Expression.Condition(31 Expression.NotEqual(target, Expression.Constant(null)),32 Expression.Call(target, m_method, parExps),33 GetTypeDefaultExpression(m_method.ReturnType));34 35 return Expression.Lambda(body, parExps).Compile();36 }37 38 private static Expression GetTypeDefaultExpression(Type t)39 {40 if (t == typeof(void)) return Expression.Call(typeof(WeakDelegateHelper).GetMethod("EmptyFunc", BindingFlags.NonPublic | BindingFlags.Static));41 else if (t.IsClass) return Expression.Constant(null, t);42 else return Expression.Constant(t.InvokeMember(null, BindingFlags.CreateInstance, null, null, null));43 }44 45 private WeakReference m_target;46 private MethodInfo m_method;47 } 這個類的變化是,移除了Invoke方法,添加ToDelegate,後者的功能是根據WeakReference關聯的源對象方法,生成一個Func<>或者Action<>類型的委托。WeakReference對象調用ToDelegate生成的委托,可以被用於各種需要委托的場合,比如上面按鈕的Click事件響應。這個類的用法如:(Func<string>)new WeakDelegate(new ClassForDeclTest(), "GetString").ToDelegate(); 雖然也沒有用專門的變量來存儲WeakDelegate對象,但ToDelegate生成的委托中含有WeakDelegate對象的強引用(Expression.Constant(this)),而WeakDelegate內部又持有源對象的弱引用,故源對象的銷毀並不受影響,能夠達到目的。
測試如下:
1 // 輸出一個委托執行時間 2 public static void PerfTimer(Action f, params string[] name) 3 { 4 Assert(name.Length <= 1); 5 6 Stopwatch watch = new Stopwatch(); 7 watch.Start(); 8 f(); 9 watch.Stop();10 float seconds = (watch.ElapsedMilliseconds / 1000.0f);11 12 if (name.Length > 0) Print(name[0], ":", seconds);13 else Print(seconds);14 }15 16 // 性能測試幫助類型17 class ClassForPerfTest18 {19 public int N { get; set; }20 public void Inc() { N += 1; }21 }22 23 ...24 25 {26 Print("-------測試 : Func<string>-------");27 28 &nb