zoukankan      html  css  js  c++  java
  • C# 从1到Core--委托与事件

      委托与事件在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(2000);
            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

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

  • 相关阅读:
    49. 字母异位词分组
    73. 矩阵置零
    Razor语法问题(foreach里面嵌套if)
    多线程问题
    Get json formatted string from web by sending HttpWebRequest and then deserialize it to get needed data
    How to execute tons of tasks parallelly with TPL method?
    How to sort the dictionary by the value field
    How to customize the console applicaton
    What is the difference for delete/truncate/drop
    How to call C/C++ sytle function from C# solution?
  • 原文地址:https://www.cnblogs.com/FlyLolo/p/12879190.html
Copyright © 2011-2022 走看看