C#基础篇 - 理解委托和事件
1.委托
委托类似于C++中的函数指针(一个指向内存位置的指针)。委托是C#中类型安全的,可以订阅一个或多个具有相同签名方法的函数指针。简单理解,委托是一种可以把函数当做参数传递的类型。很多情况下,某个函数需要动态地去调用某一类函数,这时候我们就在参数列表放一个委托当做函数的占位符。在某些场景下,使用委托来调用方法能达到减少代码量,实现某种功能的用途。
1.1.自定义委托
声明和执行一个自定义委托,大致可以通过如下步骤完成:
- 利用关键字delegate声明一个委托类型,它必须具有和你想要传递的方法具有相同的参数和返回值类型;
- 创建委托对象,并且将你想要传递的方法作为参数传递给委托对象;
- 通过上面创建的委托对象来实现该委托绑定方法的调用。
下面一段代码,完成了一次应用委托的演示:
//step01:使用delegate关键字声明委托 public delegate int CalcSumDelegate(int a, int b); class Program { static void Main(string[] args) { //step03:实例化这个委托,并引用方法 CalcSumDelegate del = new CalcSumDelegate(CalcSum); //step04:调用委托 , ); Console.WriteLine("5+5=" + result); } //step02:声明一个方法和委托类型对应 public static int CalcSum(int a, int b) { return a + b; } }
通过上面4个步骤,完成了委托从声明到调用的过程。接着,咱也学着大神用ILSpy反编译上面的代码生成的程序集。截图如下:
- 自定义委托继承关系是:System.MulticastDelegate —> System.Delegate —>System.Object。
- 委托类型自动生成3个方法:BeginInvoke、EndInvoke、Invoke。查资料得知,委托正是通过这3个方法在内部实现调用的。Invoke 方法,允许委托同步调用。上面调用委托的代码del(5, 5)执行时,编译器会自动调用 del.Invoke(5,5);BeginInvoke 方法,允许委托的异步调用。假如上述委托以异步的方式执行,则要显示调用dal.BeginInvoke(5,5)。
注意:BeginInvoke 和 EndInvoke 是.Net中使用异步方式调用同步方法的两个重要方法,具体用法详见微软官方示例。
1.2.多播委托
一个委托可以引用多个方法,包含多个方法的委托就叫多播委托。下面通过一个示例来了解什么是多播委托:
//step01:声明委托类型 public delegate void PrintDelegate(); public class Program { public static void Main(string[] args) { //step03:实例化委托,并绑定第1个方法 PrintDelegate del = Func1; //绑定第2个方法 del += Func2; //绑定第3个方法 del += Func3; //step04:调用委托 del(); //控制台输出结果: //调用第1个方法! //调用第2个方法! //调用第3个方法! } //step02:声明和委托对应签名的3个方法 public static void Func1() { Console.WriteLine("调用第1个方法!"); } public static void Func2() { Console.WriteLine("调用第2个方法!"); } public static void Func3() { Console.WriteLine("调用第3个方法!"); } }
可以看出,多播委托的声明过程是和自定义委托一样的,可以理解为,多播委托就是自定义委托在实例化时通过 “+=” 符号多绑定了两个方法。
Q:为什么能给委托绑定多个方法呢?
自定义委托的基类就是多播委托MulticastDelegate ,这就要看看微软是如何对System.MulticastDelegate定义的:
MulticastDelegate拥有一个带有链接的委托列表,该列表称为调用列表,它包含一个或多个元素。在调用多路广播委托时,将按照调用列表中的委托出现的顺序来同步调用这些委托。如果在该列表的执行过程中发生错误,则会引发异常。(--摘自MSDN)
Q:为什么使用“+=”号就能实现绑定呢?
先来看上述程序集反编译后的调用委托的代码:

