说明
chap. 20 这里结束后应该就可以开发新功能了嘻嘻,这一部分花了挺长时间的…
事件处理大致分为: 声明委托、声明事件、订阅事件、触发事件
委托的声明
委托类似于 C/C++中的函数指针
委托,可以从字面意思来理解它——这儿有一件事情,但是我不去亲自完成它,而是把它交给其他对象去做。我不过是间接参与了这件事的完成。
在C#的类库中提供了大量的委托类型,举例以下,包括其泛型版本:
Action 委托
1 | public delegate void Action(); |
这是Action委托,提示Action(void() target)
封装的方法必须对应于由此委托定义的方法签名,所以这意味着封装的方法必须没有任何参数,并且不能有返回值。
以下代码合法,且属于间接调用
1 | class Program |
Func<T>委托
1 | public delegate TResult Func<in T,out TResult>(T arg); |
这是Func<T>委托,我一看····哦! 是个泛型委托,总共可提供17个参数 (16 个类型参数(表示目标方法的参数类型),1个返回类型··
以上Calculator类添加方法:
1 | public int Add(int a,int b) |
如果需要以委托调用Add与Sub方法,可以使用Func<T>委托
Main方法中添加以下代码合法:
1 | Func<int, int, int> func = new Func<int, int, int>(calculator.Add); |
这里的Func<T>委托如 :
1 | Func<int, int, int> func = new Func<int, int, int>(calculator.Add); |
即表示获取三个参数,目标方法提供2个int参数值,且返回值也为int类型,所以与前面写到的Add与Sub方法能保持类型兼容。(签名并不需要完全一致
··· 和Action委托很直观的区别就是有无返回值
自定义委托
委托属于一种类(class 引用类型) ,声明在命名空间体中(虽然可以以嵌套的方式声明在其他类中·····调用的时候需要附带上层类名),使用delegate
关键字声明委托类型,例如
1 | public delegate int myDelegat(int x,int y); |
像这一个委托,其要求目标方法提供2个int参数值,且返回类型也为int,这即是我们对目标方法的类型约束。
调用委托的方法和调用方法的语法一致,属于间接调用。
委托的一般使用
把方法作为参数传递给另外一个方法
作为模板方法:
委托具有返回值,一般把委托作为方法参数传入方法,另一方法间接调用被委托封装的方法。即“另一个方法” “借用”指定的外部方法来产生结果。(👋 Hey····嗯?难理解嘛?看下图 👇
有选择的去调用一个方法, 使用时需要把委托类型的参数传入主调方法,此委托的参数被封装一个回调方法,
回调方法一般无返回值
委托的高级使用
多播委托( multicast )
- 属于同步调用
隐式异步调用
同步异步理解(中英文上的语言差异):
同步:你做完了,我(在你的基础上)接着做
异步:咱俩同时做
同步调用和异步调用区别:
- 每一个运行的程序就是一个进程(process)
- 每个进程可以有多个线程(thread)(第一个线程即是主线程)
- 同步调用在同一线程内
所以也可以这样去理解:串行==同步==单线程 ,并行==异步==多线程(异步的底层机理就是多线程)
委托的调用有BeginInvoke()方法,这属于一个隐式异步调用,
第一个参数是一个 AsyncCallback 委托,此委托引用在异步调用完成时要调用的方法。 第二个参数是一个用户定义的对象,该对象将信息传递到回调方法。
BeginInvoke
将立即返回,而不会等待异步调用完成。
异步调用中,多个线程的执行会抢占资源而导致冲突,为避免此冲突需要为线程加锁。
显式的异步调用可以使用thread | task
1 | public class Task<TResult> : System.Threading.Tasks.Task |
事件
事件不会自行发生,这对于订阅者来说就是一个工具,
事件的声明很简单,只需要再委托前加上event
关键字,最后跟上事件名称即可.
命名约定:
如果一个委托是为声明事件准备的,根据.Net的约定,应以EventHandler
作为委托的结尾。(用于存储事件处理器)。
如果一个类作为事件数据/参数传递,根据.Net的约定,应以EventArgs
结尾。
用于触发事件的方法一般命名为 On + 事件名称
,且其访问级别为 protected
而非 public
事件的订阅
事件只能作用于+=和-=操作符的左边(要么添加事件处理器,要么移除…)
使用new操作符创建委托实例,与事件关联,使用+=
操作符添加事件处理器
订阅者【事件处理器】之间不会相互干扰
取消订阅事件,向事件注销:
将与事件关联的委托实例,使用-=
操作符,删除事件处理器。
在事件内部中,编译器会把 +=
和-=
操作符翻译成调用add或remove访问器,这也是其底层原理。
分析一下窗体设计器中生成的事件吧:
1 | private void button1_Click_1(object sender, EventArgs e) |
详解
2020 / 12 / 8 更新
以上直接使用委托与 event 关键字、+= -=操作符的做法属于事件的简略声明(也叫字段式声明)
实际可以直接通过委托字段来进行订阅事件这一环节。event关键字的存在让程序的逻辑更加安全,其本身相当于委托的包装器
,该包装器对委托字段的访问起到限制作用
,相当于一个蒙版,它对外界隐藏了委托实例的大部分功能(Invoke、MethodInfo、ObjectInfo等)。仅暴露出订阅(+=)和去除订阅(-=)的功能,保护了委托不被人滥用。 —————— 相当于OOP中**封装(encapsulation)**的概念,其一个重要的功能就是隐藏。
事件和委托的关系
事件不是“以特殊方式声明的委托实例/字段”
为什么要使用委托类型来声明事件:
- 从Source的角度来看的话:为了表明Souce能对外传递哪些消息
- 从Subscriber的角度来看:这是一种约定,为了约束能使用什么样签名的方法来处理事件
- 委托类型的实例将用于存储(或引用)事件处理器
事件与属性的关系
属性不是字段,多数时候属性是字段的包装器,用来保护字段不会被滥用。
事件不是委托字段,且事件是委托字段的包装器,其保护委托字段不被滥用。
包装器永远不会被包装。
总结
- 委托最好声明在命名空间体内
- 委托定义,目标方法必须与委托保证类型兼容
- 适时的使用接口来取代对委托的使用