跳转至

委托与事件⚓︎

一、委托概述⚓︎

委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。

委托机制可以使方法作为参数进行传递。 尤其在事件处理程序中经常使用。你可以创建一个自定义方法,当发生特定事件时,某个类(如 Windows 控件)就可以调用你的方法。

Tip

不要试图把委托理解为方法,委托就是委托,声明的委托都隐式地继承自 System.MulticastDelegate,它的定位可以类比与结构体、枚举、类、接口等。

二、使用委托⚓︎

01 声明委托⚓︎

通过 delegate 关键字声明一个委托,如:

public delegate void Del(string message);

Note

请注意,语法可能看起来像是声明变量,但它实际上是声明类型。 可以在类中、直接在命名空间中、甚至是在全局命名空间中定义委托类型。但是不建议直接在全局命名空间中声明委托类型(或其他类型)。

namespace TestClass
{
    delegate void Del(string message); // 在命名空间下声明委托

    internal class A
    {
        // class body
    }
}

02 实例化委托⚓︎

实例化委托也可以理解为绑定委托,即将一个方法绑定给委托,然后由委托执行。

实例化委托的方法有很多:

  1. 通过new与签名匹配的方法实例化(上述示例的代码中采用该方法);
  2. 通过将方法分配给委托来实例化;
  3. 通过匿名方法实例化委托;
  4. 通过Lambda表达式实例化委托;
Del del = new Del(DelegateMethod);
Del del = DelegateMethod;
Del del = delegate(string message){ Console.WriteLine(message); };
Del del = message => { Console.WriteLine(message); };

Warning

通常,实例化委托时的方法应具有与委托相同的签名,但C#也允许协变与逆变,更多可参考使用委托中的变体 (C#) | Microsoft Learn

03 调用委托⚓︎

对委托进行实例化后,委托会将对其进行的方法调用传递到该方法。 调用方传递到委托的参数将传递到该方法,并且委托会将方法的返回值(如果有)返回到调用方, 这被称为调用委托。 实例化的委托可以按封装的方法本身进行调用。

委托调用
// 声明委托
public delegate void Del(string message);

// 创建方法
public static void DelegateMethod(string message)
{
    Console.WriteLine(message);
}

// 实例化委托:将方法绑定到委托
Del handler = new Del(DelegateMethod);

// 调用委托:传入委托的参数会传递给委托绑定的方法
handler("Hello World");

// Output:
// Hello World

04 委托实现方法作参数⚓︎

由于 实例化的委托是一个对象,因此可以作为实参传递或分配给一个属性, 允许方法接受委托作为参数并在稍后调用委托。 这被称为异步回调,是在长进程完成时通知调用方的常用方法。 当以这种方式使用委托时,使用委托的代码不需要知道要使用的实现方法。

委托表示的是方法的引用,同时,委托的实例化是一个对象,因此委托机制能够将方法作为参数传递。如果想要将一个方法作为实参传入到另一个方法中,应当使用委托来完成。

下面通过一个示例来演示委托的用途:

方法作参数
namespace DelegateApp
{  
   class PrintString  
   {  
      static FileStream fs;  
      static StreamWriter sw;  
      // 委托声明  
      public delegate void printString(string s);  

      // 该方法打印到控制台  
      public static void PrintToScreen(string str)  
      {  
         Console.WriteLine("字符串被输出到控制台: {0}", str);  
      }
        
      // 该方法打印到文件(模拟)
      public static void PrintToFile(string s)  
      {
         Console.WriteLine("字符串被输出到文件: {0}", str);  
      }
      
      // 该方法把委托作为参数,并使用它调用方法  
      public static void sendString(printString ps)  
      {
         ps("Hello World");// 委托是方法的应用,调用委托相当于调用方法
      }
      
      static void Main(string[] args)  
      {
         printString ps1 = new printString(PrintToScreen);  
         printString ps2 = new printString(PrintToFile);  
         sendString(ps1);
         sendString(ps2);  
         Console.ReadKey();  
      }
   }
}
// Output:
// 字符串被输出到控制台:Hello World
// 字符串被输出到文件:Hello World

三、多播委托⚓︎

01 委托的合并与调用⚓︎

多播委托也称为合并委托,多播委托允许通过 + - 运算符将多个方法分配到同一个委托实例,此时会形成委托列表,当多播委托被调用时,会依次执行列表中的方法。

多播委托示例
using System;

