C#中的委托(delegate)与事件(event)
委托
.NET团队之初想要实现一种用于任何后期绑定算法的引用类型,也就是想要一种可用于对方法引用的一种结构,同时又希望它能支持单播和多播,于是委托诞生了。
多播指的将多个方法调用链接在一起调用,就像一个列表一样 单播指的是单一方法的调用,其实可以认为单播是多播的一种特例
委托是.NET 1.0版本的一部分(在泛型出现之前),从功能上说,委托就是一种对方法的引用,类似于函数指针,或者说,委托是对方法的一种分类引用,相同参数类型和返回类型的方法视为一类,可被同一种类型的委托引用。
delegate
声明委托使用delegate关键字,例如:
//无返回值委托类型 public delegate void EventHandler(object? sender, EventArgs e); //有返回值委托类型 public delegate Assembly? ResolveEventHandler(object? sender, ResolveEventArgs args);
委托还可与泛型一起使用,例如使用很多的返回值的Action委托和有返回值的Func委托:
//无返回值的Action委托 public delegate void Action(); public delegate void Action<in T>(T arg); public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2); //有返回值的Func委托 public delegate TResult Func<out TResult>(); public delegate TResult Func<in T1, out TResult>(T1 arg); public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
上面Action委托和Func 委托的变体可包含多达 16 个输入参数,所以一般时候我们都不用自定义委托,当然如果需要,我们可以自行使用delegate关键字去声明委托来满足我们特殊的需求。
委托是一种类型,就像class关键字声明的是类类型,而delegate关键字声明的是委托类型,同样我们可以实例化委托类型:
class Program { public int Multiply(int a, int b) { return a * b; } public static int Plus(int a, int b) { return a + b; } static void Main(string[] args) { //静态方法转换 Func<int, int, int> func = Plus; //实例方法转换 Func<int, int, int> func = new Program().Multiply; //使用new关键字 Func<int, int, int> func = new Func<int, int, int>(Plus); //使用Lambda表达式 Func<int, int, int> func = (a, b) => a - b; int result = func(1, 2); } }
上面使用4中方式来获取一个委托类型的实例,比如将方法作为参数赋值给一个委托类型,在编译时会告知编译器将方法引用转换。还有Lambda表达式,它是一种匿名委托,在Linq中经常永达。
总之,无论是静态方法还是实例方法,能否转换为一个委托实例取决于方法的参数类型、参数顺序、返回值类型是否与委托类型一致。
Delegate类和MulticastDelegate类
委托不仅能类一样实例化,还能像类一样调用方法,也可以创建委托类型的属性,或者将委托类型变量当做参数一样传递,总之,可以将委托类型当做一种特殊的类来使用,而这一点得益于Delegate和MulticastDelegate两个类。
MulticastDelegate类是Delegate类的子类,但是我们无法直接创建Delegate和MulticastDelegate的子类,因为它是由编译器自动派生的。当使用delegate关键字声明的委托,在编译时会生成一个特定的委托类型,而这个委托类型就是派生MulticastDelegate类,因此我们无需像类一样显示用继承才是使用父类的方法(我们用的数组也是类似原理,派生于Array类)。
虽然Delegate和MulticastDelegate两个类都是委托的父类,但是不要认为Delegate就是单播类型,MulticastDelegate就是多播类型,事实上,C#中的委托都是多播类型,比如:
class Program { public static void Multiply(int a, int b) { Console.WriteLine("a*b=" + (a * b)); } public static void Plus(int a, int b) { Console.WriteLine("a+b=" + (a + b)); } static void Main(string[] args) { //第一个方法 Action<int, int> action = Plus; //链接第二个方法(多播) action += Multiply; action(2, 3); //输出 //a+b=5 //a*b=6 } }
需要注意的是,如果委托有返回值,那么返回的是最后一个链接方法的值,其他方法的返回值将会被丢弃:
class Program { public static int Multiply(int a, int b) { Console.WriteLine("a*b=" + (a * b)); return a * b; } public static int Plus(int a, int b) { Console.WriteLine("a+b=" + (a + b)); return a + b; } static void Main(string[] args) { //第一个方法 Func<int, int, int> func = Plus; //链接第二个方法(多播) func += Multiply; int result = func(2, 3); Console.WriteLine("result=" + result); //输出 //a+b=5 //a*b=6 //result=6 } }
注意,这里链接支持+=、-=、+、- 四个运算:
+:链接两个委托,并返回新的委托链,类似于数值运算中的+运算 -:从委托链中剔除一个指定委托,并返回提出后的委托链,如果委托链无任何委托,则返回null,类似于数值运算中的-运算 +=:先执行+运算,再新的委托链赋值给左边的委托变量,类似于数值运算中的+=运算 -=:先执行-运算,再新的委托链赋值给左边的委托变量,类似于数值运算中的-=运算
常用方法与属性
Invoke、BeginInvoke、EndInvoke
Invoke是调用委托方法,而BeginInvoke、EndInvoke是它的异步模式,例如:
Func<int, int, int> func = Plus; int result = func(1, 2); //等价于 int result = func.Invoke(1, 2);
因为委托类型也是应用类型,那么那也就可能为null,如果直接使用可能导致空指针异常,因此我们常将Invoke方法与null条件运算符(?.)一起使用,而非直接调用:
Func<int, int, int> func = null; int result = func(1, 2);//将导致空指针异常 int? result = func?.Invoke(1, 2);//但此时返回值类型可能需要使用可空类型接收
DynamicInvoke
DynamicInvoke来自Delegate类,和Invoke方法一样是调用委托用的,只不过DynamicInvoke调用的参数是一个object数组,所以它常和反射结合起来使用,用于参数情况未知的情况:
Func<int, int, int> func = Plus; int result = (int)func.DynamicInvoke(new object[] { 1, 2 });
Method、Target
Method:多播委托中最后一个委托方法的引用,它是一个System.Reflection.MethodInfo对象 Target:如果多播委托中最后一个委托方法是实例方法,那么Target就是调用的实例对象,如果是静态方法,那么Target就是null
事件
事件和委托类似,只不过事件时基于一种广播订阅模式,实际上,事件是建立在对委托的基础之上的,有了委托,我们才能建立起事件模型。
event
定义一个事件使用event关键字,事件类型即委托类型,如:
//点击事件 public event EventHandler Click;
定义了一个事件,同样可以像委托一样进行赋值、四则运算(+=、-=、+、-)等操作,同样可以想委托一样使用Delegate类和MulticastDelegate类中的方法,比如像方法一样触发事件,或者调用Invoke方法触发事件。
但是事件和委托还是有区别的,事件作为委托的一种特殊用法,和委托的主要却别在于:
1、事件必须定义在类或者结构体等内部,换句话说,委托与类、结构体等是一个级别的,而事件与方法、属性等是一个级别的
2、虽然事件可以使用Delegate类和MulticastDelegate类中的方法,但是它仅限在定义这个时间的类或者结构体等内部才能,比如事件的触发(像方法调用直接触发、调用Invoke方法触发)只能在类或者结构体等内部触发,列如:
class Program { static void Main(string[] args) { var btn = new Button(); btn.Click += (sender,e) => Console.WriteLine("Click"); //btn.Click(btn, EventArgs.Empty);//报错 btn.OnClick();//不报错 } } public class Button { //点击事件 public event EventHandler Click; public void OnClick() { Click(this, EventArgs.Empty); } }
3、事件采用 += 运算表示订阅,使用 -= 表示取消订阅,虽然事件也支持 +、- 运算,但是+、- 运算也只是被限制在定义这个时间的类或者结构体等内部才能使用。
4、定义的事件时一个引用类型,默认是null,所以在触发前要判断,避免空指针异常,建议采用Invoke方法与null条件运算符(?.)一起使用来触发事件,事件可以使用 = 进行赋值,但是同样被限制在定义这个时间的类或者结构体等内部才能使用。
5、标准的事件模型应该没有返回值,确实需要返回值时,应该改用参数的引用传递特性来返回数据。
一个完整简单的事件例子:
class Program { static void Main(string[] args) { MessageProducer messageProducer = new MessageProducer(); Subscriber zhangsan = new Subscriber("张三"); Subscriber lisi = new Subscriber("李四"); Subscriber wangwu = new Subscriber("王五"); messageProducer.Subscribe(zhangsan); messageProducer.Subscribe(lisi); messageProducer.Subscribe(wangwu); messageProducer.Produce("hello"); messageProducer.Unsubscribe(lisi); messageProducer.Produce("hello again"); } } public delegate void MessageProducerHandler(string message); public class MessageProducer { public event MessageProducerHandler MessageHandle; /// <summary> /// 订阅 /// </summary> /// <param name="subscriber"></param> public void Subscribe(Subscriber subscriber) { Console.WriteLine($"【{subscriber.Name}】订阅了消息"); MessageHandle += subscriber.OnMessage; } /// <summary> /// 取消订阅 /// </summary> /// <param name="subscriber"></param> public void Unsubscribe(Subscriber subscriber) { Console.WriteLine($"【{subscriber.Name}】取消了消息订阅"); MessageHandle -= subscriber.OnMessage; } /// <summary> /// 发布一条消息 /// </summary> /// <param name="message"></param> public void Produce(string message) { Console.WriteLine($"发布消息【{message}】"); MessageHandle?.Invoke(message); } } public class Subscriber { public Subscriber(string name) { Name = name; } /// <summary> /// 订阅者名称 /// </summary> public string Name { get; } /// <summary> /// 消息处理方法 /// </summary> /// <param name="message"></param> public void OnMessage(string message) { Console.WriteLine($"【{Name}】收到消息【{message}】"); } }
打印结果:
总结
我们可以认为事件时委托的一种特殊用法,什么时候用委托,什么时候用事件,总结如下:
1、如果需要像一个变量使用,在某段代码后执行回调,应该使用委托,比如 linq 的语法
2、如果需要有返回值,应该采用委托
3、如果需求表现出一次订阅,长时间内接收多个信息的特性,应该使用事假模型。
参考文档:https://docs.microsoft.com/en-us/dotnet/csharp/delegates-overview