CodeGym /課程 /C# SELF /程式碼組織建議

程式碼組織建議

C# SELF
等級 54 , 課堂 1
開放

1. 事件宣告與命名風格

事件不只是 delegate。它是應用不同部份之間溝通的獨立實體,宣告應該要清楚易懂。

使用正確的 delegate 類型

在 99% 的情況下使用標準 delegate:

  • EventHandler — 用於無資料的事件。
  • EventHandler<TEventArgs> — 當需要傳遞參數時。

標準化讓程式維護和與 .NET 函式庫整合更簡單。如果 EventHandler 適合,就不要自己發明 delegate。


public event EventHandler SomethingHappened; // 沒有資料
public event EventHandler<MyEventArgs> DataReceived; // 有額外資料

如果需要特殊自訂化才宣告自己的 delegate,但這很少見。

事件命名

在 .NET 裡事件通常用過去式命名:CompletedClickedChangedReceived。這強調事件已經發生。

範例:


public event EventHandler DataLoaded;    // 資料已載入
public event EventHandler<MessageEventArgs> MessageReceived; // 訊息已收到
public event EventHandler Saving;        // 開始儲存程序

有時會用 Changing 形式表示「變更前」,讓外部有機會干預。

2. 出版者類別的組織:受保護的 virtual 方法 OnEvent

務必加入受保護的 virtual 方法來觸發事件:這提供集中呼叫點、繼承時易於擴充,以及預期的行為。


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。使用執行緒安全的範式:把 delegate 複製到區域變數。


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. 事件架構的總結建議

分離職責

出版者只負責通報事實。訂閱者自己決定何時訂閱與退訂。

避免事件炸彈

不要沒必要地每秒產生同一個事件好幾十次 — 這會造成過多負擔。

盡量不要用事件做雙向通訊

事件適合「一方通知,多方監聽」的模式。若需要雙向溝通,考慮使用 interface、callback 或其他機制。

不要在類別中直接保留訂閱者

不要顯式保留對訂閱者的參考 — 事件和 delegate 會自動處理這件事。

8. 經典反模式

無型別事件


public event Action<object> SomethingHappened; // 裡面內容不清楚

壞處:型別安全被破壞,需要 cast,維護性下降。

忘記退訂


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