Skip to content

委托与事件

委托

在 C# 中,委托(Delegate)是一种类型安全的对象,它定义了一种引用方法的方式。

委托可以存储对方法的引用,并且能够在运行时调用这些方法。

委托是一种类型,类似于 C++ 中的函数指针,但它更加安全和灵活。

委托的定义

委托是 C# 中的引用类型,定义了一种方法签名。

语法:

C#
public delegate 返回类型 委托名(参数列表);

示例:

C#
// 定义委托
public delegate void PrintDelegate(string message);

class Program
{
    // 定义一个方法与委托签名相匹配
    public static void PrintMessage(string message)
    {
        Console.WriteLine(message);
    }

    static void Main(string[] args)
    {
        // 实例化委托,并将方法分配给它
        PrintDelegate print = new PrintDelegate(PrintMessage);

        // 通过委托调用方法
        print("Hello, World!");
    }
}

特性:

  1. 类型安全:委托在编译时检查方法的签名(返回类型和参数),保证类型安全。
  2. 多播委托:委托可以引用多个方法,并依次调用它们。
  3. 异步调用:委托支持异步调用,通过 BeginInvoke 和 EndInvoke 可以异步执行方法。
  4. 封装方法:委托提供了一种将方法作为参数传递的方式,可以实现灵活的回调机制。

C# 中有三种主要的委托类型:

  1. 单播委托:只引用一个方法。
  2. 多播委托:引用多个方法,依次调用它们。
  3. 泛型委托:C# 提供了几个常用的泛型委托,如 Action<> 和 Func<>,用于简化委托的定义。

单播委托

单播委托(Single-cast Delegate)是指只引用一个方法的委托。

示例:

C#
public delegate int CalculateDelegate(int x, int y);

class Program
{
    public static int Add(int x, int y) => x + y;
    public static int Subtract(int x, int y) => x - y;

    static void Main(string[] args)
    {
        // 创建单播委托,引用 Add 方法
        CalculateDelegate calculate = Add;
        Console.WriteLine("Add: " + calculate(10, 5));  // 输出: 15

        // 重新指向 Subtract 方法
        calculate = Subtract;
        Console.WriteLine("Subtract: " + calculate(10, 5));  // 输出: 5
    }
}

特性:

  1. 只能引用一个方法:单播委托只能存储对一个方法的引用,调用时会执行该方法。
  2. 强类型安全:单播委托确保方法签名与委托类型匹配,因此编译时进行类型检查。
  3. 灵活性:即使是单播委托,依然可以通过动态的方式指定不同的方法,提供了一定的灵活性。

多播委托

委托可以引用多个方法,称为多播委托。通过 += 操作符可以将多个方法添加到一个委托中,委托会依次调用这些方法。

示例:

C#
public delegate void PrintDelegate(string message);

class Program
{
    public static void PrintMessage1(string message)
    {
        Console.WriteLine("Message 1: " + message);
    }

    public static void PrintMessage2(string message)
    {
        Console.WriteLine("Message 2: " + message);
    }

    static void Main(string[] args)
    {
        // 创建委托实例并关联多个方法
        PrintDelegate print = PrintMessage1;
        print += PrintMessage2;

        // 调用委托,依次执行 PrintMessage1 和 PrintMessage2
        print("Hello!");
    }
}

特性:

  1. 引用多个方法:多播委托可以引用多个方法,每次调用委托时,所有被引用的方法都会依次执行。
  2. 顺序执行:委托按添加的顺序依次执行其引用的方法。
  3. 返回值处理:如果委托有返回值,只有最后一个方法的返回值会被保留,前面的返回值会被忽略。
  4. 删除方法:可以使用 -= 操作符从多播委托中移除方法。

多播委托的应用场景:

  1. 事件处理:多播委托通常用于事件机制中,一个事件可能有多个处理程序(事件处理器)。
  2. 通知机制:在需要广播消息或通知多个接收者时,可以使用多播委托来调用多个处理函数。
  3. 链式调用:当需要依次执行一系列操作时,可以使用多播委托。

泛型委托

泛型委托是指在 C# 中使用泛型类型参数定义的委托,它允许委托在调用时对不同的数据类型进行操作,而无需为每种类型定义不同的委托类型。泛型委托通过在委托声明中引入类型参数,使得同一个委托可以适用于多种类型。

常见的泛型委托类型 C# 提供了两种常用的泛型委托类型:

  1. Func<T, TResult>:表示有返回值的委托。
  2. Action<T>:表示没有返回值的委托。

应用场景

  1. 泛型算法:当算法逻辑与类型无关时,可以使用泛型委托实现通用的算法。
  2. 回调机制:泛型委托用于事件回调机制时,可以处理不同类型的数据,而不必为每种类型定义不同的委托。
  3. LINQ:在 LINQ 查询中,泛型委托(如 Func 和 Predicate)被广泛用于操作集合。