// Define a custom delegate that has a string parameter and returns void.
delegate void CustomDel(string s);

class TestClass
{
    // Define two methods that have the same signature as CustomDel.
    static void Hello(string s)
    {
        Console.WriteLine($"  Hello, {s}!");
    }

    static void Goodbye(string s)
    {
        Console.WriteLine($"  Goodbye, {s}!");
    }

    static void Main()
    {
        // Declare instances of the custom delegate.
        CustomDel hiDel, byeDel, multiDel, multiMinusHiDel;

        // Initialize the delegate object hiDel that references the
        // method Hello.
        hiDel = Hello;

        // Initialize the delegate object byeDel that references the
        // method Goodbye.
        byeDel = Goodbye;

        // The two delegates, hiDel and byeDel, are combined to
        // form multiDel.
        multiDel = hiDel + byeDel;

        // Remove hiDel from the multicast delegate, leaving byeDel,
        // which calls only the method Goodbye.
        multiMinusHiDel = multiDel - hiDel;

        Console.WriteLine("Invoking delegate hiDel:");
        hiDel("A");
        Console.WriteLine("Invoking delegate byeDel:");
        byeDel("B");
        Console.WriteLine("Invoking delegate multiDel:");
        multiDel("C");
        Console.WriteLine("Invoking delegate multiMinusHiDel:");
        if(multiMinusHiDel != null){
            multiMinusHiDel("D");
        }
    }
}
/* Output:
Invoking delegate hiDel:
  Hello, A!
Invoking delegate byeDel:
  Goodbye, B!
Invoking delegate multiDel:
  Hello, C!
  Goodbye, C!
Invoking delegate multiMinusHiDel:
  Goodbye, D!
*/

Danger

调用多播委托是应进行判空操作,如果委托列表为空仍然调用多播委托会阻塞当前进程。

02 多播委托的返回值⚓︎

多播委托会只将最后一个委托方法的结果输出来。

合并委托的返回值
namespace TestMultiDelegate
{
    public delegate int CustomDel();

    internal class MultiDelegate
    {
        public static int D1()
        { return 1; }
        public static int D2()
        { return 2; }
        public static int D3()
        { return 3; }
    }
    private static void Main(string[] args)
    {
        // Declare instances of the custom delegate.
        CustomDel multiDel;
        multiDel = D1;
        multiDel += D2;
        multiDel += D3;
        Console.WriteLine(multiDel());
    }
}
// Output:
// 3

四、事件⚓︎

01 事件概述⚓︎

事件(Event)是指一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件,例如函数回调,中断程序等。

事件是使用委托的语言支持构建的,C# 中使用事件机制实现线程间的通信。

02 发布订阅⚓︎

事件由发布-订阅两部分组成,包含事件的一部分称为发布器,接收事件并提供事件处理程序的对象称为订阅器。

发布器决定何时触发事件,订阅器决定对事件触发后作出何种响应。通过 += 运算符添加订阅,通过 -= 运算符取消订阅。

下面的示例演示事件:

事件演示
using System;

namespace DelegateDemo
{
    //定义猫叫委托
    public delegate void CatCallEventHandler();
    public class Cat
    {
        //定义猫叫事件
        public event CatCallEventHandler CatCall;
        public void OnCatCall()
        {
            Console.WriteLine("猫叫了一声");
            // 猫叫触发后发布通知
            CatCall?.Invoke();
        }
    }
    public class Mouse
    {
        //定义老鼠跑掉方法
        public void MouseRun()
        {
            Console.WriteLine("老鼠跑了");
        }
    }
    public class People
    {
        //定义主人醒来方法
        public void WakeUp()
        {
            Console.WriteLine("主人醒了");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Cat cat = new Cat();
            Mouse m = new Mouse();
            People p = new People();

            // 添加订阅
            cat.CatCall += new CatCallEventHandler(m.MouseRun);
            cat.CatCall += new CatCallEventHandler(p.WakeUp);

            // 猫叫事件触发
            cat.OnCatCall();
        }
    }
}

Note

  1. 一个事件可以拥有多个订阅器,一个订阅器也可以处理多个发布器事件;
  2. 订阅器可以有多个,当事件被触发时会同步调用所有的事件处理程序;
  3. 没有订阅器的事件永远也不会触发;
  4. 在 .NET 类库中,事件基于 EventHandler 委托和 EventArgs 基类。