委托与事件在C#1.0的时候就有了,随着C#版本的不断更新,有些写法和功能也在不断改变。本文温故一下这些改变,以及在NET Core中关于事件的一点改变。

一、C#1.0 从委托开始

1. 基本方式

  什么是委托,就不说概念了,用例子说话。

  某HR说他需要招聘一个6年 .NET5 研发经验的“高级”工程师,他想找人(委托)别人把这条招聘消息发出去。这样的HR很多,所以大家定义了一个通用的发消息规则:

public delegate string SendDelegate(string message);

  这就像一个接口的方法,没有实际的实现代码,只是定义了这个方法有一个string的参数和返回值。所有想发招聘消息的HR只要遵守这样的规则即可。

委托本质上是一个类,所以它可以被定义在其他类的内部或外部,根据实际引用关系考虑即可。本例单独定义在外部。

为HR定义了一个名为HR的类:

public class HR
{
public SendDelegate sendDelegate;
public void SendMessage(string msg)
{
sendDelegate(msg);
}
}

  HR有一个SendDelegate类型的成员,当它需要发送消息(SendMessage)的时候,只需要调用这个sendDelegate方法即可。而不需要实现这个方法,也不需要关心这个方法是怎么实现的。

当知道这个HR需要发送消息的时候,猎头张三接了这个帮忙招人的工作。猎头的类为Sender,他有一个用于发送消息的方法Send,该方法恰好符合众人定义的名为SendDelegate的发消息规则。这有点像实现了一个接口方法,但这里不要求方法名一致,只是要求方法的签名一致。

public class Sender
{
public Sender(string name)
{
this.senderName = name;
} private readonly string senderName;
public string Send(string message)
{
string serialNumber = Guid.NewGuid().ToString();
Console.WriteLine(senderName + " sending....");
Thread.Sleep();
Console.WriteLine("Sender: " + senderName + " , Content: " + message + ", Serial Number: " + serialNumber);
return serialNumber;
}
}

猎头帮助HR招人的逻辑如下:

public void Test()
{
//一个HR
HR hr = new HR(); //猎头张三来监听,听到HR发什么消息后立刻传播出去
Sender senderZS = new Sender("张三");
hr.sendDelegate = senderZS.Send; //HR递交消息
hr.SendMessage("Hello World");
}

猎头将自己的发消息方法“赋值”给了HR的SendDelegate方法,为什么可以“赋值”? 因为二者都遵守SendDelegate规则。 就像A和B两个变量都是int类型的时候,A可以赋值给B一样。

这就是一个简单的委托过程,HR将招人的工作委托给了猎头,自己不用去做招人的工作。

但经常一个招聘工作经常会有多个猎头接单,那就有了多播委托。

2. 多播委托

看一下下面的代码:

public void Test()
{
//一个HR
HR hr = new HR(); //猎头张三来监听,听到HR发什么消息后立刻传播出去
Sender senderZS = new Sender("张三");
hr.sendDelegate = senderZS.Send; //快嘴李四也来了
Sender senderLS = new Sender("李四");
hr.sendDelegate += senderLS.Send;
//HR递交消息
hr.SendMessage("Hello World");
}

与之前的代码改变不大, 只是添加了李四的方法绑定,这样HR发消息的时候,张三和李四都会发出招人的消息。

这里要注意李四绑定方法的时候,用的是+=而不是=,就像拼接字符串一样,是拼接而不是赋值,否则会覆盖掉之前张三的方法绑定。

对于第一个绑定的张三,可以用=号也可以用+=(记得之前好像第一个必须用=,实验了一下现在二者皆可)。

这同时也暴露了一些问题:

  • 如果后面的猎头接单的时候不小心(故意)用了=号, 那么最终前面的人的绑定都没有了,那么他将独占这个HR客户,HR发出的消息只有他能收到。
  • 可以偷偷的调用猎头的hr.sendDelegate
public void Test()
{
//一个HR
HR hr = new HR(); //大嘴张三来监听,听到HR发什么消息后立刻传播出去
Sender senderZS = new Sender("张三");
//hr.sendDelegate -= senderZS.Send; //即使未进行过+= 直接调用-=,也不会报错
hr.sendDelegate += senderZS.Send; //快嘴李四也来了
Sender senderLS = new Sender("李四");
hr.sendDelegate += senderLS.Send; //移除
//hr.sendDelegate -= senderZS.Send; //风险:注意上面用的符号是+=和-= 如果使用=,则是赋值操作,
//例如下面的语句会覆盖掉之前所有的绑定
//hr.sendDelegate = senderWW.Send; //HR递交消息
hr.SendMessage("Hello World"); //风险:可以偷偷的以HR的名义偷偷的发了一条消息 sendDelegate应该只能由HR调用
hr.sendDelegate("偷偷的发一条"); }

3. 通过方法避免风险

  很自然想到采用类似Get和Set的方式避免上面的问题。既然委托可以像变量一样赋值,那么也可以通过参数来传值,将一个方法作为参数传递。

    public class HRWithAddRemove
{
private SendDelegate sendDelegate; public void AddDelegate(SendDelegate sendDelegate)
{
this.sendDelegate += sendDelegate; //如果需要限制最多绑定一个,此处可以用=号
} public void RomoveDelegate(SendDelegate sendDelegate)
{
this.sendDelegate -= sendDelegate;
} public void SendMessage(string msg)
{
sendDelegate(msg);
}
}

经过改造后的HR,SendDelegate方法被设置为了private,之后只能通过Add和Remove的方法进行方法绑定。

4.模拟多播委托机制

通过上面委托的表现来看,委托就像是保存了一个相同方法名的集合 List<SendDelegate> ,可以向集合中添加或移除方法,当调用这个委托的时候,会逐一调用该集合中的各个方法。

例如下面的代码( 注意这里假设SendDelegate只对应一个方法 ):

public class HR1
{
public void Delegate(SendDelegate sendDelegate)
{
sendDelegateList = new List<SendDelegate> { sendDelegate }; //对应=
} public void AddDelegate(SendDelegate sendDelegate)
{
sendDelegateList.Add(sendDelegate); //对应+=
} public void RomoveDelegate(SendDelegate sendDelegate)
{
sendDelegateList.Remove(sendDelegate);//对应-=
} public List<SendDelegate> sendDelegateList; public void SendMessage(string msg)
{
foreach (var item in sendDelegateList)
{
item(msg);
}
}
}

二、C#1.0 引入事件

  1.简单事件

  如果既想使用-=和+=的方便,又想避免相关功能开闭的风险怎么办呢?可以使用事件:

    public class HRWithEvent
{
public event SendDelegate sendDelegate;
public void SendMessage(string msg)
{
sendDelegate(msg);
}
}

  只是将SendDelegate前面添加了一个event标识,虽然它被设置为public,但如下代码却会给出错误提示: 事件“HRWithEvent.sendDelegate”只能出现在 += 或 -= 的左边(从类型“HRWithEvent”中使用时除外)

 hr.sendDelegate = senderZS.Send;
hr.sendDelegate("偷偷的发一条");

  2.事件的访问器模式

  上文为委托定义了Add和Remove方法,而事件支持这样的访问器模式,例如如下代码:

    public class CustomerWithEventAddRemove
{
private event SendDelegate sendDelegate; public event SendDelegate SendDelegate
{
add { sendDelegate += value; }
remove { sendDelegate -= value; }
}
public void SendMessage(string msg)
{
sendDelegate(msg);
}
}

  可以像使用Get和Set方法一样,对事件的绑定与移除进行条件约束。

  3. 控制绑定事件的执行

  当多个委托被绑定到事件之后,如果想精确控制各个委托的运行怎么办,比如返回值(虽然经常为void)、异常处理等。

第一章第4节通过一个List<SendDelegate> 模拟了多播委托的绑定。 会想到如果真能循环调用一个个已绑定的委托,就可以精确的进行控制了。那么这里说一下这样的方法:

    public class HRWithEvent
{
public event SendDelegate sendDelegate;
public void SendMessage(string msg)
{
//sendDelegate(msg); 此处不再一次性调用所有
if (sendDelegate != null)
{
Delegate[] delegates = sendDelegate.GetInvocationList(); //获取所有已绑定的委托
foreach (var item in delegates)
{
((SendDelegate)item).Invoke(msg); //逐一调用
}
} }
}

  这里通过Invoke方法逐一调用各个Delegate,从而实现对每一个Delegate的调用的控制。若需要异步调用,则可以通过BeginInvoke方法实现(.NET Core之后不再支持此方法,后面会介绍。)

((SendDelegate)item).BeginInvoke(msg,null,null);

  4. 标准的事件写法

  .NET 事件委托的标准签名是:

void OnEventRaised(object sender, EventArgs args);

  返回类型为 void。 事件基于委托,而且是多播委托。 参数列表包含两种参数:发件人和事件参数。 sender 的编译时类型为 System.Object

  第二种参数通常是派生自 System.EventArgs 的类型(.NET Core 中已不强制要求继承自System.EventArgs,后面会说到)

  将上面的例子修改一下,改成标准写法,大概是下面代码的样子:

public class HRWithEventStandard
{
public delegate void SendEventHandler(object sender, SendMsgArgs e);
public event SendEventHandler Send;
public void SendMessage(string msg)
{
var arg = new SendMsgArgs(msg);
Send(this,arg); //arg.CancelRequested 为最后一个的值 因为覆盖
}
} public class SendMsgArgs : EventArgs
{
public readonly string Msg = string.Empty;
public bool CancelRequested { get; set; }
public SendMsgArgs(string msg)
{
this.Msg = msg;
}
}

三、随着C#版本改变

1. C#2.0 泛型委托

  C#2.0 的时候,随着泛型出现,支持了泛型委托,例如,在委托的签名中可以使用泛型,例如下面代码

public delegate string SendDelegate<T>(T message);

这样的委托适用于不同的参数类型,例如如下代码(注意使用的时候要对应具体的类型)

public delegate string SendDelegate<T>(T message);

public class HR1
{
public SendDelegate<string> sendDelegate1;
public SendDelegate<int> sendDelegate2;
public SendDelegate<DateTime> sendDelegate3;
} public static class Sender1
{
public static string Send1(string msg)
{
return "";
} public static string Send2(int msg)
{
return "";
}
} public class Test
{
public void TestDemo()
{
HR1 hr1 = new HR1();
hr1.sendDelegate1 = Sender1.Send1; // 注意使用的时候要对应具体的类型
hr1.sendDelegate2 = new SendDelegate<int>(Sender1.Send2);
hr1.sendDelegate3 = delegate (DateTime dateTime) { return dateTime.ToLongDateString(); }; }
}

2. C#2.0 delegate运算符

delegate 运算符创建一个可以转换为委托类型的匿名方法:

例如上例中这样的代码:

hr1.sendDelegate3 = delegate (DateTime dateTime) { return dateTime.ToLongDateString(); };

3. C#3.0 Lambda 表达式

从 C# 3 开始,lambda 表达式提供了一种更简洁和富有表现力的方式来创建匿名函数。 使用 => 运算符构造 lambda 表达式,

例如“delegate运算符”的例子可以简化为如下代码:

hr1.sendDelegate3 = (dateTime) => { return dateTime.ToLongDateString(); };

4.C#3,NET Framework3.5,Action 、Func、Predicate

Action 、Func、Predicate本质上是框架为我们预定义的委托,在上面的例子中,我们使用委托的时候,首先要定义一个委托类型,然后在实际使用的地方使用,而使用委托只要求方法名相同,在泛型委托出现之后,“定义委托”这一操作就显得越来越累赘,为此,系统为我们预定义了一系列的委托,我们只要使用即可。

例如Action的代码如下:

    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 void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);
public delegate void Action<in T1, in T2, in T3, in T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
public delegate void Action<in T1, in T2, in T3, in T4, in T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14, in T15>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15);
public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);

实际上定义了最多16个参数的无返回值的委托。

Func与此类似,是最多16个参数的有返回值的委托。Predicate则是固定一个参数以及bool类型返回值的委托。

public delegate bool Predicate<T>(T obj);

5. .NET Core 异步调用

第2.3节中,提示如下代码在.NET Core中已不支持

((SendDelegate)item).BeginInvoke(msg,null,null);

会抛出异常:

System.PlatformNotSupportedException:“Operation is not supported on this platform.”

需要异步调用的时候可以采用如下写法:

Task task = Task.Run(() => ((SendDelegate)item).Invoke(msg));

对应的 EndInvoke() 则改为: task.Wait();

5. .NET Core的 EventHandler<TEventArgs>

.NET Core 版本中,EventHandler<TEventArgs> 定义不再要求 TEventArgs 必须是派生自 System.EventArgs 的类, 使我们使用起来更为灵活。

例如我们可以有这样的写法:

EventHandler<string> SendNew

这在以前的版本中是不允许的。

