C# 从1到Core--委托与事件

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

1、C#1.0 从委托开始

1. 基本方式

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

  某HR说他须要招聘一个6年 .NET5 研发经验的“高级”工程师,他想找人(委托)别人把这条招聘消息发出去。这样的HR不少,因此你们定义了一个通用的发消息规则:函数

public delegate string SendDelegate(string message);

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

委托本质上是一个类,因此它能够被定义在其余类的内部或外部,根据实际引用关系考虑便可。本例单独定义在外部。this

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

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

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

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

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(2000);
        Console.WriteLine("Sender: " + senderName + " , Content: " + message + ", Serial Number: "  + serialNumber);
        return serialNumber;
    }
}

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

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

2、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;
    }
}

 

 

3、随着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

 

这在之前的版本中是不容许的。

相关文章
相关标签/搜索