CodeGym /课程 /C# SELF /实际场景和最佳实践

实际场景和最佳实践

C# SELF
第 53 级, 课程 4
可用

1. 事件的实际使用场景

事件不仅仅是教科书里的漂亮理论。实际上它们在几乎每第二个应用里都会用到,如果你在做 UI 或网络服务——大多数情况下都会用到。

我们来看看一些真实开发中的典型场景。同时看看标准和最佳实践如何帮助避免常见错误。

响应用户操作(像在 UI 里)

可能这是最常见的场景——用户做了点什么(按按钮、在列表里选项),应用需要响应。Windows Forms、WPF、UWP、Avalonia、MAUI 等 UI 框架就是这样工作的。

迷你示例(没有 UI — 模拟一个按钮):


public class Button
{
    public event EventHandler? Click; // 标准委托

    public void SimulateClick()
    {
        Click?.Invoke(this, EventArgs.Empty); // “按钮” 被“按下”了
    }
}

class Program
{
    static void Main()
    {
        var button = new Button();
        button.Click += (sender, e) => Console.WriteLine("按钮被按下!");
        button.SimulateClick(); // > 按钮被按下!
    }
}

在真实的 UI 框架里,事件 Click 在用户用鼠标点击或在屏幕上轻触时触发。所有订阅了这个事件的处理器都会收到通知——这就是应用逻辑运行的方式。

进度、完成和错误的通知

想象一下:文件下载、数据处理、耗时计算。需要报告进度或错误。通常会有一个“进度步”事件和一个“完成”或“错误”事件。

下载器示例:


public class FileDownloader
{
    public event EventHandler<int>? ProgressChanged;
    public event EventHandler? DownloadCompleted;
    public event EventHandler<string>? DownloadFailed;

    public void Download()
    {
        for (int i = 1; i <= 100; i += 10)
        {
            Thread.Sleep(50); // 模拟延迟
            ProgressChanged?.Invoke(this, i);
        }
        DownloadCompleted?.Invoke(this, EventArgs.Empty);
    }
}

var downloader = new FileDownloader();
downloader.ProgressChanged += (s, progress) => Console.WriteLine($"下载: {progress}%");
downloader.DownloadCompleted += (s, e) => Console.WriteLine("下载完成。");
downloader.Download();

这里有好几个事件:可以全部订阅,也可以只订阅需要的。很多标准的 .NET 类(线程、文件下载、HTTP 等)就是这么工作的。

对对象生命周期的响应(例如,“已保存”、“已删除”)

假设你有域模型。当用户保存或删除对象时,你希望某些流程响应:更新缓存、写日志、发送消息等。


public class UserRepository
{
    public event EventHandler<UserEventArgs>? UserSaved;
    public event EventHandler<UserEventArgs>? UserDeleted;

    public void Save(User user)
    {
        //... 保存用户
        UserSaved?.Invoke(this, new UserEventArgs(user));
    }

    public void Delete(User user)
    {
        //... 删除用户
        UserDeleted?.Invoke(this, new UserEventArgs(user));
    }
}

public class UserEventArgs : EventArgs
{
    public User User { get; }
    public UserEventArgs(User user) => User = user;
}

现在不同模块可以订阅并且——在没有直接耦合的情况下——接收有关用户变化的通知。而且仓库自己不知道谁在“订阅”。

异步事件和多线程

有时事件不是从主(UI)线程触发的——比如来自后台任务、定时器或异步操作。在这种情况下要记住,处理器的代码会在“别的”线程上执行。如果处理器尝试直接从其他线程更新 UI 元素,会导致错误。

什么是 marshal? Marshal(转发/派发)是把代码执行从一个线程传到另一个线程,通常是从后台线程回到 UI 线程,以安全地更新界面。在 UI 应用(WinForms、WPF)中,可以用像 SynchronizationContext 或 Dispatcher 的机制把处理器调用“重定向”到需要的线程。

不要这样做:


// 事件从后台线程触发,而处理器直接更新 UI —— 会抛异常!

