C#中的委托(delegate)与事件(event)

  委托

  .NET团队之初想要实现一种用于任何后期绑定算法的引用类型,也就是想要一种可用于对方法引用的一种结构,同时又希望它能支持单播多播,于是委托诞生了。  

    多播指的将多个方法调用链接在一起调用,就像一个列表一样
    单播指的是单一方法的调用,其实可以认为单播是多播的一种特例

  委托是.NET 1.0版本的一部分(在泛型出现之前),从功能上说,委托就是一种对方法的引用,类似于函数指针,或者说,委托是对方法的一种分类引用,相同参数类型和返回类型的方法视为一类,可被同一种类型的委托引用。

  delegate

  声明委托使用delegate关键字,例如:   

    //无返回值委托类型
    public delegate void EventHandler(object? sender, EventArgs e);
    //返回值委托类型
    public delegate Assembly? ResolveEventHandler(object? sender, ResolveEventArgs args);

  委托还可与泛型一起使用,例如使用很多的返回值的Action委托和有返回值的Func委托:  

    //无返回值的Action委托
    public delegate void Action();
    public delegate void Action<in T>(T arg);
    public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

    //有返回值的Func委托
    public delegate TResult Func<out TResult>();
    public delegate TResult Func<in T1, out TResult>(T1 arg);
    public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

  上面Action委托和Func 委托的变体可包含多达 16 个输入参数,所以一般时候我们都不用自定义委托,当然如果需要,我们可以自行使用delegate关键字去声明委托来满足我们特殊的需求。

  委托是一种类型,就像class关键字声明的是类类型,而delegate关键字声明的是委托类型,同样我们可以实例化委托类型:  

    class Program
    {
        public int Multiply(int a, int b)
        {
            return a * b;
        }
        public static int Plus(int a, int b)
        {
            return a + b;
        }
        static void Main(string[] args)
        {
            //静态方法转换
            Func<int, int, int> func = Plus;
            //实例方法转换
            Func<int, int, int> func = new Program().Multiply;
            //使用new关键字
            Func<int, int, int> func = new Func<int, int, int>(Plus);
            //使用Lambda表达式
            Func<int, int, int> func = (a, b) => a - b;

            int result = func(1, 2);
        }
    }

  上面使用4中方式来获取一个委托类型的实例,比如将方法作为参数赋值给一个委托类型,在编译时会告知编译器将方法引用转换。还有Lambda表达式,它是一种匿名委托,在Linq中经常永达。

  总之,无论是静态方法还是实例方法,能否转换为一个委托实例取决于方法的参数类型、参数顺序、返回值类型是否与委托类型一致。

  Delegate类和MulticastDelegate类

  委托不仅能类一样实例化,还能像类一样调用方法,也可以创建委托类型的属性,或者将委托类型变量当做参数一样传递,总之,可以将委托类型当做一种特殊的类来使用,而这一点得益于Delegate和MulticastDelegate两个类。

  MulticastDelegate类是Delegate类的子类,但是我们无法直接创建Delegate和MulticastDelegate的子类,因为它是由编译器自动派生的。当使用delegate关键字声明的委托,在编译时会生成一个特定的委托类型,而这个委托类型就是派生MulticastDelegate类,因此我们无需像类一样显示用继承才是使用父类的方法(我们用的数组也是类似原理,派生于Array类)。

  虽然Delegate和MulticastDelegate两个类都是委托的父类,但是不要认为Delegate就是单播类型,MulticastDelegate就是多播类型,事实上,C#中的委托都是多播类型,比如:  

    class Program
    {
        public static void Multiply(int a, int b)
        {
            Console.WriteLine("a*b=" + (a * b));
        }
        public static void Plus(int a, int b)
        {
            Console.WriteLine("a+b=" + (a + b));
        }
        static void Main(string[] args)
        {
            //第一个方法
            Action<int, int> action = Plus;
            //链接第二个方法(多播)
            action += Multiply;

            action(2, 3);

            //输出
            //a+b=5
            //a*b=6
        }
    }

  需要注意的是,如果委托有返回值,那么返回的是最后一个链接方法的值,其他方法的返回值将会被丢弃:  

    class Program
    {
        public static int Multiply(int a, int b)
        {
            Console.WriteLine("a*b=" + (a * b));
            return a * b;
        }
        public static int Plus(int a, int b)
        {
            Console.WriteLine("a+b=" + (a + b));
            return a + b;
        }
        static void Main(string[] args)
        {
            //第一个方法
            Func<int, int, int> func = Plus;
            //链接第二个方法(多播)
            func += Multiply;

            int result = func(2, 3);
            Console.WriteLine("result=" + result);

            //输出
            //a+b=5
            //a*b=6
            //result=6
        }
    }

  注意,这里链接支持+=、-=、+、- 四个运算:  

    +:链接两个委托,并返回新的委托链,类似于数值运算中的+运算
    -:从委托链中剔除一个指定委托,并返回提出后的委托链,如果委托链无任何委托,则返回null,类似于数值运算中的-运算
    +=:先执行+运算,再新的委托链赋值给左边的委托变量,类似于数值运算中的+=运算
    -=:先执行-运算,再新的委托链赋值给左边的委托变量,类似于数值运算中的-=运算

  常用方法与属性

  Invoke、BeginInvoke、EndInvoke

  Invoke是调用委托方法,而BeginInvoke、EndInvoke是它的异步模式,例如:  

    Func<int, int, int> func = Plus;
    int result = func(1, 2);
    //等价于
    int result = func.Invoke(1, 2);

  因为委托类型也是应用类型,那么那也就可能为null,如果直接使用可能导致空指针异常,因此我们常将Invoke方法与null条件运算符(?.)一起使用,而非直接调用:  

    Func<int, int, int> func = null;
    int result = func(1, 2);//将导致空指针异常
    int? result = func?.Invoke(1, 2);//但此时返回值类型可能需要使用可空类型接收

  DynamicInvoke

  DynamicInvoke来自Delegate类,和Invoke方法一样是调用委托用的,只不过DynamicInvoke调用的参数是一个object数组,所以它常和反射结合起来使用,用于参数情况未知的情况:  

    Func<int, int, int> func = Plus;
    int result = (int)func.DynamicInvoke(new object[] { 1, 2 });

  Method、Target  

    Method:多播委托中最后一个委托方法的引用,它是一个System.Reflection.MethodInfo对象
    Target:如果多播委托中最后一个委托方法是实例方法,那么Target就是调用的实例对象,如果是静态方法,那么Target就是null

 

  事件

  事件和委托类似,只不过事件时基于一种广播订阅模式,实际上,事件是建立在对委托的基础之上的,有了委托,我们才能建立起事件模型。

  event

  定义一个事件使用event关键字,事件类型即委托类型,如:  

    //点击事件
    public event EventHandler Click;

  定义了一个事件,同样可以像委托一样进行赋值、四则运算(+=、-=、+、-)等操作,同样可以想委托一样使用Delegate类和MulticastDelegate类中的方法,比如像方法一样触发事件,或者调用Invoke方法触发事件。

  但是事件和委托还是有区别的,事件作为委托的一种特殊用法,和委托的主要却别在于:

  1、事件必须定义在类或者结构体等内部,换句话说,委托与类、结构体等是一个级别的,而事件与方法、属性等是一个级别的

  2、虽然事件可以使用Delegate类和MulticastDelegate类中的方法,但是它仅限在定义这个时间的类或者结构体等内部才能,比如事件的触发(像方法调用直接触发、调用Invoke方法触发)只能在类或者结构体等内部触发,列如:  

    class Program
    {
        static void Main(string[] args)
        {
            var btn = new Button();
            btn.Click += (sender,e) => Console.WriteLine("Click");
            //btn.Click(btn, EventArgs.Empty);//报错
            btn.OnClick();//不报错
        }
    }
    public class Button
    {
        //点击事件
        public event EventHandler Click;

        public void OnClick()
        {
            Click(this, EventArgs.Empty);
        }
    }

  3、事件采用 += 运算表示订阅,使用 -= 表示取消订阅,虽然事件也支持 +、- 运算,但是+、- 运算也只是被限制在定义这个时间的类或者结构体等内部才能使用。

  4、定义的事件时一个引用类型,默认是null,所以在触发前要判断,避免空指针异常,建议采用Invoke方法与null条件运算符(?.)一起使用来触发事件,事件可以使用 = 进行赋值,但是同样被限制在定义这个时间的类或者结构体等内部才能使用。

  5、标准的事件模型应该没有返回值,确实需要返回值时,应该改用参数的引用传递特性来返回数据。

  一个完整简单的事件例子:  

    class Program
    {
        static void Main(string[] args)
        {
            MessageProducer messageProducer = new MessageProducer();

            Subscriber zhangsan = new Subscriber("张三");
            Subscriber lisi = new Subscriber("李四");
            Subscriber wangwu = new Subscriber("王五");

            messageProducer.Subscribe(zhangsan);
            messageProducer.Subscribe(lisi);
            messageProducer.Subscribe(wangwu);

            messageProducer.Produce("hello");

            messageProducer.Unsubscribe(lisi);

            messageProducer.Produce("hello again");
        }
    }

    public delegate void MessageProducerHandler(string message);
    public class MessageProducer
    {
        public event MessageProducerHandler MessageHandle;

        /// <summary>
        /// 订阅
        /// </summary>
        /// <param name="subscriber"></param>
        public void Subscribe(Subscriber subscriber)
        {
            Console.WriteLine($"【{subscriber.Name}】订阅了消息");
            MessageHandle += subscriber.OnMessage;
        }
        /// <summary>
        /// 取消订阅
        /// </summary>
        /// <param name="subscriber"></param>
        public void Unsubscribe(Subscriber subscriber)
        {
            Console.WriteLine($"【{subscriber.Name}】取消了消息订阅");
            MessageHandle -= subscriber.OnMessage;
        }

        /// <summary>
        /// 发布一条消息
        /// </summary>
        /// <param name="message"></param>
        public void Produce(string message)
        {
            Console.WriteLine($"发布消息【{message}】");
            MessageHandle?.Invoke(message);
        }
    }

    public class Subscriber
    {
        public Subscriber(string name)
        {
            Name = name;
        }

        /// <summary>
        /// 订阅者名称
        /// </summary>
        public string Name { get; }

        /// <summary>
        /// 消息处理方法
        /// </summary>
        /// <param name="message"></param>
        public void OnMessage(string message)
        {
            Console.WriteLine($"【{Name}】收到消息【{message}】");
        }
    }

  打印结果:

  

  

  总结

  我们可以认为事件时委托的一种特殊用法,什么时候用委托,什么时候用事件,总结如下:

  1、如果需要像一个变量使用,在某段代码后执行回调,应该使用委托,比如 linq 的语法

  2、如果需要有返回值,应该采用委托

  3、如果需求表现出一次订阅,长时间内接收多个信息的特性,应该使用事假模型。

 

  参考文档:https://docs.microsoft.com/en-us/dotnet/csharp/delegates-overview

 

posted @ 2022-08-26 20:19  没有星星的夏季  阅读(3125)  评论(0编辑  收藏  举报