委托delegate是一种依赖倒置的调用机制
假设我们有两个对象a和b,如果对象a需要调用对象b的一个函数func,那么对象a就需要取得对象b的引用,才能调用到对象b的函数func。也可以说成是,对象a提供了逻辑运行的时机,对象b提供了逻辑运行的内容。这里的依赖关系是a依赖b。
同样的需求:对象a提供逻辑运行的时机,对象b提供逻辑运行的内容。通过委托的形式,我们就可以更改这种依赖关系。即对象a申明一个委托的实例delegate,对象b取得对象a的引用,然后将对象b的函数func注册到对象a的委托实例delegate。这里的依赖关系是b依赖于a。
所以我认为使用委托的初衷之一应该是有这种依赖需要倒置的地方。
谁依赖谁不好理解的话,打个比方就是:你女朋友依赖于你,则可以翻译为你没有你女朋友可以活,你女朋友没有你则不可以活。虽然感情方面就是互相依赖的,但在程序中互相依赖意味着这可能不是一个好的设计。
委托对垃圾回收的影响
我们知道GC是一种垃圾回收的机制,堆里面的数据如果没有栈上的引用指向它,则这个数据则被标记为垃圾,GC会在合适的时候将它清理出内存。那么,以上面的例子来说:对象b的函数func注册到对象a的委托实例delegate,会怎么影响对象a和对象b的垃圾回收呢?
在这里a是发布方,b是监听方。只要监听方的函数注册到了某个委托中,事实上它就在栈上多了一个引用。也就是说,本来监听方b该被垃圾回收的时候监听方b是不会被GC回收的,他会继续占用内存。
只有两种情况下监听方b会被垃圾回收(前提是b在栈上已经没有了其他引用):
- 监听方b所注册的发布方a也被垃圾回收之后。(皮之不存毛将焉附)
- 注销了监听方b在发布方a那边注册的函数。
因此我们知道了,也只有这种时候才需要做注销函数的事情,如果直接销毁的是发布方a,那么是不需要注销 注册在发布方a上的函数的。而且,通常这也是无法做到的,往往注册的函数都是监听方内部private的,发布方也拿不到这些函数。当然,如果你一定要在发布方这边注销函数,也是能做到的,只是这种需求不常见,如何做到看下一节。
注销所有注册的函数
所有的委托类型都继承于System.Delegate,System.Delegate有一个实例方法GetInvocationList,得到的返回值是System.Delegate的数组。遍历这个数组,将遍历取得的元素as为我们申明的委托类型,是可以注销的(-=)。遍历的顺序是我们注册进这个委托实例的顺序。
还有,如果委托类型是具有返回值的,我们直接调用委托得到的是最后一个注册函数的返回值。如果要得到所有注册函数的返回值,也可以通过这个数组来得到。
委托作为参数传递 和 匿名函数
委托还使得我们可以将某类函数(也许注册了一个或多个函数)作为实参传递。
但在实参传递的时候,事实上传递的是一个这个委托的注册函数的展开,也就是说传递过去的实参是无法在函数中再次注册其他内容的。只有在标注了ref实参传递关键字之后,这样才是传递的委托实例本身而不是其展开,这样才能在实参所在的函数中注册其他的内容。以上两点总结其来就是说,委托实例作为普通实参传递时,只能当作方法Invok或者出现在+= -=的右边;委托实例作为ref实参传递时,它当然也能Invoke,并且也能出现在+= -=的两边。
如果某个委托实例注册的是一个实例的函数,那么该实例的引用计数则+1,也就是说如果想GC回收掉这个实例函数所在的对象,那么就得解除这段注册关系。事实上,注册的内容如果涉及到了一个实例的成员(函数,字段等),这个实例的引用计数都会+1。
如果函数注册的是匿名函数,这个注册在委托里的匿名函数将会以闭包的形式,存在内存空间上。这个匿名函数就不再是某个实例的函数了,所以也不会影响该实例的生命周期。除非,匿名函数中使用了该实例的某个成员,这种情况下就与上一段描述的情况一致了。如果想GC回收掉这个成员所在的实例对象,那么就得解除这段注册关系。
委托和事件是一个东西吗?
private delegate void MyDelegate(int n);
private event MyDelegate delegate0;
private MyDelegate delegate1;
这里的MyDelegate是一个委托类型,即只有一个参数int类型并且没有返回值,只要是具备同样形参和返回值的函数都可以注册到这个委托类型的某个实例当中。delegate0和delegate1都是这个委托类型的实例。前者为事件后者为委托。event关键字我认为其实理解为一种访问修饰符,event关键字限制了非申明这个实例的类型的外部的访问权,即只允许外部注册和注销函数,不开放其他成员变量与函数。不加event关键字则没有这种访问权限制。
我感觉这与中文委托与事件所表达的含义也是非常贴切的,比如说你委托我做一件事,OK交给我了,但是也许这件事我也是找我朋友帮我做的,但是你并不需要了解这个,你只需要了解到这件事在合适的时间完成了就好。而你让我通知你一件事,OK,那我会在合适的时间通知你,我不会让我朋友来通知你,因为你并不认识他。
c#内置有常用的几种委托类型,Action、Func、Predicate。