推荐做法: 检查当前执行线程,必要时把调用 marshal 到 UI 线程(通过 SynchronizationContext 或 Dispatcher)。在控制台或服务器应用里通常没有这样的限制。

Event Aggregator / 消息分发

在大应用中常用集中式的事件聚合器(Event Aggregator)。它降低耦合:发布者和订阅者互不知晓,通过中心交换消息。


public class EventAggregator
{
    public event EventHandler<SomeEventArgs>? SomeEvent;

    public void Publish(SomeEventArgs args)
    {
        SomeEvent?.Invoke(this, args);
    }
}

2. 最佳模板和实践

对错误使用 [CallerMemberName]

如果你写基础设施代码(比如日志、跟踪器),用 CallerMemberName 记录事件来源方法名——这能简化诊断。

如果需要,尽量让事件是线程安全的

事件是 multicast 的,从不同线程访问要小心:在调用前把委托拷贝到局部变量。


var handler = MyEvent;
if (handler != null) handler(this, args);

在 C# 6+ 可以用安全调用: MyEvent?.Invoke(this, args)。

把自定义的 EventArgs 设计为 immutable

定义自己的 EventArgs 类型时用只读属性——这样可以防止订阅者意外修改事件状态。


public class WorkCompletedEventArgs : EventArgs
{
    public string Message { get; }
    public WorkCompletedEventArgs(string message) => Message = message;
}

public 还是 private event

让事件是 public event,把触发封装在 protected virtual 的 OnEventName 方法里。这样支持可扩展性(派生类能重写行为),外部不能直接触发事件。

别破坏生命周期的顺序

如果你触发的是一长串处理器,记住:有人可能在处理期间订阅并试图改变内部逻辑。记录好在哪儿、何时触发事件,以及事件是否可能被触发多次。

多个事件与组合

当一个对象监听多个来源时,把订阅和取消订阅集中到一个地方(比如 Subscribe/Unsubscribe 方法)。必要时保存私有的委托字段以便于取消订阅。

3. 不该如何使用事件:常见错误

错误 №1: 忽视标准事件模式。
如果每个类都为每个事件声明自己的委托,很快就会变成一团糟。尽量使用标准委托 EventHandler 或 EventHandler<T>,其中 T 继承自 EventArgs。这种方式让代码对 .NET 开发者更清晰、更熟悉。

错误 №2: 从外部代码错误地调用事件。
永远不要在发布者类外直接调用事件。事件应该只在类内部触发,通常通过受保护的方法 OnEventName。订阅者只能订阅(+=)和取消订阅(-=)。

糟糕:


foo.MyEvent(); // 错误!不能从外部直接调用事件

正确:


// 仅在 foo 内部:
// protected virtual void OnMyEvent()
// {
//     MyEvent?.Invoke(this, EventArgs.Empty);
// }

错误 №3: 在调用事件前不检查是否有订阅者(null)。
如果试图调用没有订阅者的事件,会遇到 NullReferenceException。在 C# 6.0+ 使用 ?.Invoke(...) —— 只有存在订阅者时事件才会被调用。

错误 №4: 忘记取消订阅事件(内存泄漏)。
如果对象订阅了事件但在销毁前没有取消订阅,发布者会持有对订阅者的引用。垃圾回收器无法回收内存,这在长期存在的发布者和短生命周期的订阅者(例如 ViewModel、窗体)场景中尤其严重。最佳做法是显式取消订阅或在合适场合使用弱引用(WeakReference)。

错误 №5: 在受保护的虚方法 OnEvent 之外触发事件。
直接触发事件会剥夺派生类正确重写行为的机会。正确的模式是声明一个 protected virtual 方法来触发事件。

标准示例:


protected virtual void OnSomething(EventArgs e)
{
    Something?.Invoke(this, e);
}
2
任务
C# SELF, 第 53 级, 课程 4
已锁定
通过事件管理对象的生命周期
通过事件管理对象的生命周期
1
调查/小测验
事件生命周期第 53 级,课程 4
不可用
事件生命周期
详细解析事件订阅
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION