zoukankan      html  css  js  c++  java
  • [C#]实现弱委托

       在C#中,使用 Delegate d = Object.Method; 的方式创建一个委托,在实现上,这个委托对象内部持有了源对象的一个强引用(System.Object),如果使用者恰好有特殊需求,比如“要求源对象一旦在其他任何地方都不再使用,应该被及时回收。”,那么,一旦委托对象的生命期足够长,由于委托内部的强引用存在,源对象的销毁将被延迟,与使用者预期不符,可能会导致Bug等问题。

      比如,这里有一个简单的测试对象: 

    1 class ClassForDeclTest
    2 {
    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) ((ClassForDeclTest)o).PrintString(s);  };
    8 a("def");
    9 GC.Collect();
    10 a("def");

      输出是: 

    abc
    abc
    def

      "def”只被输出了一次,是因为,第6行用new ClassForDeclTest()创建的对象并没有被专门的变量保存下来,所以第9行的GC.Collect()会将这个对象给回收掉,第10行的委托Invoke将判断WeakReferece.Targetnull,于是第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 ClassForPerfTest
    18 {
    19 public int N { get; set; }
    20 public void Inc() { N += 1; }
    21 }
    22
    23 ...
    24
    25 {
    26 Print("-------测试 : Func<string>-------");
    27
    28 Func<string> f = new ClassForDeclTest().GetString;
    29 Print(f());
    30 GC.Collect();
    31 Print(f());
    32
    33 f = (Func<string>)new WeakDelegate(new ClassForDeclTest(), "GetString").ToDelegate();
    34 Print(f());
    35 GC.Collect();
    36 Print(f());
    37 }
    38
    39 {
    40 Print("-------测试 : Func<int, int, int>-------");
    41
    42 Func<int, int, int> f = new ClassForDeclTest().AddInt;
    43 Print(f(1, 2));
    44 GC.Collect();
    45 Print(f(1, 2));
    46
    47 f = (Func<int, int, int>)new WeakDelegate(new ClassForDeclTest(), "AddInt").ToDelegate();
    48 Print(f(1, 2));
    49 GC.Collect();
    50 Print(f(1, 2));
    51 }
    52
    53 {
    54 Print("-------测试 : Action<string>-------");
    55
    56 string s = "法国人";
    57 Action<string> a = new ClassForDeclTest().PrintString;
    58 a(s);
    59 GC.Collect();
    60 a(s);
    61
    62 a = (Action<string>)new WeakDelegate(new ClassForDeclTest(), "PrintString").ToDelegate();
    63 a(s);
    64 GC.Collect();
    65 a(s);
    66 }
    67
    68 {
    69 Print("-------测试 : 性能-------");
    70
    71 const int LOOP_CNT = 1 << 25;
    72
    73 {
    74 ClassForPerfTest b = new ClassForPerfTest();
    75 PerfTimer(
    76 () =>
    77 {
    78 Action a = b.Inc;
    79 for (int i = 0; i < LOOP_CNT; ++i) a();
    80 });
    81 Print(b.N);
    82 }
    83
    84 {
    85 ClassForPerfTest b = new ClassForPerfTest();
    86 PerfTimer(
    87 () =>
    88 {
    89 Action a = (Action)new WeakDelegate(b, "Inc").ToDelegate();
    90 for (int i = 0; i < LOOP_CNT; ++i) a();
    91 });
    92 Print(b.N);
    93 }
    94 }

      输出:

    -------测试 : Func<string>-------
    call GetString
    call GetString
    call GetString
    null
    -------测试 : Func<int, int, int>-------
    3
    3
    3
    0
    -------测试 : Action<string>-------
    法国人
    法国人
    法国人
    -------测试 : 性能-------
    0.122
    33554432
    1.085
    33554432

      性能测试方面输出了累加结果,是想要避免Release版本中的编译器优化。上面性能测试输出的时间是在Release版本下得到的,在Debug下两者的差距更小。

       这个类我还想修改,因为ToDelegate的每次调用,都执行了一次表达式树编译到字节码的过程,如果ToDelegate调用频繁,开销会比较显著。我希望进一步的修改能够达到这样的效果:对每个要进行委托的方法(MethodInfo),只进行一次Compile操作。

      目标委托如下:

    1 (arg0, arg1, ...) =>
    2 {
    3 object o = weakRef.target;
    4 if (o != null) return method.invoke(o, arg0, arg1, ...);
    5 else return null;
    6 };

      方法调用会涉及三个组成:方法,this对象,参数表。观察上面这个委托,这三个组成中,方法本身,即MethodInfo,是和委托一一对应的,在一个编译好的委托中,方法地址是固定的,是静态的。虽然this对象和参数表都是动态的,但因为MethodInfo和委托的参数表完全相同,那唯一需要动态绑定的就只剩下this对象,即上面的WeakReference.Target。这个对象可以用闭包来实现。方案如下:

    1 Delegate ToDelegate(orginObj, method)
    2 {
    3 Func<WeakReference, Delegate> d =
    4 Compile(
    5 (weakRef) =>
    6 {
    7 (args)=>
    8 {
    9 object o = weakRef.Target;
    10 if (o != null) method.invoke(o, args);
    11 else ;
    12 };
    13 };
    14 )
    15 return d(new WeakReference(orginObj));
    16 }

      实现以上方案的第三种尝试:

    1 static class WeakDelegateHelper
    2 {
    3 private static Dictionary<MethodInfo, Func<Func<object>, Delegate>> g_delegateCache = new Dictionary<MethodInfo, Func<Func<object>, Delegate>>();
    4 private static void EmptyFunc() { }
    5 private static Expression GetTypeDefaultExpression(Type t)
    6 {
    7 if (t == typeof(void)) return Expression.Call(typeof(WeakDelegateHelper).GetMethod("EmptyFunc", BindingFlags.NonPublic | BindingFlags.Static));
    8 else if (t.IsClass) return Expression.Constant(null, t);
    9 else return Expression.Constant(t.InvokeMember(null, BindingFlags.CreateInstance, null, null, null));
    10 }
    11 private static Func<Func<object>, Delegate> GenerateDelegateImpl(MethodInfo methodInfo)
    12 {
    13 ParameterExpression[] parExps = null;
    14 {
    15 ParameterInfo[] parInfos = methodInfo.GetParameters();
    16 parExps = new ParameterExpression[parInfos.Length];
    17 for (int i = 0; i < parExps.Length; ++i)
    18 {
    19 parExps[i] = Expression.Parameter(parInfos[i].ParameterType, "p" + i);
    20 }
    21 }
    22
    23 ParameterExpression getObjExp = Expression.Parameter(typeof(Func<object>), "getObj");
    24 Expression getObjIvkExp = Expression.Convert(Expression.Invoke(getObjExp), methodInfo.ReflectedType);
    25
    26 Expression innerBody =
    27 Expression.Condition(
    28 Expression.NotEqual(getObjIvkExp, Expression.Constant(null)),
    29 Expression.Call(getObjIvkExp, methodInfo, parExps),
    30 GetTypeDefaultExpression(methodInfo.ReturnType));
    31
    32 LambdaExpression innerLambda = Expression.Lambda(innerBody, parExps);
    33
    34 Expression<Func<Func<object>, Delegate>> e = Expression.Lambda<Func<Func<object>, Delegate>>
    35 (innerLambda, getObjExp);
    36 return e.Compile();
    37 }
    38 public static Delegate CreateDelegate(object o, MethodInfo methodInfo)
    39 {
    40 /*
    41 (f) =>
    42 {
    43 (args)=>
    44 {
    45 object o = f();
    46 if (o != null) method.invoke(o, args);
    47 else ;
    48 };
    49 };
    50 */
    51 Func<Func<object>, Delegate> d;
    52 if (!g_delegateCache.TryGetValue(methodInfo, out d))
    53 {
    54 d = GenerateDelegateImpl(methodInfo);
    55 g_delegateCache.Add(methodInfo, d);
    56 }
    57 WeakReference weakRef = new WeakReference(o);
    58 return d(() => weakRef.Target);
    59 }
    60 public static Delegate CreateDelegate(object o, string methodName)
    61 {
    62 return CreateDelegate(o, o.GetType().GetMethod(methodName));
    63 }
    64 }

      在生成的委托单次调用上,方案3比方案2效率稍低,但方案3针对每一个MethodInfo只编译一次的行为,在平均性能上更优。

      另外,对于委托调用时源对象已经失效这种情况,也可以考虑调用设置的默认函数等,我上面的实现中,在这种情况下没有做任何事。

      因为我对C#远算不上熟悉,很可能走弯路了,希望看到文章的朋友们多多指点。其实这篇文章只是为了一点语法糖,避免重复编写弱委托,就写了这许多东西。最好有朋友跳出来指出,要实现这个功能只需要几行代码什么的。我更倾向于练手。

  • 相关阅读:
    如何在XSLT中实现勾选的效果
    6个jQuery图表Web应用扩展
    数据仓库(集市)之间共享维度表
    Json.NET
    jquery调用页面的方法
    使用javascript打开模态对话框
    使用ApplicationDeployment 类以编程方面对ClickOnce程序进行更新 【转载】
    XSLT 编译器 (xsltc.exe)
    在xslt中添加链接的方式
    哀悼
  • 原文地址:https://www.cnblogs.com/cbscan/p/2059899.html
Copyright © 2011-2022 走看看