CodeGym /课程 /C# SELF /用 delegate 创建事件 (event 和 delegate)

用 delegate 创建事件 (event 和 delegate)

C# SELF
第 52 级, 课程 1
可用

1. 介绍

所以,当我们在 C# 上下文里说到“事件”(event),指的是一种机制,可以安全地通知一个或多个对象某件事发生了。事件不是魔法:底层是 delegate(关键字 delegate),外面加了防止误用的保护。

想象订阅一个 YouTube 频道。频道(事件拥有者对象)有一个“订阅”按钮——任何观众(另一个对象)都可以点它并加入订阅者列表(delegate 的处理器)。当作者发布新视频(触发事件)时,只有订阅者会收到通知,而且只有频道作者决定什么时候触发。重要的一点:观众可以订阅或取消订阅,但不能自己发送通知——只有频道拥有者可以触发事件。

一些正式的语法


// 声明一个 delegate,用来定义事件处理器的签名
public delegate void SimpleEventHandler();

// 基于 delegate 声明 event
public event SimpleEventHandler SomethingHappened;

很简单:event 是用来声明事件的关键字,事件的类型总是由 delegate 指定。

2. 第一个简单例子:事件“按钮被按下!”

步骤 1. 为事件处理器定义 delegate


// 事件处理器:不返回值,不接受参数
public delegate void ButtonClickHandler();

步骤 2. 带事件的类


public class Button
{
    // 事件:可以订阅和取消订阅,但不能从外部直接触发
    public event ButtonClickHandler Click;

    // 方法,表示“按下按钮”
    public void Press()
    {
        Console.WriteLine("按钮被按下!");
        // 触发事件:通知所有订阅者
        Click?.Invoke();
    }
}

注意:事件是基于 delegate 声明的,在 Press 方法里通过 ?.Invoke() 来调用事件。为什么要这样?因为事件可能没有订阅者,这时 Click 为 null。安全调用操作符保证只有在有订阅者时才会执行处理器。

步骤 3. 订阅事件并运行代码


// 使用示例
public class Program
{
    public static void Main()
    {
        var button = new Button();

        // 添加“监听器”(订阅事件)
        button.Click += OnButtonClicked;
        button.Click += () => Console.WriteLine("再来一个处理器!");

        button.Press(); // 模拟按下按钮

        // 可以取消订阅事件
        button.Click -= OnButtonClicked;
        button.Press();
    }

    // 普通的事件处理器
    public static void OnButtonClicked()
    {
        Console.WriteLine("处理器:按钮被按下了!");
    }
}

第一次按下会触发两个处理器,第二次按下只会触发 lambda。

3. event 和 delegate 的区别

对 delegate 可以自由赋值,甚至完全替换订阅列表——有人可能写 button.Click = null,这样之前的所有订阅都会被清掉。

event 就不一样了——它和对象绑定得更严格。只有声明事件的类的拥有者能直接触发它(比如在类内部调用 Click())。其他代码只能通过 += 订阅或通过 -= 取消订阅,但不能“一下子清空”所有订阅。这保护了封装,不让订阅机制被“弄坏”。

4. 签名:事件的 delegate 类型和参数

在 .NET 中的习惯是:事件处理器接受两个参数——object sender(谁触发了事件)和事件参数(EventArgs)。建议使用 EventHandler 或 EventHandler<TEventArgs>。

示例:带参数的 delegate


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

EventArgs 是用于传递额外信息的基类。如果需要更多数据,就创建自己的子类。

把它应用到我们的按钮上


// 事件参数的类
public class ButtonClickEventArgs : EventArgs
{
    public string UserName { get; }

    public ButtonClickEventArgs(string userName)
    {
        UserName = userName;
    }
}

public class Button
{
    public event EventHandler<ButtonClickEventArgs> Click;

    public void Press(string userName)
    {
        Console.WriteLine("按钮被按下!");
        Click?.Invoke(this, new ButtonClickEventArgs(userName));
    }
}

public static void Main()
{
    var button = new Button();
    button.Click += OnButtonClicked;

    button.Press("瓦西里");
}

public static void OnButtonClicked(object sender, ButtonClickEventArgs e)
{
    Console.WriteLine($"用户 {e.UserName} 按下了按钮!");
}

5. 可视化示意:事件如何工作


+-------------+
|    用户     |
+-------------+
      |
      v
+--------------+
|  Button.Press|
+--------------+
      |
      v
+-----------------+         +------------------------+
|  调用 Click?    |----->---|   订阅者 1             |
+-----------------+         +------------------------+
      |                     |   执行处理器           |
      v                     +------------------------+
+-----------------+         +------------------------+
|   如果有订阅者  |----->---|   订阅者 2             |
+-----------------+         +------------------------+
      :                     |   执行处理器           |
      v                     +------------------------+

— Button.Press 触发事件;订阅者的处理器按顺序执行。

6. 有用的细节

在 .NET 中推荐的事件形式

使用 EventHandler 和 EventHandler<TEventArgs>,这样代码更容易和库、工具互操作。这个做法方便事件演进:你可以在自己的 EventArgs 里添加新属性,而不会破坏现有订阅者。

文档: EventHandler, event.

事件 vs delegate

如果你只想把一个组件和一个具体方法绑定,delegate 就够了(delegate)。如果你需要不同部分的程序可以随时订阅和取消订阅,就用 event(event)。

7. 常见错误和尴尬点

错误 №1:在没有检查 null 的情况下调用事件。 如果事件没有订阅者,直接调用会导致 NullReferenceException。使用安全调用:


Click?.Invoke(...);

错误 №2:试图在类外部触发事件。 事件只能在声明它的类内部被触发。其他类尝试触发会被编译器拒绝——这是对封装的保护。

错误 №3:同一个处理器被多次订阅。 如果同一个处理器被订阅多次,它会被调用相应的次数。这是订阅机制的特性。注意不要重复使用 +=,并正确使用 -= 取消订阅。

2
任务
C# SELF, 第 52 级, 课程 1
已锁定
实现一个简单的“按钮被点击”事件
实现一个简单的“按钮被点击”事件
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION