.Net 委托探秘

前言

探秘委托之前,让我们先了解一下回调函数。回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

.Net 中通过委托提供了回调函数的机制。


一、委托

在 .Net中,委托不仅仅是回调函数机制。委托确保回调方法是类型安全的,因为委托只允许传入兼容于委托定义的方法。委托还允许顺序调用多个方法(多播委托),并支持调用静态方法和实例方法。

在 C# 用delegate关键字定义一个委托,并且要指定一个回调方法签名:

// 声明一个委托类型,回调方法的签名:一个 string 类型的参数,它的实例引用一个方法
internal delegate void Print(string message);

委托对象是方法的包装器,使方法能通过包装器来间接回调。包装器中包装了一个方法和调用该方法时要操作的对象。

理解:委托是一个可以指向方法的类型,调用委托变量时,执行的就是变量所指向的方法

在构造委托对象的实例时,编译器会确保传入的方法的签名兼容于委托定义的签名。

public class TestStatic
{
    public static void Call(string name)
    {
        System.Console.WriteLine($"呼叫{name}");
    }
}

public class TestInstance
{
    public void Call(string name)
    {
        System.Console.WriteLine($"呼叫{name}");
    }
}

委托同样使用 new 操作符构造委托实例,调用方式就像调用方法一样:

var print = new Print(TestStatic.Call);  // 传入一个兼容 Print 委托的静态方法
print("张三");  // 调用委托 ==> 调用委托变量所指向的方法 Call

1. 原理揭秘

反编译代码,可以看到委托被编译器定义成了一个完整的类:
在这里插入图片描述
代码结构类似这样(实际不能这样定义):

internal class Print : System.MulticastDelegate
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="object">对象的引用</param>
    /// <param name="method">引用了回调方法的整数</param>
    public Print(object @object, IntPtr method)
    {
        this._target = @object;
        this._methodPtr= method;
        this._invocationList =  null;
    }

    // Invoke方法的签名和委托的签名匹配
    public virtual void Invoke(string message)
    {
    }

    // BeginInvoke、EndInvoke方法实现对回调方法的异步调用
    public virtual IAsyncResult BeginInvoke(string message, AsyncCallback callback, object @object)
    {
    }

    public virtual void EndInvoke(IAsyncResult result)
    {
    }
}

所有的委托都有一个构造器,它们都如上所示构造器取两个参数:一个是对象引用,一个是引用了回调方法的整数。

因为所有的委托类型都派生自System.MulticastDelegateMulticastDelegate又派生自Delegate,所以委托都继承了MulticastDelegate的字段、属性和方法。 其中三个重要的非公共字段_target_methodPtr_invocationList。就像上面代码演示的那样:

字段类型说明
_targetSystem.Object1. 当委托对象包装一个静态方法时,这个字段为 null
2. 当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象
_methodPtrSystem.IntPtr一个内部的整数值,CLR 用它标识要回调的方法
_targetSystem.Object通常为 null,构造委托链时将引用一个委托数组

C#编译器知道要构造的是委托,所以会分析源代码来确定引用的是哪个对象和方法。对象引用被传给构造器的 object 参数,标识了方法的一个特殊 IntPtr 值(从 MethodDef 或 MethodRef 元数据 token 获得)被传给构造器的 method 参数。

包装静态方法和实例方法的委托对象如下:
在这里插入图片描述
在回到上面案例:

var print = new Print(TestStatic.Call);
print("张三");  // 也可以直接 print.Invoke("张三");

编译器知道 print 是引用了委托对象的变量,所以会生成代码调用 Print 实例的 Invoke 方法(Invoke 方法的签名和委托的签名匹配),就像这样:

print.Invoke("张三");

2. 委托链

委托链是委托对象的集合,可利用委托链调用集合中的委托所代表的的全部方法。Delegate 类的公共静态方法 Combine 可以将委托添加到链中:

var d1 = new Print(TestStatic.Call);

TestInstance instance1 = new TestInstance();
var d2 = new Print(instance1.Call);

TestInstance instance2 = new TestInstance();
var d3 = new Print(instance2.Call);

Delegate? delegates = Delegate.Combine(d1);                 // Combine1
delegates = Delegate.Combine(delegates, d2);                // Combine2
delegates = Delegate.Combine(delegates, d3);                // Combine3

如上面,执行 Combine1 时,Combine 方法发现试图合并的是 null 和委托 d1,在内部 Combine 会直接返回 d1 中的值,这时 delegates 变量同样指向 d1 所指向的委托对象。

执行 Combine2 时,Combine 内部发现 delegates 已经引用了一个委托对象,Combine 会构造一个新的委托对象,新委托对象会默认初始化_target_methodPtr(内部初始化值,这里不用考虑),这时_invocationList字段不在为 null,而是被初始化为引用一个委托对象数组。数组的第0、1位元素分别被初始化为引用包装方法的委托 d1、d2。最后 delegates 变量指向新构造的委托对象
在这里插入图片描述
指行 Combine3 时,同上面类似,Combine 同样会构造一个新的委托对象,并对新的委托对象进行初始化。_invocationList同样是被初始化为引用一个委托对象数组,并把数组中的元素指向对应的委托对象,然后 delegates 变量重新指向新构造的委托对象。最后之前 Combine2 新建的委托以及其_invocationList字段引用的数组可以进行垃圾回收

可以加向委托链添加委托,同样的也可以调用 Delegate 的公共静态方法 Remove 从链中删除一个委托:

delegates = Delegate.Remove(delegates, d1);  // 从 delegates 委托 _invocationList 数组中删除委托 d1

Remove 方法内部会扫描 delegates 委托对象内部维护的委托数组(从末尾向索引0扫描),然后进行删除。删除委托后有几种情况:

  1. 委托数组中任然有多个委托,这时候构建一个新的委托,并初始化委托数组
  2. 委托数组中只有一个委托,则直接返回唯一委托的引用(把 delegates 指向这个委托)
  3. 一个委托也不剩,返回 null

委托链的调用有其局限性。如果是有返回值的委托,委托链的调用如下(伪代码):

internal delegate string  Print(string message);  // 有返回值的委托
public string Invoke(string message)
{
    string result;
    Delegate[] delegateSet = _invocationList as Delegate[];
    if (delegateSet != null)      // 是委托链
    {
        foreach (Print p in delegateSet)
            result = p(message);  // 遍历调用每个委托
    }
    else                          // 不是委托链
    {
        result = _methodPtr.Invoke(_target, message);
    }
    // 当然可能还有一些非空判断
    return result;
}

委托链调用时,数组中每个委托都会被调用,并把结果保存到 result 变量中。循环结束后只包含最后一次调用的结果,然后把最后一次调用的结果返回给 Invoke 调用者。

局限不仅仅是其他返回结果会被丢弃,因为委托数组是遍历顺序调用,那么如果有一个委托抛异常或者阻塞,将会影响后续所有委托对象的调用。

GetInvocationList

MulticastDelegate 类以及其基类 Delegate 提供了实例方法 GetInvocationList 可以用于显示获取委托链中的每个委托,你可以自己定义每个委托的调用方式。
GetInvocationList 内部会构造并初始化一个 Delegate 组,数组每一个元素都引用链中的一个委托,最后返回对该数组的引用。

if (delegates != null)
{
   Delegate[] delegateSet = delegates.GetInvocationList();
   foreach (Print p in delegateSet)
   {
       p("张三");
   }
}

非必要建议直接使用 Microsoft 定义好的委托FuncAction

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Qanx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值