更多应用参考lambda表达式和匿名方法

Func<T, TResult>

Func 是一个有返回值的泛型委托,最多可以有 16 个输入参数,最后一个泛型参数是返回类型。

C#
Func<int, int, int> add = (x, y) => x + y;
int result = add(3, 4);  // 输出: 7
Console.WriteLine(result);

Action<T>

Action 是一个没有返回值的泛型委托,最多可以有 16 个输入参数,但不返回任何结果。

C#
Action<string> printMessage = message => Console.WriteLine(message);
printMessage("Hello from Action!");

自定义泛型委托

除了使用内置的 Func、Action 和 Predicate,你还可以自己定义泛型委托。

C#
public delegate T Calculate<T>(T x, T y);

class Program
{
    static int Add(int x, int y) => x + y;
    static double Multiply(double x, double y) => x * y;

    static void Main()
    {
        Calculate<int> add = Add;
        Console.WriteLine(add(3, 4));  // 输出: 7

        Calculate<double> multiply = Multiply;
        Console.WriteLine(multiply(2.5, 4.0));  // 输出: 10.0
    }
}

事件

事件(Event)是 C# 中的一种特殊成员,用于封装委托,并用于发布-订阅模式的实现。

事件通常用于通知机制,让一个对象在某个动作发生时,通知其它对象(事件监听器)。

事件本质上是对委托的封装,但提供了更严格的访问控制。

事件的特点

  1. 发布-订阅模式:事件是发布-订阅模式的核心,允许一个对象(事件发布者)通知多个对象(事件订阅者)。
  2. 基于委托:事件基于委托,每个事件都关联一个委托,事件的触发实际上是委托的调用。
  3. 访问控制:事件只能在声明它的类内部触发(调用),而在类外部只能订阅或取消订阅事件,不能直接触发事件。
  4. 类型安全:事件可以确保订阅者只能传入与事件类型匹配的方法,提供了类型安全性。

应用场景

  1. UI 编程:事件广泛用于图形用户界面(GUI)编程中,按钮点击、输入框变化等都会触发事件。
  2. 通知机制:在复杂的业务逻辑中,事件用于通知不同模块或组件执行相应的操作。
  3. 异步编程:事件通常与异步操作相关联,异步完成时触发事件通知。

事件的使用

定义事件需要定义一个委托,事件使用该委托作为它的类型。

可以使用内置的 EventHandler 委托,也可以定义自定义的委托。

在事件发布者类之外,可以通过事件对象订阅该事件。订阅者需要传入与事件委托签名匹配的方法。

内置事件委托

C# 提供了内置的事件委托 EventHandler,用于定义没有返回值并接受两个参数的事件。第一个参数是事件的发送者,第二个参数是事件数据。

C#
public class Publisher
{
    // 使用内置的 EventHandler 委托
    public event EventHandler<EventArgs> NotifyEvent;

    public void Notify()
    {
        // 触发事件,并传入事件发送者和事件数据
        NotifyEvent?.Invoke(this, EventArgs.Empty);
    }
}

public class Subscriber
{
    public void OnNotifyReceived(object sender, EventArgs e)
    {
        Console.WriteLine("Event received.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Publisher publisher = new Publisher();
        Subscriber subscriber = new Subscriber();

        // 订阅事件
        publisher.NotifyEvent += subscriber.OnNotifyReceived;

        // 触发事件
        publisher.Notify();
    }
}

自定义事件参数

如果需要传递更多的事件数据,可以定义自己的事件参数类,继承自 EventArgs。

C#
// 自定义事件参数类
public class NotifyEventArgs : EventArgs
{
    public string Message { get; set; }
    public NotifyEventArgs(string message)
    {
        Message = message;
    }
}

public class Publisher
{
    // 使用自定义的事件参数类型
    public event EventHandler<NotifyEventArgs> NotifyEvent;

    public void Notify(string message)
    {
        // 触发事件,并传递自定义的事件参数
        NotifyEvent?.Invoke(this, new NotifyEventArgs(message));
    }
}

public class Subscriber
{
    public void OnNotifyReceived(object sender, NotifyEventArgs e)
    {
        Console.WriteLine("Event received with message: " + e.Message);
    }
}

class Program
{
    static void Main(string[] args)
    {
        Publisher publisher = new Publisher();
        Subscriber subscriber = new Subscriber();

        // 订阅事件
        publisher.NotifyEvent += subscriber.OnNotifyReceived;

        // 触发事件
        publisher.Notify("Hello, Custom EventArgs!");
    }
}

取消对事件的监听

可以使用 -= 操作符可以从事件中移除订阅者,取消对事件的监听。

C#
publisher.NotifyEvent -= subscriber.OnNotifyReceived;

Document Base on VitePress.