2.5 委托、事件、装箱、拆箱
2.5.1 委托与事件
使用过C或C++的读者对指针是什么应该很清楚,指针是一个需要谨慎对待的东西,它不仅可以指向变量的地址,还可以指向函数的地址,本质上,它是指向内存的地址。
在C#中万物皆是类,我们使用C#的大部分时间里都没有指针的身影,最多也只是引用,因为指针被封装在内部函数中了。不过回调函数却依然存在,于是C#多了一个委托(delegate)的概念,所有函数指针功能都以委托的方式来完成。委托可以视为一个更高级的函数指针,它不仅能把地址指向另一个函数,而且还能传递参数、获得返回值等多个信息。系统还会为委托对象自动生成同步、异步的调用方式,开发人员使用BeginInvoke()、EndInvoke()方法就可以避开Thread类,从而直接使用多线程调用。
那么委托(delegate)在C#中是如何实现的呢?我们来一探究竟。
首先不要错误地认为委托是一个语言的基本类型,我们在创建委托时,其实就是创建一个delegate类实例,这个delegate委托类继承了System.MulticastDelegate类,类实例里有BeginInvoke()、EndInvoke()、Invoke()这三个函数,分别表示异步开始调用、结束异步调用及直接调用。
但我们不能直接写一个类来继承System.MulticastDelegate类,因为它不允许被继承,它的父类Delegate也同样有这个规则,官方文档中就是这么定的一个规则,相关表述翻译后如下:
MulticastDelegate类是一个特殊的类,编译器或其他工具可以从它这里继承,但你不能直接继承它。Delegate类也有同样的规则。
Delegate类中有一个变量是用来存储函数地址的,当变量操作=(等号)时,把函数地址赋值给变量保存起来。不过这个存储函数地址的变量是一个可变数组,你可以认为它是一个链表,每次直接赋值时会换一个链表。
Delegate委托类还重写了+=、-=这两个操作符,其实就是对应MulticastDelegate类的Combine()和Remove()方法,当对函数进行+=和-=操作时,相当于把函数地址推入链表尾部,或者移出链表。
当委托被调用时,委托实例会把所有链表里的函数依次用传进来的参数调用一遍。官方文档中的表述翻译后如下:
MulticastDelegate类中有一个已经连接好的delegate列表,被称为调用列表,它由一个或者更多个元素组成。当一个multicast delegate被启动调用时,所有在调用列表里的delegate都会按照它们出现的顺序被调用。如果在执行列表期间遇到一个错误,就会立即抛出异常并停止调用。
看到这里我们彻底明白了,原来delegate关键字其实只是一个修饰用词,背后是由C#编译器来重写的代码,我们可以认为是编译时把delegate这一句换成了Delegate,从而变成一个class,它继承自System.MulticastDelegate类。
那么什么是event(事件),它和delegate又有什么关系?
event很简单,它在委托(delegate)上又做了一次封装,这次封装的意义是,限制用户直接操作delegate实例中变量的权限。
封装后,用户不能再直接用赋值(即使用=(等号)操作符)操作来改变委托变量,只能通过注册或者注销委托的方法来增减委托函数的数量。也就是说,被event声明的委托不再提供“=”操作符,但仍然有“+=”和“-=”操作符可供操作。
为什么要限制呢?因为在平时的编程中,项目太过庞大,经手的人员数量太多,导致我们无法得知其他人编写的代码是什么,以及有什么意图,这样公开的delegate会直接暴露在外,随时会被“=”赋值而清空前面累积起来的委托链表,委托的操作权限范围太大,导致问题会比较严重。声明event后,编译器内部重新封装了委托,让暴露在外面的委托不再担心有随时被清空和重置的危险。因为经过event封装后,不再提供赋值操作来清空前面的累加,只能一个个注册或者一个个注销委托(或者说函数地址),这样就保证了“谁注册就必须谁负责销毁”的目的,更好地维护了delegate的秩序。