CodeGym /課程 /C# SELF /實作情境與最佳實務

實作情境與最佳實務

C# SELF
等級 53 , 課堂 4
開放

1. 實作情境下的事件使用

事件不是書本上的漂亮理論。實務上幾乎每第二個應用都會用到事件,若你在做 UI 或網路服務,幾乎大部分情況都需要。

我們來看幾個典型的真實開發場景。順便看看標準做法和最佳實務如何幫你避免常見錯誤。

回應使用者操作(像在 UI)

可能最常見的情境是:使用者做了某件事(按按鈕、在列表選取項目),應用要回應。這就是 Windows Forms、WPF、UWP、Avalonia、MAUI 等 UI framework 的運作方式。

迷你範例(無 UI — 模擬按鈕):


public class Button
{
    public event EventHandler? Click; // 標準 delegate

    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 framework 裡,事件 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 等)就是這樣工作的。

對物件生命週期的反應(例如「已儲存」、「已刪除」)

假如你有一個領域模型。當使用者儲存或刪除物件時,希望有些流程能回應:更新 cache、寫 log、發送訊息等等。


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;
}

現在不同模組可以訂閱,並且——在沒有直接耦合的情況下——接收使用者變更的通知。而 repository 本身不知道誰訂閱了它。

非同步事件與多執行緒

有時事件是從非主要(UI)執行緒產生的,例如背景工作、計時器或非同步操作。這時要注意,處理器的程式會在「外來」執行緒執行。如果處理器直接從其他執行緒更新 UI 元件,會產生錯誤。

什麼是 marshaling? Marshaling 是把執行從一個執行緒傳到另一個執行緒,通常是把背景執行緒的結果傳回 UI 執行緒以安全更新介面。在 UI 應用(WinForms、WPF)會用像 SynchronizationContextDispatcher 的機制,讓你把處理器的呼叫「導回」到正確執行緒。

不要這樣做:


// 事件從背景執行緒呼叫,而處理器直接更新 UI — 會得到例外!

建議: 檢查目前執行緒,必要時把呼叫 marshal 回 UI 執行緒(透過 SynchronizationContextDispatcher)。在主控台或伺服器應用通常沒有這種限制。

Event Aggregator / Messaging

大型應用常用集中式的事件聚合器(Event Aggregator)。它降低耦合度:訂閱者和發布者互不相識,透過中介交換訊息。


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

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

2. 最佳範式與實務

對錯誤使用 [CallerMemberName]

如果你在做基礎設施程式(像 logger、tracer),用 CallerMemberName 記錄事件來源方法名稱,能讓診斷更簡單。

若有需要,讓事件 thread-safe

事件是 multicast 的,且不同執行緒存取時要小心:在呼叫前把 delegate 複製到本地變數。


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;
}

publicprivate event

把事件聲明為 public event,並把觸發事件的邏輯封裝在一個 protected virtual 方法(像 OnEventName)中。這樣可擴充(繼承者可覆寫),外部使用者也不能直接觸發事件。

不要破壞生命週期的順序

若你啟動一連串的處理者,記得有人可能會在處理過程中訂閱或改變內部邏輯。把何時何地會觸發事件寫清楚,並說明事件是否可能被觸發多次。

多個事件與組合

當一個物件監聽多個來源時,把訂閱與取消訂閱放在同一地方(例如 Subscribe/Unsubscribe 方法)。必要時保存私有的 delegate 欄位以方便取消訂閱。

3. 不該如何使用事件:常見錯誤

錯誤 №1:忽略標準的事件 pattern。
如果每個類別都為每個事件宣告自己的 delegate,很快會變成一團亂。盡量使用標準的 delegate EventHandlerEventHandler<T>,其中 T 繼承自 EventArgs。這讓程式碼對 .NET 開發者更直覺與容易理解。

錯誤 №2:外部程式不正確地呼叫事件。
絕不要在 publisher 類別外直接呼叫事件。事件應該只在類別內部被觸發,通常透過受保護的方法 OnEventName。訂閱者只能做訂閱(+=)與取消訂閱(-=)。

糟糕做法:


foo.MyEvent(); // 錯誤!不能在外部直接呼叫事件

正確做法:


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

錯誤 №3:在呼叫事件前沒檢查是否有訂閱者 (null)。
若嘗試呼叫沒有訂閱者的事件,會得到 NullReferenceException。在 C# 6.0+ 使用 ?.Invoke(...) — 只有在有訂閱者時才會呼叫事件。

錯誤 №4:忘記取消訂閱事件(記憶體洩漏)。
若物件訂閱了事件但在被銷毀前未取消訂閱,publisher 會持有對訂閱者的參考。GC 不能釋放這些記憶體,這在長生命期的 publisher 和短生命期的 subscriber(像 ViewModel、表單)時尤其致命。最好的做法是明確取消訂閱,或在適當時候使用弱參考(WeakReference)。

錯誤 №5:不透過受保護的 virtual 方法 OnEvent 來觸發事件。
直接觸發事件會剝奪繼承者覆寫行為的機會。正確的 pattern 是宣告一個 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