说明

chap. 20 这里结束后应该就可以开发新功能了嘻嘻,这一部分花了挺长时间的…

事件处理大致分为: 声明委托、声明事件、订阅事件、触发事件

委托的声明

委托类似于 C/C++中的函数指针

委托,可以从字面意思来理解它——这儿有一件事情,但是我不去亲自完成它,而是把它交给其他对象去做。我不过是间接参与了这件事的完成。

在C#的类库中提供了大量的委托类型,举例以下,包括其泛型版本:

Action 委托

1
2
3
public delegate void Action();
// 泛型版本:
public delegate void Action<in T>(T obj); // 无返回值

这是Action委托,提示Action(void() target)

封装的方法必须对应于由此委托定义的方法签名,所以这意味着封装的方法必须没有任何参数,并且不能有返回值。

以下代码合法,且属于间接调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Program
{
static void Main(string[] args)
{
Calculator calculator = new Calculator();
Action action = new Action(calculator.Report);
action(); // 简写(函数指针式写法)完整形式为:action.Invoke();
}
}
class Calculator
{
public void Report()
{
Console.WriteLine("This is a method...");
}
}


Func<T>委托

1
public delegate TResult Func<in T,out TResult>(T arg);

这是Func<T>委托,我一看····哦! 是个泛型委托,总共可提供17个参数 (16 个类型参数(表示目标方法的参数类型),1个返回类型··

以上Calculator类添加方法:

1
2
3
4
5
6
7
8
public int Add(int a,int b)
{
return a + b;
}
public int Sub(int a, int b)
{
return a - b;
}

如果需要以委托调用Add与Sub方法,可以使用Func<T>委托

Main方法中添加以下代码合法:

1
2
3
4
5
6
7
8
Func<int, int, int> func = new Func<int, int, int>(calculator.Add);
Func<int, int, int> funcSub = new Func<int, int, int>(calculator.Sub);
int a = 20; int b = 30;
int result = 0;
result = func.Invoke(a,b);
Console.WriteLine(result);
result = funcSub.Invoke(a, b);
Console.WriteLine(result);

这里的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····嗯?难理解嘛?看下图 👇

回调(callback)方法(又称好莱坞方法、面试方法:

有选择的去调用一个方法, 使用时需要把委托类型的参数传入主调方法,此委托的参数被封装一个回调方法,

回调方法一般无返回值

委托的高级使用

  • 多播委托( multicast )

    • 属于同步调用
  • 隐式异步调用

    • 同步异步理解(中英文上的语言差异):

      • 同步:你做完了,我(在你的基础上)接着做

      • 异步:咱俩同时做

    • 同步调用和异步调用区别:

      • 每一个运行的程序就是一个进程(process)
      • 每个进程可以有多个线程(thread)(第一个线程即是主线程)
      • 同步调用在同一线程内

所以也可以这样去理解:串行==同步==单线程 ,并行==异步==多线程(异步的底层机理就是多线程

委托的调用有BeginInvoke()方法,这属于一个隐式异步调用

第一个参数是一个 AsyncCallback 委托,此委托引用在异步调用完成时要调用的方法。 第二个参数是一个用户定义的对象,该对象将信息传递到回调方法。 BeginInvoke 将立即返回,而不会等待异步调用完成。

异步调用中,多个线程的执行会抢占资源而导致冲突,为避免此冲突需要为线程加锁。

显式的异步调用可以使用thread | task

1
2
3
public class Task<TResult> : System.Threading.Tasks.Task

public sealed class Thread : System.Runtime.ConstrainedExecution.CriticalFinalizerObject

事件

我滴Mind Map,主要展示事件模型

事件不会自行发生,这对于订阅者来说就是一个工具,

事件的声明很简单,只需要再委托前加上event关键字,最后跟上事件名称即可.

命名约定:

如果一个委托是为声明事件准备的,根据.Net的约定,应以EventHandler作为委托的结尾。(用于存储事件处理器)。

如果一个类作为事件数据/参数传递,根据.Net的约定,应以EventArgs结尾。

用于触发事件的方法一般命名为 On + 事件名称 ,且其访问级别为 protected 而非 public

事件的订阅

事件只能作用于+=和-=操作符的左边(要么添加事件处理器,要么移除…)

使用new操作符创建委托实例,与事件关联,使用+=操作符添加事件处理器

订阅者【事件处理器】之间不会相互干扰

取消订阅事件,向事件注销:

将与事件关联的委托实例,使用-=操作符,删除事件处理器。

在事件内部中,编译器会把 +=-=操作符翻译成调用add或remove访问器,这也是其底层原理。

分析一下窗体设计器中生成的事件吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void button1_Click_1(object sender, EventArgs e)
{
// 这个是双击控件button1后自动创建的事件,默认存在一个引用,即订阅事件 由button1订阅 订阅代码生成在Form1.Designer.cs
}
// Designer source code :
this.button1.Click += new System.EventHandler(this.button1_Click_1);
// 这边使用了一个EventHandle委托, Microsoft Document说明如下:
public delegate void EventHandler(object? sender, EventArgs e);
//实际上也可以这么写(直接附上方法名):
this.button1.Click += this.button1_Click_1;
/*
'sender'Object:事件源
'e'EventArgs:不包含事件数据的对象
继承 Object -> Delegate -> EventHandle
*/

// 还有其他的订阅方式 挂接事件处理
this.button1.Click += delegate(object sender,EventArgs e)
{
this.textBox1.text="hah,这其实就是一个 匿名委托 ";
};
// 不过上面这个写法已经过时了 , 取代的是使用lambda表达式的写法
this.button1.Click += (object sender,EventArgs e) =>{
this.textBox1.Text = "应该使用lambda表达式写";
};
// 甚至可以这么写
this.button1.Click += (sender,e)=>{
this.textBox1.Text = "hah";
}; // 编译器将根据委托的约束自动判断事件参数是何种数据类型

详解

2020 / 12 / 8 更新

以上直接使用委托与 event 关键字、+= -=操作符的做法属于事件的简略声明(也叫字段式声明)

实际可以直接通过委托字段来进行订阅事件这一环节。event关键字的存在让程序的逻辑更加安全,其本身相当于委托的包装器,该包装器对委托字段的访问起到限制作用,相当于一个蒙版,它对外界隐藏了委托实例的大部分功能(Invoke、MethodInfo、ObjectInfo等)。仅暴露出订阅(+=)和去除订阅(-=)的功能,保护了委托不被人滥用。 —————— 相当于OOP中**封装(encapsulation)**的概念,其一个重要的功能就是隐藏。

事件和委托的关系

事件不是“以特殊方式声明的委托实例/字段”

为什么要使用委托类型来声明事件:

  • 从Source的角度来看的话:为了表明Souce能对外传递哪些消息
  • 从Subscriber的角度来看:这是一种约定,为了约束能使用什么样签名的方法来处理事件
  • 委托类型的实例将用于存储(或引用)事件处理器
事件与属性的关系

属性不是字段,多数时候属性是字段的包装器,用来保护字段不会被滥用。

事件不是委托字段,且事件是委托字段的包装器,其保护委托字段不被滥用。

包装器永远不会被包装。

总结

  • 委托最好声明在命名空间体内
  • 委托定义,目标方法必须与委托保证类型兼容
  • 适时的使用接口来取代对委托的使用