CodeGym /课程 /C# SELF /关于组织代码的建议

关于组织代码的建议

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

1. 事件声明和命名风格

事件不是简单的委托。它是应用各部分之间通信的独立实体,声明应该清晰易懂。

使用合适的委托类型

在 99% 的情况下使用标准委托:

  • EventHandler — 用于不带数据的事件。
  • EventHandler<TEventArgs> — 需要传递参数时使用。

标准化能让代码更易维护并且更好地和 .NET 库集成。如果 EventHandler 能满足需求,就不要自己造委托。


public event EventHandler SomethingHappened; // 没有数据
public event EventHandler<MyEventArgs> DataReceived; // 有额外数据

如果需要特别的自定义 —— 可以自己声明委托,但这种情况少见。

事件命名

在 .NET 中事件通常使用过去式命名:CompletedClickedChangedReceived。这强调事件已经发生了。

示例:


public event EventHandler DataLoaded;    // 数据已加载
public event EventHandler<MessageEventArgs> MessageReceived; // 收到消息
public event EventHandler Saving;        // 保存过程开始

有时会用 Changing 表示“之前”的事件,给外部一个介入的机会。

2. 发布者类的组织:受保护的虚方法 OnEvent

总是添加受保护的虚方法来触发事件:这样形成调用的集中点,便于继承时扩展,并让行为可预测。


public class FileLoader
{
    public event EventHandler<FileLoadedEventArgs> FileLoaded;

    protected virtual void OnFileLoaded(FileLoadedEventArgs e)
    {
        FileLoaded?.Invoke(this, e);
    }

    public void Load(string filename)
    {
        // ... 加载文件的逻辑 ...
        OnFileLoaded(new FileLoadedEventArgs(filename));
    }
}

public class FileLoadedEventArgs : EventArgs
{
    public string FileName { get; }
    public FileLoadedEventArgs(string fileName) => FileName = fileName;
}

让只有 OnFileLoaded 去触发事件 —— 更容易维护和测试。

3. 订阅和取消订阅的规则:生命周期,IDisposable

如果订阅者的生命周期短于发布者,务必在订阅者销毁前取消订阅。通常实现 IDisposable 并在 Dispose() 中取消订阅是个好办法。


public class TemporaryListener : IDisposable
{
    private readonly Publisher _publisher;

    public TemporaryListener(Publisher publisher)
    {
        _publisher = publisher;
        _publisher.DataReceived += HandleData;
    }

    private void HandleData(object sender, EventArgs e)
    {
        // 处理数据
    }

    public void Dispose()
    {
        _publisher.DataReceived -= HandleData;
    }
}

// 使用 using:
using (var listener = new TemporaryListener(myPublisher))
{
    // listener 在这里监听事件
}
// 退出 using 后 - Dispose 被调用,已取消订阅

如果忘记取消订阅,发布者会持有对订阅者委托的引用 —— 会导致内存泄漏和“僵尸对象”。

4. 线程安全地触发事件

在多线程代码中,订阅者可能在触发事件的同时被添加或移除。这会导致竞态和 NullReferenceException。使用线程安全的模式:把委托拷贝到本地变量。


protected virtual void OnSomethingHappened()
{
    EventHandler handler = SomethingHappened;
    handler?.Invoke(this, EventArgs.Empty);
}

在 C# 6+ 可以直接写:


SomethingHappened?.Invoke(this, EventArgs.Empty);

5. 用 EventArgs 而不是 object

不要通过 object 或类字段来传数据。通过继承 EventArgs 做强类型传递。


public class DownloadCompletedEventArgs : EventArgs
{
    public string FileName { get; }
    public long Size { get; }
    public DownloadCompletedEventArgs(string fileName, long size)
    {
        FileName = fileName;
        Size = size;
    }
}

public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;

6. 对事件和订阅者做文档化

写清楚:什么时候触发事件,EventArgs 字段的含义,是否需要取消订阅以及何时取消。


/// <summary>
/// 事件在成功加载数据后发生。
/// </summary>
public event EventHandler<DataLoadedEventArgs> DataLoaded;

7. 事件架构的总结建议

职责分离

发布者只负责通知事实。订阅者自己决定何时订阅和取消订阅。

避免事件“轰炸”

不要在没有必要的情况下每秒触发同一个事件几十次 —— 会造成不必要的开销。

尽量不要用事件做双向通信

事件适合“一对多”通知。双向通信时考虑使用接口、回调或其他机制。

不要在类中保存订阅者的引用

不要显式持有订阅者引用 —— 事件和委托会自动管理订阅关系。

8. 经典反模式

无类型事件


public event Action<object> SomethingHappened; // 不清楚里面是什么

坏处:类型信息丢失,需要强转,可维护性下降。

忘记取消订阅


public class ShortLivedListener
{
    public ShortLivedListener(Publisher p) =>
        p.DataReceived += DoWork;

    private void DoWork(object sender, EventArgs e) { /* ... */ }
    // 没有 Dispose,没有取消订阅 => 僵尸对象!
}

违反 SRP

类同时充当发布者、订阅者和处理器 —— 角色混杂。请分离职责。

9. 面试和项目中的实践应用

在很多使用发布-订阅的项目中,良好的事件组织是可扩展性和可维护性的关键。在面试中常被要求:

  • 实现一个具有正确类型的事件系统,
  • 演示如何管理订阅者的生命周期,
  • 解释如何线程安全地触发事件。

干净、文档化、组织良好的事件代码会让你在候选人中脱颖而出。

2
任务
C# SELF, 第 54 级, 课程 1
已锁定
组织有多个订阅者的事件
组织有多个订阅者的事件
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION