[CLR]委托 Delegate 回调函数

[CLR]委托 Delegate 回调函数

非托管程序回调函数

在非托管c/c++中非成员函数值是一个内存地址,这个地址不携带任何信息比如函数的参数 返回值。参数类型。

托管程序回调函数-委托

委托就是c#内的回调函数,它是类型安全的
能定义类的地方都能定义委托。

包装器

委托对象是回调方法的一个包装器(wrapper),当一个方法绑定一个委托时,允许引用类型的协变性(方法能返回冲委托类型派生的一个类型)和逆变性(方法可以获取的参数可以是委托参数类型的基类),

协变性实例:delegate Object MyCallback(FileStream s) -> String SomeMethod(Stream s) 错误示例delegate Object MyCallback(FileStream s) -> int32 SomeMethod(Stream s)//因为int32是值类型,不允许逆变性/协变性,值类型to引用类型数据结构会发生变化。而引用类型,只是指针变了。

包装器原理 :C#编译器知道要构造的是委托,所以会分析源代码来确定引用的是哪个对象和方法。对象引用被传给构造器的object参数,标识了方法的一个特殊IntPtr值(从MethodDef或MemberRef元数据token获得)被传给构造器的method参数。对于静态方法,会为object参数传递null值。在构造器内部,这两个实参分别保存在_target和_methodPtr私有字段

如果一个静态方法,并方法定义为私有,它可以通过委托对象传递到其它类中调用。

声明委托类型,它的实例引用一个方法。
该方法读取一个int32参数,返回void
internal delegate void Feedback(int32 value);

用委托回调静态方法

new Feedback(Propram.FeedBackToConsole);

private static void staticDelegateDemo()
{
    Counter(1, 3, new Feedback(Propram.FeedBackToConsole));
}

用委托回调实例方法

在执行实例委托是,新购置的对象P的地址将作为隐式的this参数传给这个实例方法。
包装实例方法非常有用,因为对象内部的代码可以访问对象的实例成员。可以维护一些状态,并在回调方法执行期间利用这些状态信息。
new Feedback(p.FeedBackToFile);

private static void InstaneDelegateDemo()
{
    Program p=new Program();
    Counter(1, 3, new Feedback(p.FeedBackToFile));
}
private static void Counter (Int32 from, Int32 to, Feedback fb) {
    for (Int32 val = from; val <= to; val++) 
    {
        //如果指定了任何回调,就调用它们
        if (fb != null) 
        {
            //其实,完全可以修显式调用Invoke方法fb.Invoke(val); 
            fb (val); 
        }
    }
}

这段代码看上去像是调用了一个名为fb的函数,并向它传递一个参数(val)。但事实上,这里没有名为fh的函数。再次提醒你注意,因为编译器知道fb是引用了一个委托对象的变量,所以会生成代码调用该委托对象的Invoke方法

委托的执行与调用 (同上)
delegateObj(val); 
delegateObj.Invoke(val)

委托的异步执行 BeginInvoke

本质上是调用了 ThreadPool.QueueUseWorkItem;

Net中定义了常用的泛型委托,他们分别是17个action委托以及17个fun 委托。如果需要ref/out 泛型约束或者可变量的参数(params)就得自己去定义委托了。

委托原理

委托最终会编译曾class 它继承于MulticastDelegate。
System.MulticastDelegate :System.Delegate 所有委托的基类
System.Delegate//里面有两个静态方法combine remove

internal delegate void Feedback(int32 value);

internal class void Feedback:System.MulticastDelegate {
    //构造器
    public Feedback(Ojbect object,intPtr method);
    //这个方法和源代码指定的原型一样。
    public virtual void Invoke (Int32 value);
    //以下方法实现了对回调方法的异步回调
    public virtual IAsyncResult BeginInvoke (Int32 value, Asynccallback callback, Object object);
    public virtual void EndInvoke (IAsyncResult result);

}

构造器:public Feedback(Ojbect object,intPtr method);

注意,所有委托都有一个构造器,它要获取两个参数:一个是对象引用,另一个是引用回调方法的一个整数。然而,如果仔细查看前面的源代码,会发现传递的是Program.FeedbackToConsole 或 p.FeedbackToFile这样的值。根据迄今为止学到的编程知识,似乎没有可能通过编译!然而,C#编译器知道要构造的是委托,所以会分析源代码来确定引用的是哪个对象和方法。对象引用被传给构造器的object参数,标识了方法的一个特殊IntPtr值(从 MethodDef或MemberRef元数据token获得)被传给构造器的method参数。对于静态方法,会为object参数传递null值。在构造器内部,这两个实参分别保存在_target和_methodPtr私有字段中。除此以外,构造器还将_invocationList字段设为null,对这个字段的讨论将推迟到17.5节“用委托回调许多方法(委托链)”进行。

常用字段
_target 保存实例方法“this”的值,静态方法为null
_methodPtr 回调方法地址
_invocationList 该字段通常为null构造委托链时,它是委托链数组。
以及属性
Target 返回_target
Method 把内部有转换机制可以将—_methodPtr转换成System.Reflection.MethodInfo 对象并返回。

委托链操作combine/remove +=/-=

GetInvocationList 此方法返回此多播委托的Invocation列表。

为方便C#开发人员,C#编译器自动为委托类型的实例重载了+=和-=操作符。Delegate.Combine和Delegate.Remove也是使用这些运算符,可简化委托链的构造。

System.Delegate.combine

会拼接两个委托并返回新产生的委托,并且新委托的_invocationList是所有委托的引用集合。连续System.Delegate.combine拼接会产生多个委托。它们最终会在垃圾回收时候被回收掉。

委托链构建1
var c = System.Delegate.combine(a, b) ;
c = System.Delegate.combine(c, d);  此处又参数一个新的对象赋值给C,原来c对应的委托链对象最终会在垃圾回收时候被回收掉。
委托链构建2
Feedback delegate = null;
Feedback += new Feedback(p.FeedBackToFile); // +-符号重写

同样地, Combine方法发现fbChain已经引用了一个委托对象,因而又构造一个新的委托对象,和前面一样,这个新的委托对象对私有字段_target和_methodPtr进行初始化,具体的值就目前来说并不重要。_invocationList字段被初始化为引用一个委托对象数组。该数组的第一个元素和第二个元素(索引0和1)被初始化为引用fb1和fb2所引用的委托。数组的第三个元素(索引2)被初始化为引用包装了FeedbackToFile方法的委托(这是fb3所引用的委托)。最后, fbChain被设为引用这个新建的委托对象。注意,之前新建的委托及其_invocationList字段引用的数组现在可以进行垃圾回收

lambda 拉姆达最终会在对应的类中生存匿名函数,这个函数有可能是静态的也有可能是非静态的,据说是取决于拉姆达是否有访问到实例属性。

语法糖

  1. 不需要创建委托对象直接传入方法案例1(不需要委托对象包装类)
internal sealed class AClass
{
    public static void CallbackWithoutNewingADelegateObject ()
    {
        ThreadPool. QueueUserworkItem (SomeAsyncTask, 5);//这里,ThreadPool类的静态方法QueueUserWorkItem期望接收对一个WaitCallback委托对象的引用,委托对象中包装的是对SomeAsyncTask方法的一个引用。由于C#编译器能够自己进行推断,所以可以省略构造WaitCallback委托对象的代码,
    }
    private static void SomeAsyncTask (Object o) {
        Console.writeLine (o)
    }
}
  1. 不需要定义回调方法
internal sealed class AClass 
{
    public static void callbackwithoutNewingADelegateObject () 
    {
    ThreadPool.QueueUserWorkItem ( obj => Console.WriteLine (obj), 5); // 1. 编译器看到这个lambda表达式之后,会在类(本例是AClass)中自动定义一个新的私有方法。这个新方法称为匿名函数(anonymous function) 
    2 .由于C#编译器能够自己进行推断,所以可以省略构造WaitCallback委托对象的代码,
    }
}

//最终编译器
internal sealed class AClass1
{
    //创建该私有字段的目的是缓存委托对象,11优点:每次调用时, CallbackWithoutNewingADelegateobject不会新建一个对象1/缺点:缓存的对象永远不会被垃圾回收
    [CompilerGenerated]
    private static Naitcallback <>9__cachedAnonymousMethodDelegatel;
    public static void CallbackwithoutNewingADelegateObject () 
    {
         if (<>9CachedAnonymousMethodDelegatel a= null) 
        {
            //11第一次调用时,创建委托对象,并缓存它
            <>9__cachedAnonymousMethodDelegate1 =new Waitcallback (<CallbackwithoutNewingADelegateObject>b_D): 
            ThreadPool.QueueUserWorkitem (<>9__CachedAnonymousMethodDelegatel, 5):
        }
    }
    
    [CompilerGenerated]
    private static void <CallbackwithoutNewingADelegateObject>b_0 (Object obj)
    {
        Console.WriteLine (obj): 
    }
}
  1. 局部变量不需要手动包装到类中即可传给回调方法

前面展示了回调代码如何引用类中定义的其他成员。但有的时候,还希望回调代码引用存在于定义方法中的局部参数或变量。

 class AClass
 {
     public void callbackwithoutNewingADelegateObject()
     {
         var msg = "hello";//此变量被回调方法引用,注意此时lamada不会变成Aclass的匿名方法,因为访问了局部变量,所以编译器会创建一个类,回调方法作为此类的成员方法。局部变量也会变成该类的成员供回调方法访问.
         ThreadPool.QueueUserWorkItem(obj => {
             Console.WriteLine(msg);
         }, null);
     }
 }

 //最终编译
 [CompilerGenerated]
 private sealed class <>c_DisplayClass2 : Object1
 {
     // 回调代码要使用的每个局部变量都有一个对应的公共字段,
     public string msg:
     // 公共无参构造器
     public <>cDisplayClass2 [)
     // 包含回调代码的公共实例方法
     public void <UsingLocalVariablesInTheCallbackCode>b_o (Object obj) 
     {
         Console.WriteLine(msg);
     }
}

反射

var delegateType = Type.GetTpye("DelegateName");
MethodInfo methodInfo = typeof (Program).getMethod("methodName");
var d = Delegate.CreateDelegate(delegateType, methodInfo);
Object result = d.Dynamicinvoke(callbackArgs);

总结

  1. 方法入参需要一个委托时,可以直接传入一个具有相同签名的方法/或者拉姆达表达式作为参数传入,如果传入的是方法,编译器会自动添加委托包装类。如果传入的是拉姆达表达式,编译器会先生成一个方法,然后在把此方法包装成委托类对应的实例。

  2. 关于 编译器自动包装委托类对象时的几点。
    拉姆达编译器会生成方法。

    1. 如果拉姆达内访问里成员函数编译器会以包装实例方法的方式进行包装,
    2. 如果没访问对象成员编译器会以静态方法形式进行包装。
    3. 如果访问里局部变量,编译器会为其创建一个class 并将局部变量作为此class的成员属性供回调方法访问,回调方法同时也是此类的成员。然后以包装实例方法的形式进行包装
  3. combine/remove 、+= /-=的过程会返回一个新的委托包装类,原有的会在GC时会回收掉。

posted @ 2022-02-22 09:13  一身大膘  阅读(539)  评论(0编辑  收藏  举报