C# 从1到Core--委托与事件的更多相关文章

  1. 详解C#泛型(二) 获取C#中方法的执行时间及其代码注入 详解C#泛型(一) 详解C#委托和事件(二) 详解C#特性和反射(四) 记一次.net core调用SOAP接口遇到的问题 C# WebRequest.Create 锚点“#”字符问题 根据内容来产生一个二维码

    详解C#泛型(二)   一.自定义泛型方法(Generic Method),将类型参数用作参数列表或返回值的类型: void MyFunc<T>() //声明具有一个类型参数的泛型方法 { ...

  2. C#基本功之委托和事件

    定义:委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用. 在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联 目的:方法声明和方法实现的分离,使得程序更容易扩展 一 ...

  3. js实例分析JavaScript中的事件委托和事件绑定

    我们在学习JavaScript中,难免都会去网上查一些资料.也许偶尔就会遇到“事件委托”(也有的称我“事件代理”,这里不评论谁是谁非.以下全部称为“事件委托”),尤其是在查JavaScript的事件处 ...

  4. 【详细】【转】C#中理解委托和事件 事件的本质其实就是委托 RabbitMQ英汉互翼(一),RabbitMQ, RabbitMQ教程, RabbitMQ入门

    [详细][转]C#中理解委托和事件   文章是很基础,但很实用,看了这篇文章,让我一下回到了2016年刚刚学委托的时候,故转之! 1.委托 委托类似于C++中的函数指针(一个指向内存位置的指针).委托 ...

  5. .NET面试题系列[7] - 委托与事件

    委托和事件 委托在C#中具有无比重要的地位. C#中的委托可以说俯拾即是,从LINQ中的lambda表达式到(包括但不限于)winform,wpf中的各种事件都有着委托的身影.C#中如果没有了事件,那 ...

  6. .NET基础拾遗(4)委托、事件、反射与特性

    Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...

  7. [转载]C#深入分析委托与事件

    原文出处: 作者:风尘浪子 原文链接:http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html 同类链接:http://www.c ...

  8. [转载]C#委托和事件(Delegate、Event、EventHandler、EventArgs)

    原文链接:http://blog.csdn.net/zwj7612356/article/details/8272520 14.1.委托 当要把方法作为实参传送给其他方法的形参时,形参需要使用委托.委 ...

  9. C#委托与事件

    一.在控制台下使用委托和事件 我们都知道,C#中有"接口"这个概念,所谓的"接口"就是定义一套标准,然后由实现类来具体实现其中的方法,所以说"接口,是 ...

  10. C#委托与事件的简单使用

    前言:上一篇博文从原理和定义的角度介绍了C#的委托和事件.本文通过一个简单的小故事,来说明C#委托与事件的使用方法及其方便之处. 在阅读本文之前,需要你对委托和事件的基本概念有所了解.如果你是初次接触 ...

随机推荐

  1. java方法句柄-----4.你所不知道的MethodHandle【翻译】

    Method Handles in Java 1.介绍 在本文中,我们将探讨一个重要的API,它是在Java 7中引入的,并在Java 7版本之后更加完善:全限定名是:Java.lang.invoke ...

  2. Java实现 LeetCode 764 最大加号标志(暴力递推)

    764. 最大加号标志 在一个大小在 (0, 0) 到 (N-1, N-1) 的2D网格 grid 中,除了在 mines 中给出的单元为 0,其他每个单元都是 1.网格中包含 1 的最大的轴对齐加号 ...

  3. Java实现 蓝桥杯 算法训练 Lift and Throw

    试题 算法训练 Lift and Throw 问题描述 给定一条标有整点(1, 2, 3, -)的射线. 定义两个点之间的距离为其下标之差的绝对值. Laharl, Etna, Flonne一开始在这 ...

  4. Java实现 蓝桥杯 算法提高 p1001

    算法提高 P1001 时间限制:1.0s 内存限制:256.0MB 提交此题  当两个比较大的整数相乘时,可能会出现数据溢出的情形.为避免溢出,可以采用字符串的方法来实现两个大数之间的乘法.具体来说 ...

  5. Java实现 蓝桥杯VIP 算法训练 连续正整数的和

    问题描述 78这个数可以表示为连续正整数的和,1+2+3-+12,18+19+20+21,25+26+27. 输入一个正整数 n(<=10000) 输出 m 行(n有m种表示法),每行是两个正整 ...

  6. Java实现高效便捷还容易懂的排序算法

    PS:我现在越来越认为排序大法是,很深的算法了,就是简单的几个步骤,网上的大佬们能给你玩出花来(ง •_•)ง public class zimuzhenlie2 { public static vo ...

  7. Java实现 LeetCode 203 移除链表元素

    203. 移除链表元素 删除链表中等于给定值 val 的所有节点. 示例: 输入: 1->2->6->3->4->5->6, val = 6 输出: 1->2 ...

  8. java实现第四届蓝桥杯好好学习

    好好学习 汤姆跟爷爷来中国旅游.一天,他帮助中国的小朋友贴标语.他负责贴的标语是分别写在四块红纸上的四个大字:"好.好.学.习".但是汤姆不认识汉字,他就想胡乱地贴成一行. 请你替 ...

  9. PAT 德才论

    宋代史学家司马光在<资治通鉴>中有一段著名的“德才论”:“是故才德全尽谓之圣人,才德兼亡谓之愚人,德胜才谓之君子,才胜德谓之小人.凡取人之术,苟不得圣人,君子而与之,与其得小人,不若得愚人 ...

  10. 记一次Docker中Redis连接暴增的问题排查

    周六生产服务器出现redis服务器不可用状态,错误信息为: 状态不可用,等待后台检查程序恢复方可使用.Unexpected end of stream; expected type 'Status' ...