More Effective C# 20.了解事件如何增進物件之間執行期的耦合 More Effective C# 20.了解事件如何增進物件之間執行期的耦合(Understand How Events Increase Runtime Coupling Among Objects)

這個做法說明雖然使用事件可以降低程式碼之間的耦合性,但同時又會帶來資源管理或是 EventArgs 參數共用等潛在耦合問題。

由於一個事件能夠被多人訂閱,所以某些設計在 EventArgs 的屬性會發生問題,例如你可以在自訂的 EventArgs 新增一個取消屬性, 並透過事件將這個 EventArgs 交給其它訂閱者。

下面這段程式碼就會將 WorkerEventArgs 傳遞給所有訂閱者,關鍵是這個 WorkerEventArgs 是所有訂閱者共用的,所以只要有其中多個訂閱者 修改 Cancel 屬性,那可能就會造成這個屬性處於無法預知的狀態。

public class WorkerEngine
{
    public event EventHandler<WorkerEventArgs> OnProgress;

    public void DoLotsOfStuff()
    {
        for (int i = 0; i < 100; i++)
        {
            SomeWork();
            var args = new WorkerEventArgs { Percent = i };
            OnProgress?.Invoke(this, args);
            if (args.Cancel)
                return;
        }
    }

    private void SomeWork()
    {
        // 模擬工作
    }
}

public class WorkerEventArgs : EventArgs
{
    public int Percent { get; set; }
    public bool Cancel { get; set; }
}

要處理這個問題也很簡單,就是把屬性設定成私有並提供一個關閉方法,這樣 Cancel 屬性就只能被設定為 ture,沒辦法設定回 false。

public class WorkerEventArgs : EventArgs
{
    public int Percent { get; set; }
    public bool Cancel { get; private set; }

    public void RequestCancel()
    {
        Cancel = true;
    }
}

之前也有提到過,當方法添加到 EventHandler 變成訂閱者時,代表他需要在背後等待通知後執行,這就代表訂閱者沒辦法也不能被釋放掉, 也就會一直佔著記憶體空間不放,最後影響整個 event 的生命週期。

例如下面這段程式碼需要在類別實做 IDisposable 介面,之後按正常流程在訂閱的時候將方法加入進 EventHandler,最後使用完畢要記得在 Dispose 內部解除訂閱,才能確保資源能正常釋放。

public class WorkerEngine
{
    public event EventHandler<WorkerEventArgs> OnProgress;
}

public class Subscriber : IDisposable
{
    private readonly WorkerEngine _engine;

    public Subscriber(WorkerEngine engine)
    {
        _engine = engine;
        _engine.OnProgress += HandleProgress;
    }

    private void HandleProgress(object sender, WorkerEventArgs e)
    {
        // 處理進度
    }

    public void Dispose()
    {
        _engine.OnProgress -= HandleProgress; // 解除訂閱
    }
}

Summary

雖然事件可以減少發佈者與訂閱者之間的耦合關係,但是使用時要注意事件參數共享修改的問題與添加訂閱者時可能會導致記憶體釋放等問題。