“+=”的本质是调用了Delegate.Combine方法,该方法将两个委托连接在一起,并返回合并后的委托对象。
Q:多播委托能引用多个具有返回值的方法吗?
答案是,当然能。委托的方法可以是无返回值的,也可以是有返回值的。不过,对于有返回值的方法需要我们从委托列表上手动调用。否则,就只能得到委托调用的最后一个方法的结果。下面通过两段代码验证下:
public delegate string GetStrDelegate(); public class Program { public static void Main(string[] args) { GetStrDelegate del = Func1; del += Func2; del += Func3; string result = del(); Console.WriteLine(result); //控制台输出结果: //You called me from Func3 } public static string Func1() { return "You called me from Func1!"; } public static string Func2() { return "You called me from Func2!"; } public static string Func3() { return "You called me from Func3!"; } }
直接执行
正确做法:利用GetInvocationList获得委托列表上所有方法,循环依次执行委托,并处理委托返回值。
public delegate string GetStrDelegate(); public class Program { public static void Main(string[] args) { GetStrDelegate del = Func1; del += Func2; del += Func3; //获取委托链上所有方法 Delegate[] delList = del.GetInvocationList(); //遍历,分别处理每个方法的返回值 foreach (GetStrDelegate item in delList) { //执行当前委托 string result = item(); Console.WriteLine(result); //控制台输出结果: //You called me from Func1 //You called me from Func2 //You called me from Func3 } Console.ReadKey(); } public static string Func1() { return "You called me from Func1"; } public static string Func2() { return "You called me from Func2"; } public static string Func3() { return "You called me from Func3"; } }
遍历执行
1.3.匿名方法
匿名方法是C#2.0版本引入的一个新特性,用来简化委托的声明。假如委托引用的方法只使用一次,那么就没有必要声明这个方法,这时用匿名方法表示即可。
//step01:定义委托类型 public delegate string ProStrDelegate(string str); public class Program { public static void Main(string[] args) { //step02:将匿名方法指定给委托对象 ProStrDelegate del = delegate(string str) { return str.ToUpper(); }; string result = del("KaSlFkaDhkjHe"); Console.WriteLine(result); Console.ReadKey(); //输出:KASLFKAFHKJHE } }
匿名方法只是C#提供的一个语法糖,方便开发人员使用。在性能上与命名方法几乎无异。
匿名方法通常在下面情况下使用:
- 委托需要指定一个临时方法,该方法使用次数极少;
- 这个方法的代码很短,甚至可能比方法声明都短的情况下使用。
1.4.Lambda表达式
Lambda表达式是C#3.0版本引入的一个新特性,它提供了完成和匿名方法相同目标的更加简洁的格式。下面示例用Lambda表达式简化上述匿名方法的例子:
public delegate string ProStrDelegate(string str); public class Program { public static void Main(string[] args) { //匿名委托 //ProStrDelegate del = delegate(string str) { return str.ToUpper(); }; //简化1 //ProStrDelegate del1 = (string str) =>{ return str.ToUpper(); }; //简化2 //ProStrDelegate del2 = (str) =>{ return str.ToUpper(); }; //简化3 ProStrDelegate del3 = str => str.ToUpper(); string result = del3("KaSlFkaDhkjHe"); Console.WriteLine(result); Console.ReadKey(); //输出:KASLFKAFHKJHE } }
- 简化1:去掉delegate关键字,用"=>"符号表示参数列表和方法体之间的关系;
- 简化2:去掉方法的参数类型;假如只有一个参数,参数列表小括号()也可省略;
- 简化3:如果方法体中的代码块只有一行,可以去掉 return,去掉方法体的大括号{}。
1.5.内置委托
上述几种委托的使用,都没能离开定义委托类型这一步骤。微软干脆直接把定义委托这一步骤封装好,形成三个泛型类:Action<T>、Func<T>和Predicate<T>,这样就省去了定义的步骤,推荐使用。
public class Program { public static void Main(string[] args) { //Action Action<string> action = delegate(string str) { Console.WriteLine("你好!" + str); }; action("GG"); //Func Func<int, int, int> func = delegate(int x, int y) { return x + y; }; Console.WriteLine(, )); //Predicate Predicate<bool> per = delegate(bool isTrue) { return isTrue == true; }; Console.WriteLine(per(true)); } }
它们的区别如下:
- Action<T>委托:允许封装的方法有多个参数,不能有返回值;
- Func<T>委托:允许封装的方法有多个参数,必须有返回值;
- Predicate<T>委托:允许封装的方法有一个参数,返回值必须为bool类型。
2.事件
委托是一种类型,事件依赖于委托,故事件可以理解为是委托的一种特殊实例。它和普通的委托实例有什么区别呢?委托可以在任意位置定义和调用,但是事件只能定义在类的内部,只允许在当前类中调用。所以说,事件是一种类型安全的委托。
2.1.定义事件
通过一个简单的场景来演示下事件的使用:
/// <summary> /// 音乐播放器 /// </summary> public class MusicPlayer { //step01:定义 音乐播放结束 事件 public event EventHandler<EventArgs> PlayOverEvent; public string Name { get; set; } public MusicPlayer(string name) { this.Name = name; } //step02:定义一个触发事件的方法 public void PlaySong() { //模拟播放 Console.WriteLine("正在播放歌曲:" + this.Name); ; i < ; i++) { Console.Write("."); Thread.Sleep(); } //播放结束,则触发PlayOverEvent事件 if (PlayOverEvent != null) { PlayOverEvent(this, null); } } } public class Program { static void Main(string[] args) { //创建音乐播放器对象 MusicPlayer player = new MusicPlayer("自由飞翔"); //step03:注册事件 player.PlayOverEvent += player_PlayOverEvent; //播放歌曲,结束后触发事件 player.PlaySong(); Console.ReadKey(); } static void player_PlayOverEvent(object sender,EventArgs e) { MusicPlayer player = sender as MusicPlayer; Console.WriteLine("\r\n{0}播完了!", player.Name); } }
程序运行结果:
总结上面事件使用的几个步骤:
- 用event关键字定义事件,事件必须要依赖一个委托类型;
- 在类内部定义触发事件的方法;
- 在类外部注册事件并引发事件。
public event EventHandler<EventArgs> PlayOverEvent
这句代码在MusicPlayer类定义了一个事件成员PlayOverEvent,我们说事件依赖于委托、是委托的特殊实例,所以EventHandler<EventArgs>肯定是一个委托类型。下面我们来验证一下:
EventHandler是微软封装好的事件委托,该委托没有返回值类型,两个参数:sender事件源一般指的是事件所在类的实例;TEventArgs事件参数,如果有需要创建,要显示继承System.EventArgs。
2.2.事件的本质
MusicPlayer player = new MusicPlayer("自由飞翔"); //注册事件 player.PlayOverEvent += player_PlayOverEvent; player.PlaySong();
从上面代码我们观察到,事件要通过"+="符号来注册。我们猜想,事件是不是像多播委托一样通过Delegate.Combine方法可以绑定多个方法?还是通过反编译工具查看下。
我们看到PlayOverEvent事件内部生成了两个方法:add_ PlayOverEvent和remove_ PlayOverEvent。add方法内部调用Delegate.Combine把事件处理方法绑定到委托列表;remove方法内部调用Delegate.Remove从委托列表上移除指定方法。其实,事件本质上就是一个多播委托。
3.参考文章
[1] Edison Chou,http://www.cnblogs.com/edisonchou/p/4827578.html
[2] jackson0714,http://www.cnblogs.com/jackson0714/p/5111347.html
[3] Sam Xiao, http://www.cnblogs.com/xcj26/p/3536082.html
作者:gao-yang
出处:http://www.cnblogs.com/gao-yang/p/5493028.html
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
C#基础篇 - 理解委托和事件的更多相关文章
- 【详细】【转】C#中理解委托和事件 事件的本质其实就是委托 RabbitMQ英汉互翼(一),RabbitMQ, RabbitMQ教程, RabbitMQ入门
[详细][转]C#中理解委托和事件 文章是很基础,但很实用,看了这篇文章,让我一下回到了2016年刚刚学委托的时候,故转之! 1.委托 委托类似于C++中的函数指针(一个指向内存位置的指针).委托 ...
- 【Unity|C#】基础篇(8)——委托(Delegate)/ 事件(Event)
[学习资料] <C#图解教程>(第13~14章):https://www.cnblogs.com/moonache/p/7687551.html 电子书下载:https://pan.bai ...
- C#知识体系(二)用案例来理解委托与事件
上一篇博客讲到了LinQ和lambda的常用方法 还有很多我们未知但c#设计团队已经为我们封装好的类和方法.随着我们不断的熟悉C#语言,渐渐的就会接触到其他的知识点,委托.事件.反射.线程.同步,异步 ...
- C#学习之初步理解委托、事件、匿名方法和Lambda
最经在学习LinqtoSql,然后扯到Lambda表达式,然后扯到匿名方法,然后扯到委托,最后扯到事件处理...后来发现对委托这个概念和事件处理这个过程理解得不是很清晰,遂得一下学习笔记.那里说得不对 ...
- C# 篇基础知识5——委托和事件
事件处理程序是基于“委托”机制运行的. 1.委托 (1)委托的定义和使用 有时需要将一个函数作为另一个函数的参数,这时就要用到委托(Delegate)机制.例如设计一个马戏表演函数: //定义委托 d ...
- 【详细】【转】C#中理解委托和事件
文章是很基础,但很实用,看了这篇文章,让我一下回到了2016年刚刚学委托的时候,故转之! 1.委托 委托类似于C++中的函数指针(一个指向内存位置的指针).委托是C#中类型安全的,可以订阅一个或多个具 ...
- js 基础篇(点击事件轮播图的实现)
轮播图在以后的应用中还是比较常见的,不需要多少行代码就能实现.但是在只掌握了js基础知识的情况下,怎么来用较少的而且逻辑又简单的方法来实现呢?下面来分析下几种不同的做法: 1.利用位移的方法来实现 首 ...
- (spring-第15回【IoC基础篇】)容器事件
五个人在报社订阅了报纸.报社一旦有了新报纸,就派员工分别送到这五个人手里.在这个例子中,“报纸”就是事件,“报社”就是广播器,五个订阅者就是监听器.广播器收到事件,把事件传给监听器,监听器对事件做一些 ...
- 自动化测试基础篇--Selenium鼠标键盘事件
摘自https://www.cnblogs.com/sanzangTst/p/7477382.html 前面几篇文章我们学习了怎么定位元素,同时通过实例也展示了怎么切换到iframe,怎么输入用户名和 ...
随机推荐
- iOS开发系列--打造自己的“美图秀秀”
--绘图与滤镜全面解析 概述 在iOS中可以很容易的开发出绚丽的界面效果,一方面得益于成功系统的设计,另一方面得益于它强大的开发框架.今天我们将围绕iOS中两大图形.图像绘图框架进行介绍:Quartz ...
- Android 获取meta-data中的数据
在 Android 的 Mainfest 清单文件中,Application,Activity,Recriver,Service 的节点中都有这个的存在.很多时候我们可以通过 meta-data 来配 ...
- salesforce 零基础学习(六十二)获取sObject中类型为Picklist的field values(含record type)
本篇引用以下三个链接: http://www.tgerm.com/2012/01/recordtype-specific-picklist-values.html?m=1 https://github ...
- 1.初始Windows Server 2012 R2 Hyper-V + 系统安装详细
干啥的?现在企业服务器都是分开的,比如图片服务器,数据库服务器,redis服务器等等,或多或少一个网站都会用到多个服务器,而服务器的成本很高,要是动不动采购几十台,公司绝对吃不消的,于是虚拟化技术出来 ...
- 【翻译】MongoDB指南/聚合——聚合管道
[原文地址]https://docs.mongodb.com/manual/ 聚合 聚合操作处理数据记录并返回计算后的结果.聚合操作将多个文档分组,并能对已分组的数据执行一系列操作而返回单一结果.Mo ...
- [Nginx笔记]关于线上环境CLOSE_WAIT和TIME_WAIT过高
运维的同学和Team里面的一个同学分别遇到过Nginx在线上环境使用中会遇到TIME_WAIT过高或者CLOSE_WAIT过高的状态 先从原因分析一下为什么,问题就迎刃而解了. 首先是TIME_WAI ...
- [BootStrap] 富编辑器,基于wysihtml5
在我的周围,已经有很多人在使用BootStrap,但对于任何一个带留言.评论.提问.文章编辑功的网站,编辑器永远是重中之重,显然,早期的编辑器完全没考虑过BootStrap的出现,或皮肤跟网站不匹配, ...
- C# 序列化与反序列化几种格式的转换
这里介绍了几种方式之间的序列化与反序列化之间的转换 首先介绍的如何序列化,将object对象序列化常见的两种方式即string和xml对象; 第一种将object转换为string对象,这种比较简单没 ...
- Javascript中的valueOf与toString
基本上,javascript中所有数据类型都拥有valueOf和toString这两个方法,null除外.它们俩解决javascript值运算与显示的问题,本文将详细介绍,有需要的朋友可以参考下. t ...
- jquery.each()
$(selector).each(function(index,element)) index - 选择器的 index 位置 element - 当前的元素(也可使用 "this" ...