C#.Net筑基-解密委托与事件
委托与事件是C#中历史比较悠久的技术,从C#1.0
开始就有了,核心作用就是将方法作为参数(变量)来传递和使用。其中委托是基础,需要熟练掌握,编程中常用的Lambda表达式、Action、Func都是委托,包括事件也是基于委托实现的。
01、认识委托delegate
1.1、什么是委托?
委托是一种用来包装方法的特殊类型,可以将方法包装为对象进行传递、调用,类似函数指针。delegate 关键字用来定义一个委托类型,语法类似方法申明,可以看做是一个“方法签名模板”,和方法一样定义了方法的返回值、参数。
- 用
delegate
定义的委托是一个类,继承自 System.MulticastDelegate、System.Delegate,“方法名”就是委托类型的名称。 - 委托的使用同其他普通类型,实例指向一个方法的引用,该方法的申明和委托定义的“方法签名模板”须匹配(支持协变逆变)。
- 委托支持连接多个委托(方法),称为多播委托(MulticastDelegate),执行时都会调用。
public delegate void Foo(string name); //申明一个委托类型
void Main()
{
Foo faction; //申明一个Foo委托(实例)变量
faction = DoFoo; //赋值一个方法
faction += str => { Console.WriteLine($"gun {str}"); }; //添加多个"方法实例"
faction += DoFoo; //继续添加,可重复
faction("sam"); //执行委托,多个方法会依次执行
faction.Invoke("zhang"); //同上,上面调用方式实际上还是执行的Invoke方法。
}
private void DoFoo(string name){
Console.WriteLine($"hello {name}");
}
委托的主要使用场景:核心就是把方法作为参数来传递,分离方法申明和方法实现。
- 回调方法,包装方法为委托,作为参数进行传递,解耦了方法的申明、实现和调用,可以在不同的地方进行。
- Lambda表达式,这是委托的简化语法形式,更简洁,比较常用。
- 事件,事件是一种特殊的委托,是基于委托实现的,可以看做是对委托的封装。
1.2、Delegate API
Delegate属性 | 说明 |
---|---|
Method | 获取委托所表示的方法信息,多个值返回最后一个 |
Target | 获取委托方法所属的对象实例,多个值返回最后一个,静态方法则为null 。所以要注意:委托、事件不用时要移除,避免GC无法释放资源。 |
Delegate静态成员 | - |
CreateDelegate | 用代码创建指定类型的委托,包括多个重载方法 |
Combine(Delegate, Delegate) | 将多个委托组合为一个新委托(链),简化语法+ 、+= :Foo d = d1 + d2; |
Remove(source, value) | 移除指定委托的调用列表,返回新的委托。简化语法- 、-= :d -= d1 |
RemoveAll(source, value) | 同上,区别是Remove 值移除找到的最后一个,RemoveAll 移除所有找到的 |
MulticastDelegate成员 | - |
GetInvocationList() | 按照调用顺序返回此多路广播委托的委托列表 |
1.3、解密委托“类型”
用 delegate
定义的委托,编译器会自动生成一个密封类,so,委托本质上就是一个类。该委托类继承自 System.MulticastDelegate,MulticastDelegate
又继承自 System.Delegate,Delegate是委托的基类,她们都是抽象类( abstract class)。
delegate
定义的委托编译后的IL代码如下(已简化),可查看在线sharplab。
public delegate void Foo(string name,int age); //申明一个委托类型
//编译器生成的Foo委托类(简化代码)
class public auto ansi sealed Foo extends [System.Runtime]System.MulticastDelegate]
{
void Foo(object obj, IntPtr method) { ... }
public virtual void Invoke (string name,int32 age) { ... }
public virtual BeginInvoke (string name,int32 age,System.AsyncCallback callback, object 'object') { ... }
public virtual void EndInvoke (class [System.Runtime]System.IAsyncResult result) { ... }
}
- 委托的构造函数有两个参数,
obj
为方法所在的对象,method
为方法指针。该构造函数由编译器调用,了解即可。 - 执行委托的三个方法
Invoke
、BeginInvoke
和EndInvoke
签名和委托申明一致。 - 执行一个委托(方法)就是调用
foo.Invoke()
,其简化语法为foo()
。BeginInvoke
和EndInvoke
用于异步调用。 - 因为委托本质上就是一个类,所以委托的定义通常在类外部(和类平级)。
委托、事件的执行,推荐使用
?.Invoke
,判断是否为null
:foo?.Invoke()
测试一下委托的继承层次:
public delegate void Foo(string name); //申明一个委托类型
void Main()
{
Foo faction; //申明一个Foo委托变量
faction = DoFoo; //赋值
var ftype = faction.GetType();
while (ftype != null)
{
Console.WriteLine(ftype.FullName);
ftype = ftype.BaseType;
}
//输出:
//Foo
//System.MulticastDelegate
//System.Delegate
//System.Object
}
private void DoFoo(string name){
Console.WriteLine($"hello {name}");
}
1.4、多播委托MulticastDelegate
我们编码中使用的委托、事件其实都是多播委托 MulticastDelegate,可包含多个(单一)委托。MulticastDelegate
中有一个委托链表_invocationList
,可存放多个(单一)委托(可重复添加),当执行委托时,委托链表中的委托方法会依次执行。
添加移除:推荐用+
、-
操作符添加、移除委托,其本质是调用Delegate
的静态方法Delegate.Combine
、Delegate.Remove
。
注意:委托方法的
+
、-
是线程不安全的,事件的add
、remove
是线程安全的。
执行委托 A.Invoke()/A()
,:所有(委托)方法都会执行。可通过 GetInvocationList() 获取委托(方法)列表,手动控制执行。
- 如果其中一个方法执行报错,链表后面的就不会执行了。
- 如果委托方法有返回值,则只能获取最后一个结果。
注意:添加、移除操作都会返回一个新的委托,原有委托并不受影响,委托是恒定的!
public delegate void Foo(string name); //申明一个委托类型
void Main()
{
Foo f1 = default; //申明一个Foo委托变量
f1 += DoFoo; //添加一个方法
f1 += DoFoo; //再添加一个方法
f1 += str => { Console.WriteLine($"gun {str}"); }; //继续添加
f1("sam"); //执行了3次方法
f1 -= DoFoo;//移除
f1("sam"); //执行了2次方法
Foo f2 = DoFoo;
Foo f3 = f1+f2; //组合委托
Foo f4 = (Foo)Delegate.Combine(f1,f2); //同上
Console.WriteLine(f3==f4); //True,内部方法列表中的元素相同,则委托相同
Console.WriteLine(f3-f2 == f1); //True,移除委托
}
private void DoFoo(string name)
{
Console.WriteLine($"hello {name}");
}
1.5、匿名方法和Lambda表达式
- 匿名方法是一种没有名分(名字)的方法,用
delegate
关键字申明,可传递给委托或Lambda表达式。 - Lambda表达式和匿名方法一样,本质上都是委托,生成的IL代码是类似的。Lambda表达式更简洁,支持类型推断,所以现代的编程中基本都是用Lambda表达式了。
public delegate void Foo(string name); //申明一个委托类型
void Main()
{
//匿名方法
Foo f1 = delegate(string name){
Console.WriteLine(name);
};
Action a1 = delegate() { Console.WriteLine("hello");};
f1("sam");
a1();
//Lambda表达式
Foo f2 = name=>Console.WriteLine(name);
f2("king");
}
匿名方法、Lambda方法 会被编译为一个私有方法,在一个私有的类中。
02、内置委托类型Action、Func
由上文可知委托在编译时会创建一个类型,为提高性能、效率,避免大量不必要重复的委托定义,.Net
内置了一些泛型委托 Action、Func,基本上可以满足大多数常用场景。
- Action:支持0到16个泛型参数的委托,无返回值。
- Func:支持0到16个输入泛型参数,及一个返回值的泛型委托。
- Predicate:
bool Predicate<in T>(T obj)
,用于测试判断的委托,返回测试结果bool
。
源代码:
public delegate void Action();
public delegate void Action<in T>(T obj);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
...
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
...
public delegate bool Predicate<in T>(T obj);
上面委托参数in
、out
是标记可变性(协变、逆变)的修饰符,详见后文《泛型T & 协变逆变》
03、认识事件Event
3.1、什么是事件event
?
事件是一种特殊类型的委托,他是基于委托实现的,是对委托的进一步封装,因此使用上和委托相似。事件使用 event
关键字进行申明,任何其他组件都可以订阅事件,当事件被触发时,它会调用所有已经订阅它的委托(方法)。
事件是基于委托的一种(事件驱动)编程模型,用于在对象之间实现基于发布-订阅模式的通知机制,是实现观察者模式的方式之一。常用在GUI编程、异步编程以及其他需要基于消息的系统。
void Main()
{
var u = new User();
//订阅事件
u.ScoreChanged += (sender, e) => { Console.WriteLine(sender); };
u.AddScore(100);
u.AddScore(200);
}
public class User
{
public int Score { get; private set; }
public event EventHandler ScoreChanged; //定义事件,使用内置的“事件”委托 EventHandler
public void AddScore(int score)
{
this.Score += score;
this.ScoreChanged?.Invoke(this, null); //触发事件
}
}
事件的关键角色:
①事件的发布者,发布事件的所有者,在合适的时候触发事件,并通过事件参数传递信息:
sender
:事件源,就是引发事件的发布者。EventArgs
:事件参数,一般是继承System.EventArgs
的对象,当然这不是必须的,在.NET Core
中事件参数可以是任意类型。System.EventArgs
只是一个空的class
,啥也没有。
②事件的订阅者:订阅发布的事件,事件发生后执行的具体操作。
EventHandler(object? sender, EventArgs e)、EventArgs
<T>
、Button.Click
算是微软的标准事件模式,是一种习惯约定。
事件使用实践:
- 使用
+=
订阅事件,支持任意多个订阅。-=
移除不用的事件订阅,避免内存溢出,注意-=
对匿名方法、Lambda无效,因为每次都是新的委托。 - 事件的触发需判断
null
,避免没有订阅时触发报错:Progress?.Invoke()
。 - 事件委托类型以“EventHandler”结尾,大多数场景下使用
EventHandler<TEventArgs>
即可,当然也可以自定义,或使用Action
。
事件命名:名词+动词(被动)
- 事件已发生用过去式:Closed、PropertyChanged。
- 事件将要发生用现在式,Closing、ToolTipOpening。
- 订阅的方法前缀通常加“
On
”、“Raise
”,fileLister.Progress += OnProgress;
3.2、解密事件-“封装委托”
事件的定义:public event EventHandler MyEvent;
,其中EventHandler
就是一个委托,下面为其源码:
public delegate void EventHandler(object? sender, EventArgs e);
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
当定义个事件时,C#编译器会生成对委托的事件包装,类似属性对字段的包装,在线sharplab源码。
//定义一个事件
public event EventHandler MyEvent;
//用其他委托定义事件
public event Action<string> MyEvent2;
//编译后的IL代码(简化)**********
//委托字段
private EventHandler m_MyEvent;
//类似属性的get、set访问器,通过+ - 来订阅、取消事件订阅。
public event EventHandler MyEvent
{
add { m_MyEvent += value; } //Delegate.Combine
remove { m_MyEvent -= value; } //Delegate.Remove
}
- 定义事件的“EventHandler”为一个委托,可以是任意委托类型,C#中大多使用内置泛型委托
EventHandler<TEventArgs>
。 - 编译后生成了一个私有委托字段
m_MyEvent
,这是事件的核心。 - 生成了
add
订阅、remove
取消订阅的方法,控制委托的新增和移除,使用时用+=
、-=
语法。上面代码是简化过的,实际代码要稍复杂一点点,主要是加了线程安全处理。 - 自定义事件也可以直接使用上面示例中的
add
、remove
的方式封装。
由上可以看出事件是基于委托封装的,类似属性封装字段。外部只能
add
订阅、remove
取消订阅,事件(委托)的执行(触发)只能在内部进行。
3.3、标准事件模型
C#内部有大量的事件应用,形成了一个默认的事件(标准的)模式,主要定义了用于创建事件的委托、事件参数。
- System.EventArgs :事件参数,这是标准事件模型的核心,作为事件参数的基类,用来继承自定义实现一些事件要传递的字段(属性)。
- 委托返回值为
void
。 - 委托两个参数
sender
、EventArgs
,sender
为触发事件的对象,也是事件的广播者;EventArgs
为事件的参数。 - 委托以“EventHandler”命名结尾。
- 内置的泛型版本
EventHandler<TEventArgs>
可以满足上述条件,是一个比较通用的标准事件委托。
public class EventArgs
{
public static readonly EventArgs Empty = new EventArgs();
}
public delegate void EventHandler(object? sender, EventArgs e);
//通用泛型版本
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
当然这个这个模式并不是必须的,只是一种编程习惯或规范。
3.4、该用委托还是事件?
事件是基于委托的,事件的功能委托大都能支持,两者功能和使用都比较相似,都支持单播、多播,后期绑定,那两者该如何选择呢?
- 事件一般没有返回值,当然你想要也是可以的。
- 事件提供更好的封装,类似属性对字段的封装,符合开闭原则。事件的执行只能在内部,外部只能
+=
订阅、-=
取消订阅。
所以结论:
- 简单场景用委托:一对一通讯、传递方法。
- 复杂场景用事件:一对多通讯、需要安全权限封装。
04、其他-委托的性能问题?
由前文我们知道委托实际上都是一个多播委托类型,执行委托时实际是执行Invoke()
方法,内部会迭代执行方法列表,这要比直接方法调用要慢不少。
public static int Sum(int x, int y) => x + y; //方法
public static Func<int, int, int> SumFunc = Sum;//委托
public void Sum_MethodCall() //直接调用方法
{
int sum = 0;
for (int i = 0; i < 10; i++)
{
sum += Sum(i, i + 1);
}
}
public void Sum_FuncCall() //调用委托
{
int sum = 0;
for (int i = 0; i < 10; i++)
{
sum += SumFunc(i, i + 1);
}
}
在.Net6
中运行Benchmark
测试对比如下,直接调用的效率要高4-5倍。
在.Net7
、.Net8
中作了大量性能优化,委托调用达到了类似直接调用的性能,因此再也不用担心委托的性能缺陷了。下图为.Net8
中Benchmark
测试。
参考资料
- System.Delegate 和 delegate 关键字
- 标准 .NET 事件模式
- 还弄不明白【委托和事件】么?,适合入门。
- 由浅入深理解C#中的事件,比较细致,适合入门
- C# 的委托与事件大致是怎么一回事,B站视频
- .NET中委托性能的演变
️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀
C#.Net筑基-解密委托与事件的更多相关文章
- .NET面试题系列[7] - 委托与事件
委托和事件 委托在C#中具有无比重要的地位. C#中的委托可以说俯拾即是,从LINQ中的lambda表达式到(包括但不限于)winform,wpf中的各种事件都有着委托的身影.C#中如果没有了事件,那 ...
- .NET基础拾遗(4)委托、事件、反射与特性
Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...
- [转载]C#深入分析委托与事件
原文出处: 作者:风尘浪子 原文链接:http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html 同类链接:http://www.c ...
- [转载]C#委托和事件(Delegate、Event、EventHandler、EventArgs)
原文链接:http://blog.csdn.net/zwj7612356/article/details/8272520 14.1.委托 当要把方法作为实参传送给其他方法的形参时,形参需要使用委托.委 ...
- C#委托与事件
一.在控制台下使用委托和事件 我们都知道,C#中有"接口"这个概念,所谓的"接口"就是定义一套标准,然后由实现类来具体实现其中的方法,所以说"接口,是 ...
- C#委托与事件的简单使用
前言:上一篇博文从原理和定义的角度介绍了C#的委托和事件.本文通过一个简单的小故事,来说明C#委托与事件的使用方法及其方便之处. 在阅读本文之前,需要你对委托和事件的基本概念有所了解.如果你是初次接触 ...
- C#之委托与事件
委托与事件 废话一堆:网上关于委托.事件的文章有很多,一千个哈姆雷特就有一千个莎士比亚,以下内容均是本人个人见解. 1. 委托 1.1 委托的使用 这一小章来学习一下怎么简单的使用委托,了解一些基本的 ...
- [ASP.NET MVC 大牛之路]02 - C#高级知识点概要(1) - 委托和事件
在ASP.NET MVC 小牛之路系列中,前面用了一篇文章提了一下C#的一些知识点.照此,ASP.NET MVC 大牛之路系列也先给大家普及一下C#.NET中的高级知识点.每个知识点不太会过于详细,但 ...
- .NET委托和事件
.net学习之委托和事件 1.什么是委托 通俗的说:委托就是一个能够存储符合某种格式(方法签名)的方法的指针的容器 上传图片: 2.委托语法 准备一个方法:string Hello(string ...
- C#委托和事件
委托和事件都可以用来调用跟自己方法签名一样的方法,两者在使用中主要有以下区别: 委托和事件没有可比性,因为委托是类型,事件是对象: 委托可以在声明它的类外部进行调用,而事件只能在类的内部进行调用: 委 ...
随机推荐
- 开源一款功能强大的 .NET 消息队列通讯模型框架 Maomi.MQ
目录 文档说明 导读 快速开始 消息发布者 IMessagePublisher 连接池 消息过期 事务 发送方确认模式 独占模式 消费者 消费者模式 事件模式 分组 消费者模式 消费.重试和补偿 消费 ...
- babel 基础概念 & 从零到一写一个 babel 插件
babel 基础概念 简单来说,做语法转换兼容的, 复杂一点的说,babel可以将我们写的 ES6+ 的Javascript语法转换为向后兼容的语法,以便能够在旧版本的浏览器或者其他环境运行. bab ...
- 浅拷贝、深拷贝与序列化【初级Java必需理解的概念】
浅拷贝 首先创建两个类,方便理解浅拷贝 @Data class Student implements Cloneable{ //年龄和名字是基本属性 private int age; private ...
- FEL - Fast Expression Language
开源好用的表达式计算语言FEL,可惜了官网文档不在国内,我来copy个过来. Fel是轻量级的高效的表达式计算引擎 Fel在源自于企业项目,设计目标是为了满足不断变化的功能需求和性能需求. Fel是开 ...
- ssh_exchange_identification: Connection closed by remote host 错误解决方案
问题 今天登陆服务器时候,ssh 后返回 ssh_exchange_identification: Connection closed by remote host 错误,重试了几次,会有一定概率失败 ...
- FFmpeg开发笔记(三十二)利用RTMP协议构建电脑与手机的直播Demo
不管是传统互联网还是移动互联网,实时数据传输都是刚需,比如以QQ.微信为代表的即时通信工具,能够实时传输文本和图片.其中一对一的图文通信叫做私聊,多对多的图文通信叫做群聊. 除了常见的图文即时通信,还 ...
- Goland断点调试一直进gopark
现象 使用Goland断点调试一直进gopark 分析 直接运行调试,不打断点,会有一个warning: undefined behavior - version of Delve is too ol ...
- 【论文阅读】Pylot: A Modular Platform for Exploring Latency-Accuracy Tradeoffs in Autonomous Vehicles
参考与前言 resource 代码:https://github.com/erdos-project/pylot 论文地址:https://www.ionelgog.org/data/papers/2 ...
- java --面试题大全
J2EE面试题 文档版本号:V2.0 2016年11月 目 录 1. Java基础部分 8 1.1. 一个".java"源文 ...
- C语言gcc编译环境搭建
第一步,根据以下链接下载gcc工具包: gcc工具包下载地址: 链接:https://pan.baidu.com/s/1JqEjakTcWLPv7p6zkah6sA提取码:k4d2 第二步,将下载好的 ...