首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

C#.Net筑基-解密委托与事件

编程知识
2024年08月05日 07:00

image.png

委托与事件是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.MulticastDelegateMulticastDelegate又继承自 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为方法指针。该构造函数由编译器调用,了解即可。
  • 执行委托的三个方法InvokeBeginInvokeEndInvoke 签名和委托申明一致。
  • 执行一个委托(方法)就是调用foo.Invoke(),其简化语法为foo()BeginInvokeEndInvoke用于异步调用。
  • 因为委托本质上就是一个类,所以委托的定义通常在类外部(和类平级)。

📢 委托、事件的执行,推荐使用?.Invoke,判断是否为nullfoo?.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.CombineDelegate.Remove

image.png

📢注意:委托方法的+-是线程不安全的,事件的addremove是线程安全的。

image.png

🔸执行委托 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内置了一些泛型委托 ActionFunc,基本上可以满足大多数常用场景。

  • Action:支持0到16个泛型参数的委托,无返回值。
  • Func:支持0到16个输入泛型参数,及一个返回值的泛型委托。
  • Predicatebool Predicate<in T>(T obj),用于测试判断的委托,返回测试结果bool

image.png

源代码:

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);

上面委托参数inout是标记可变性(协变、逆变)的修饰符,详见后文《泛型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、解密事件-“封装委托”

image.png

事件的定义: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取消订阅的方法,控制委托的新增和移除,使用时用+=-=语法。上面代码是简化过的,实际代码要稍复杂一点点,主要是加了线程安全处理。
  • 自定义事件也可以直接使用上面示例中的addremove的方式封装。

📢 由上可以看出事件是基于委托封装的,类似属性封装字段。外部只能add订阅、remove取消订阅,事件(委托)的执行(触发)只能在内部进行。

3.3、标准事件模型

C#内部有大量的事件应用,形成了一个默认的事件(标准的)模式,主要定义了用于创建事件的委托、事件参数。

  • System.EventArgs :事件参数,这是标准事件模型的核心,作为事件参数的基类,用来继承自定义实现一些事件要传递的字段(属性)。
  • 委托返回值为void
  • 委托两个参数senderEventArgssender为触发事件的对象,也是事件的广播者;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倍。

image.png

.Net7.Net8中作了大量性能优化,委托调用达到了类似直接调用的性能,因此再也不用担心委托的性能缺陷了。下图为.Net8Benchmark测试。

image.png


参考资料


©️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀

From:https://www.cnblogs.com/anding/p/18229672
本文地址: http://shuzixingkong.net/article/788
0评论
提交 加载更多评论
其他文章 推荐一款.NET开源、功能强大的二维码生成类库
前言 在日常开发需求中,生成二维码以分享文本内容或跳转至指定网站链接等场景是比较常见的。今天大姚给大家分享一款.NET开源(MIT License)、免费、简单易用、功能强大的二维码生成类库:QrCodeGenerator。 项目特点 跨平台兼容性:&#160;支持.NET Standard 2.0
推荐一款.NET开源、功能强大的二维码生成类库 推荐一款.NET开源、功能强大的二维码生成类库 推荐一款.NET开源、功能强大的二维码生成类库
golang 指定权限是 0o755 而不是 0755
在Go语言中,当指定文件权限时,使用前缀 0o 来明确表示八进制数是一种推荐的做法。 这是因为在Go语言中,八进制字面量必须以 0o 或 0O 开头,后跟八进制数字(0-7)。 这种语法是从 Go 1.8 开始引入的,目的是为了减少由于 八进制 字面量与零开头的 十进制数 之间的混淆。 在更早的 G
PowerBI_一分钟学会利用ALLEXCPET分组计算(以计算门店开业前3天销售金额为例)
在某些特殊场景,我们往往需要去计算一些特定的组别的聚合数据 今天,就以计算门店开业前3天的销售情况,来学习一下,利用计算列和DAX度量值,两种快捷计算此类问题的方案。 一:XMIND 二:示例数据 2.1 示例数据列说明 为了方便验证和更清晰的检查结果,数据源只用了三列,分别是3个门店,分别为A,B
PowerBI_一分钟学会利用ALLEXCPET分组计算(以计算门店开业前3天销售金额为例) PowerBI_一分钟学会利用ALLEXCPET分组计算(以计算门店开业前3天销售金额为例) PowerBI_一分钟学会利用ALLEXCPET分组计算(以计算门店开业前3天销售金额为例)
实现一个终端文本编辑器来学习golang语言:第三章文本查看器part1
本章我们来完成文本编辑器的文件打开和查看功能,最后成品如上图。我们将分4步,逐渐完成本章所需功能。内容比较多,会分为两个部分,第一部分主要关注于“View视图”和“buffer及文本读取”。 如上图最终效果所示,我们希望在终端的最下方增加一个状态栏,能够展示当前被打开的文件和当前的光标位置。 同时我
实现一个终端文本编辑器来学习golang语言:第三章文本查看器part1 实现一个终端文本编辑器来学习golang语言:第三章文本查看器part1
【解决方案】Java 互联网项目中消息通知系统的设计与实现(下)
书接上回,消息通知系统(notification-system)作为一个独立的微服务,完整地负责了 App 端内所有消息通知相关的后端功能实现。该系统既需要与文章系统、订单系统、会员系统等相关联,也需要和其它业务系统相关联,是一个偏底层的通用服务系统。
【解决方案】Java 互联网项目中消息通知系统的设计与实现(下) 【解决方案】Java 互联网项目中消息通知系统的设计与实现(下)
SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒!
开心一刻 快 8 点了,街边卖油条的还没来,我只能给他打电话 大哥在电话中说到:劳资卖了这么多年油条,从来都是自由自在,自从特么认识了你,居然让我有了上班的感觉! Session 共享 SpringBoot session 共享配置,我相信你们都会,但出于负责的态度,我还是给你们演示一遍 添加依赖
SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒! SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒! SpringBoot Session共享,配置不生效问题排查 → 你竟然在代码里下毒!
面向对象的编码设计原则
简单讲过程思维是数据结构加操作;对象思维则是一个整体,既包含数据结构又包含操作,也就是面向对象中的属性和行为。 在进行面向对象设计和编码的道路上,众多知名前辈结合自己的实践和认知高度抽象概况出了具有指导思想意义的设计原则。这里的每个原则细细品来都是意味深长,但是需要注意的是,就像数据库范式一样,它是
面向对象的编码设计原则 面向对象的编码设计原则
《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境
一、前言 大家好呀,我是summo,前面我已经写了我为啥要做这个摸鱼小网站的原因,从这篇文章开始我会一步步跟大家聊聊我是怎么搭起这个网站的。我知道对很多新手来说,建网站可能挺头大的,不知道从哪里开始,所以我会尽量写得简单明了,让大家一看就懂,少走弯路。 咱们先从买服务器开始说起。现在阿里云好像还有免
《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境 《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境 《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境