【C#】委托与事件与EventHandler

ban-boi-making-dinner / 2024-12-19 / 原文

委托

C# 中的委托(Delegate)是一种类型安全的函数指针,它允许你将方法作为参数传递给其他方法。委托可以引用一个或多个方法,并且可以在运行时调用这些方法。它们是实现事件和回调的基础。

 

委托的基本概念

  • 定义委托:首先需要定义一个委托类型,这指定了可被委托调用的方法签名。
  • 实例化委托:创建一个委托实例,该实例指向具体的一个或多个方法。
  • 使用委托:通过委托实例来调用其关联的方法。

 

定义委托

要定义一个委托,你需要使用 delegate 关键字。委托定义语句中的返回值类型和参数类型,规定了这个委托的实例可以指向的方法的返回值类型和参数类型。例如:

public delegate void MyDelegate(string message);

这里定义了一个名为 MyDelegate 的委托,它可以用来引用任何带有单个 string 参数并且没有返回值的方法。

 

实例化委托和使用委托

一旦定义了委托类型,你可以创建该类型的实例并让它指向某个方法。例如:

public class Program
{
    public static void ShowMessage(string message)
    {
        Console.WriteLine(message);
    }

    public static void Main()
    {
        // 创建委托实例并指向ShowMessage方法
        MyDelegate myDelegate = new MyDelegate(ShowMessage);

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

通过委托调用方法也可以用我们经常在事件中看到的:

// 使用 Invoke 调用方法
myDelegate?.Invoke("Hello, World!");

(我们常见的事件触发,实际上是调用了委托实例的 Invoke 方法)

如果不确定委托是否可能为空,使用 myDelegate?.Invoke(message) 更加安全,可以避免空引用异常。

 

在这个例子中,myDelegate 是一个委托实例,它被初始化为指向 ShowMessage 方法。之后可以通过 myDelegate 来间接调用 ShowMessage 方法。

 

多播委托

C# 中的委托还支持多播(即一个委托对象可以包含对多个方法的引用)。当你调用一个多播委托时,所有绑定到该委托的方法都会按顺序执行。

public static void AnotherMethod(string message)
{
    Console.WriteLine($"Another: {message}");
}

public static void Main()
{
    MyDelegate first = new MyDelegate(ShowMessage);
    MyDelegate second = new MyDelegate(AnotherMethod);

    // 合并两个委托
    MyDelegate combined = first + second;

    // 调用combined会依次执行ShowMessage和AnotherMethod
    combined("Hello, Multicast!");
}

 

事件

事件是一个使用event关键字声明的成员,它基于委托类型。委托可以用来定义和处理事件。

为什么说它基于委托类型?这里详细讲讲。

你可以自定义一个委托类型并使用它来定义事件。由上文可知:在 C# 中,委托是一种类型安全的函数指针,允许方法作为参数传递。当你定义一个事件时,实际上是在定义一个委托类型的实例,它用于表示将被触发的事件的处理方法。

具体来说,事件是通过委托来实现的,这样可以确保只有在特定条件下,某个方法才会响应事件,从而实现事件驱动编程模型。例如,你可以声明一个委托类型来表示事件处理程序,然后通过 event 关键字将其与某个事件关联。

事件的基本概念

  1. 事件的拥有者:这是声明事件的对象或类。例如,在一个计时器类中,计时器本身是事件的拥有者。

  2. 事件本身:这是一个使用event关键字声明的成员,它基于委托类型。当特定条件满足时,事件会被触发(即调用所有注册到该事件的处理方法)。

  3. 事件处理器:这是响应事件的方法。它们是与事件关联的函数,当事件被触发时,这些方法将被执行。

  4. 事件订阅:这是将事件处理器添加到事件的过程。通过+=操作符来完成订阅,而-=操作符用来取消订阅。

  5. 事件触发:这是执行所有已注册的事件处理器的行为。通常是通过调用委托实例来实现的。

 

事件的工作原理

  • 当一个事件被定义后,它可以像字段一样被初始化,但它是不可直接访问的(只能通过+=-=进行订阅/退订)。这是因为event关键字提供了额外的封装,确保了外部代码不能直接修改事件的内部状态。

  • 事件本质上是一个委托类型的字段,这意味着它可以指向一个或多个方法。当事件被触发时,所有注册到该事件的方法都会按顺序执行。

  • 事件通常会携带一些数据给事件处理器。这通过创建自定义的EventArgs派生类来实现,然后将其作为参数传递给事件处理器。

 

委托和事件

到这里,大家应该已经意识到事件与上文中所提到的委托在一些行为上的相似

这里再举两个例子对比一下,这个是一个事件的声明、订阅、触发:

public class MyClass
{
    // 定义委托类型
    public delegate void MyEventHandler(string message);

    // 声明事件
    public event MyEventHandler MyEvent;

    public void TriggerEvent(string message)
    {
        // 触发事件
        MyEvent?.Invoke(message);
    }
}

public class Program
{
    public static void Main()
    {
        MyClass obj = new MyClass();

        // 订阅事件(通过+=添加事件处理程序)
        obj.MyEvent += MyEventHandlerMethod;

        // 触发事件
        obj.TriggerEvent("Hello, world!");
    }

    // 事件处理程序
    public static void MyEventHandlerMethod(string message)
    {
        Console.WriteLine($"Event triggered: {message}");
    }
}

这个是用一个委托实现相同的效果:

public class MyClass
{
    // 定义委托类型
    public delegate void MyEventHandler(string message);

    // 直接声明一个委托实例
    public MyEventHandler MyEvent;

    public void TriggerEvent(string message)
    {
        // 触发(直接调用委托实例的 Invoke 方法)
        MyEvent?.Invoke(message);
    }
}

public class Program
{
    public static void Main()
    {
        MyClass obj = new MyClass();

        // 创建委托实例并添加事件处理程序
        obj.MyEvent = new MyClass.MyEventHandler(MyEventHandlerMethod);

        // 触发事件
        obj.TriggerEvent("Hello, world!");
    }

    // 事件处理程序
    public static void MyEventHandlerMethod(string message)
    {
        Console.WriteLine($"Event triggered: {message}");
    }
}

在事件语句中,obj.MyEvent += MyEventHandlerMethod; 是将事件处理程序方法添加到事件的委托实例中。事件实际上是基于委托来工作的,它为委托实例提供了一种封装,使得外部代码无法直接操作委托实例,只能通过事件的 +=-= 来订阅和取消订阅方法。

 

事件是对委托的一种封装,并且通过 event 关键字,C# 提供了一些额外的语法糖来使事件更加安全、简洁。除了语法上的一些糖外,事件和委托之间还有一些重要的区别。

1. 委托与事件的语法糖区别

  • 委托:委托只是一个类型,它允许你存储方法的引用,并直接调用这些方法。

    public delegate void MyDelegate(string message);
    MyDelegate myDelegate = MyEventHandlerMethod;
    myDelegate("Hello"); // 直接调用
    
  • 事件:事件基于委托,但 C# 提供了额外的封装来限制事件的操作,防止外部代码直接修改事件的委托实例。外部代码只能通过 +=-= 来订阅或取消订阅事件,无法直接将方法赋值给事件。

    public event MyDelegate MyEvent; // 事件声明
    
    // 订阅事件
    MyEvent += MyEventHandlerMethod;
    
    // 触发事件
    MyEvent?.Invoke("Hello"); // 通过事件触发
    

    事件是对委托的进一步封装,尤其是在安全性和事件的管理上。

2. 事件与委托的区别:

除了语法糖的不同,事件和委托的根本区别在于它们的行为和访问权限。

1. 委托实例的修改权限:

  • 委托:委托实例是可以直接修改的,外部代码可以直接给委托赋值或修改它的 Invoke 调用列表。

    MyClass.MyEventHandler myDelegate = MyEventHandlerMethod;
    obj.MyEvent = myDelegate;  // 直接给事件赋值
    
  • 事件:事件的委托实例不能被直接修改。只有事件的声明者(通常是类的内部)才能直接改变事件的委托实例,外部代码只能通过 +=-= 订阅和取消订阅事件处理方法。这样可以防止外部代码不小心(或者故意)覆盖事件的委托实例。

    obj.MyEvent += MyEventHandlerMethod;  // 外部只能通过 += 订阅事件
    // obj.MyEvent = myDelegate;  // 错误:事件不能直接赋值
    

2. 事件的封装性:

  • 委托:委托实例是可以公开的,可以在类外部任意访问和修改。
  • 事件:事件对外提供了封装,外部代码不能直接访问事件的委托实例,而只能通过事件的 +=-= 语法进行订阅和取消订阅,这种封装机制帮助保护事件的管理,避免外部代码意外或恶意地改变事件处理程序。

3. 事件的多播:

  • 委托:委托支持多播,即一个委托可以指向多个方法,但这种多播行为需要开发者显式管理。
  • 事件:事件本质上也是多播委托,但 C# 提供了更强的管理能力,事件的多播委托通过 +=-= 操作符进行管理。C# 自动处理事件处理程序的添加和删除,并确保事件不会被错误地清空或覆盖。

4. 委托的直接调用 vs 事件的间接触发:

  • 委托:可以直接通过调用 Invoke 方法触发所有绑定的委托处理程序。

    myDelegate.Invoke("Message");
    
  • 事件:事件不能直接调用 Invoke 方法。必须通过事件的触发机制(即通过事件的声明者)来调用绑定的委托处理程序。

    MyEvent?.Invoke("Message");
    

3. 事件的优势

事件相对于直接使用委托有一些优势,特别是在封装和安全性方面:

  • 封装性:通过 event,C# 对委托进行了封装,确保只有事件的声明者可以直接修改委托实例,外部代码只能订阅和取消订阅方法。
  • 事件的管理:事件管理更加方便且安全,避免了委托实例被外部代码随意修改的风险。
  • 防止误操作:事件防止外部代码直接访问或覆盖事件的委托实例,确保不会无意中丢失或覆盖绑定的事件处理程序。
  • 多播管理:事件的多播处理比委托更加简洁,外部代码可以通过 +=-= 轻松管理多个事件处理程序。

4. 总结

  • 委托 是 C# 中一种引用类型,它可以表示一个方法的签名,并允许将方法作为参数传递或者直接调用。
  • 事件 是对委托的封装,它不仅具有委托的多播特性,还通过 event 关键字提供了额外的安全性,防止外部代码直接修改事件的委托实例。
  • 事件通过 +=-= 提供了对委托的安全管理,使得订阅和取消订阅事件处理程序变得更加简洁和安全。

简而言之,事件确实是对委托的封装,提供了额外的安全性和更清晰的语法。

 

EventHandler

在 C# 中,EventHandler 是一种特殊的委托类型,专门用于事件处理。它定义在 System 命名空间中,并且通常用来实现发布-订阅模式,这是 .NET 框架中处理事件的标准方式。

EventHandler 的定义如下:

public delegate void EventHandler(object sender, EventArgs e);

这里有几个关键点需要注意:

  1. sender 参数:这是一个 object 类型的参数,代表触发事件的对象。这通常是发出事件的那个对象实例(比如一个按钮控件)。

  2. EventArgs 参数:这是一个 EventArgs 类型的参数,提供了与事件相关的数据。对于不需要传递额外信息的基本事件,可以使用这个基类。如果需要携带更多信息,则可以继承自 EventArgs 创建特定的事件参数类。

使用示例

下面是一个简单的例子,展示如何使用 EventHandler 来定义和处理事件:

using System;

// 定义一个自定义的EventArgs类来携带额外的数据
public class MyEventArgs : EventArgs
{
    public string Message { get; set; }

    public MyEventArgs(string message)
    {
        Message = message;
    }
}

// 一个类,包含一个事件
public class EventPublisher
{
    // 定义事件
    public event EventHandler<MyEventArgs> MyEvent;

    // 触发事件的方法
    protected virtual void OnMyEvent(MyEventArgs e)
    {
        MyEvent?.Invoke(this, e);
    }

    // 产生事件的动作
    public void DoSomething()
    {
        Console.WriteLine("Doing something...");
        // 在某些条件下触发事件
        OnMyEvent(new MyEventArgs("Something happened!"));
    }
}

class Program
{
    static void Main()
    {
        var publisher = new EventPublisher();

        // 订阅事件
        publisher.MyEvent += (sender, e) => 
            Console.WriteLine($"Event received: {e.Message}");

        // 执行操作,可能会触发事件
        publisher.DoSomething();
    }
}

在这个例子中:

  • 我们定义了一个 MyEventArgs 类来携带事件数据。
  • EventPublisher 类有一个 MyEvent 事件,当调用 DoSomething 方法时,该事件会被触发。
  • 在主程序中,我们创建了 EventPublisher 的一个实例,并为 MyEvent 事件添加了一个处理器(lambda 表达式),该处理器将在事件被触发时打印一条消息。

通过这种方式,EventHandler 和它的泛型版本 EventHandler<TEventArgs> 提供了一种标准化的方式来处理事件,使得组件之间的通信更加松散耦合。


通过显示委托将事件处理方法订阅到事件(Handler常用)

在实际使用中,我们可以先创建一个委托实例(evenHandler),并让它指向某个方法(事件处理方法),然后再将这个委托实例添加到事件的订阅中。这种方法也很常见,尤其是在以下几种场景下:

  1. 需要在多个地方使用同一个委托实例: 如果你需要在多个地方重用同一个委托实例,创建一个委托实例并绑定到事件是一个常见的做法。这样,你可以控制委托的生命周期,并且可以在多个事件中使用相同的处理方法。

  2. 事件订阅时需要更多的控制: 在某些情况下,可能需要为事件处理程序提供更多的控制,例如传递额外的参数或对方法进行封装。此时,通过创建一个委托实例并将其绑定到事件,可以提供更灵活的控制。

让我们通过一个实际的例子来进一步分析:

1. 通过创建委托实例并将其指向方法来订阅事件

public class MyClass
{
    // 定义委托类型
    public delegate void MyEventHandler(string message);

    // 声明事件
    public event MyEventHandler MyEvent;

    public void TriggerEvent(string message)
    {
        // 触发事件
        MyEvent?.Invoke(message);
    }
}

public class Program
{
    public static void Main()
    {
        MyClass obj = new MyClass();

        // 创建委托实例并将其指向事件处理方法
        MyClass.MyEventHandler eventHandler = new MyClass.MyEventHandler(MyEventHandlerMethod);

        // 将委托实例订阅到事件
        obj.MyEvent += eventHandler;

        // 触发事件
        obj.TriggerEvent("Hello, world!");
    }

    // 事件处理方法
    public static void MyEventHandlerMethod(string message)
    {
        Console.WriteLine($"Event triggered: {message}");
    }
}

在这个例子中,MyClass.MyEventHandler 是我们定义的委托类型。我们首先创建了一个委托实例 eventHandler,并将它指向 MyEventHandlerMethod 这个方法。然后我们使用 += 操作符将 eventHandler 委托实例添加到 obj.MyEvent 事件中。最后,我们触发事件时,eventHandler 会执行 MyEventHandlerMethod

2. 委托实例与直接通过 += 订阅事件的区别

通过委托实例和直接订阅事件(通过 +=)本质上是等效的。只是通过委托实例订阅事件提供了更多的灵活性和控制,尤其在以下情况下:

  • 控制委托实例的生命周期:如果你需要管理委托实例,尤其是在多次订阅同一事件的场景下,使用委托实例更加方便。
  • 对事件处理进行更复杂的封装或管理:通过委托实例,你可以为方法添加一些逻辑,比如通过 new MyDelegate(MyEventHandlerMethod) 将方法封装在特定的委托类型下,或者使用匿名方法或 lambda 表达式。

3. 直接通过 += 订阅事件(简化形式)

这是一种更简单和常见的方式:

public class MyClass
{
    // 定义委托类型
    public delegate void MyEventHandler(string message);

    // 声明事件
    public event MyEventHandler MyEvent;

    public void TriggerEvent(string message)
    {
        // 触发事件
        MyEvent?.Invoke(message);
    }
}

public class Program
{
    public static void Main()
    {
        MyClass obj = new MyClass();

        // 直接通过 += 订阅事件处理方法
        obj.MyEvent += MyEventHandlerMethod;

        // 触发事件
        obj.TriggerEvent("Hello, world!");
    }

    // 事件处理方法
    public static void MyEventHandlerMethod(string message)
    {
        Console.WriteLine($"Event triggered: {message}");
    }
}

在这种方式中,我们直接通过 += 订阅事件,无需显式创建委托实例。这种方式非常简洁,适用于大多数情况。

4. 总结

  • 通过委托实例订阅事件:这种方法允许你显式地控制委托实例,尤其在你需要重用同一个委托实例、封装委托或对委托实例有更多控制时是非常有用的。你首先创建委托实例,然后将它与事件关联。

  • 直接通过 += 订阅事件:这种方式是最常见的,简洁且方便。它不需要显式地创建委托实例,直接通过事件处理方法来订阅。

这两种方式本质上是等效的,选择使用哪种方式取决于具体的需求和场景。

这样做允许开发者将逻辑(在这个例子中是对到达的数据包进行处理)与事件触发分离,使得代码更加模块化和易于管理。

 

为什么要使用显示的委托而不是直接绑定方法

尽管直接绑定方法到事件是简洁和直观的方式,但在某些情况下使用显式的委托变量(如你的例子中的arrivalEventHandler)可以带来一些好处:

  1. 可重用性:如果你需要多次订阅或取消订阅同一个事件处理程序,使用委托变量可以使代码更清晰。例如,你可以轻松地从多个地方引用相同的委托实例。

  2. 解耦合:通过委托变量,你可以更容易地在运行时改变事件处理器。比如,你可以在某个条件满足时更换事件处理器,而不需要重新设置整个事件订阅。

  3. 取消订阅方便:如果你想在某个时刻取消对事件的订阅,拥有委托变量会使这个过程更加明确和简单。例如:

    device.OnPacketArrival -= arrivalEventHandler;
  4. 调试和维护:使用显式的委托可以让代码更加易于理解和维护。特别是当事件处理器逻辑比较复杂时,可以通过委托变量更好地组织代码。

  5. 传递额外状态:如果你需要向事件处理器传递额外的状态信息,可以通过委托闭包来实现。虽然这不是你当前示例的情况,但它展示了委托的一种强大功能。