首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 事件進階:派生類引發基類事件與介面事件實作全解析

     編輯:關於C語言

核心問題:派生類無法直接引發基類事件

C# 事件本質是 多點傳播委派,編譯器只允許在宣告事件的類別內部引發它。派生類雖然繼承了事件,但不能直接呼叫 base.MyEvent(...)。

標準解決模式:受保護的虛擬引發方法

.NET Framework 广泛採用此模式(如 Control.OnClick、ComponentModel.INotifyPropertyChanged):

// 1. 自訂 EventArgs 傳遞豐富資訊
public class ShapeEventArgs : EventArgs
{
    public double NewArea { get; }
    public ShapeEventArgs(double area) => NewArea = area;
}

// 2. 基類定義事件與受保護虛擬引發方法
public abstract class Shape
{
    protected double area;
    public double Area { get => area; set => area = value; }

    // 使用泛型 EventHandler<T> 避免自訂委派
    public event EventHandler<ShapeEventArgs> ShapeChanged;

    public abstract void Draw();

    // 關鍵:受保護虛擬方法,派生類可覆寫並呼叫 base
    protected virtual void OnShapeChanged(ShapeEventArgs e)
    {
        // 競態條件防護:建立暫存副本
        EventHandler<ShapeEventArgs> handler = ShapeChanged;
        handler?.Invoke(this, e);
    }
}

派生類正確引發基類事件

public class Circle : Shape
{
    private double radius;
    public Circle(double r) { radius = r; area = 3.14 * r * r; }

    public void Update(double r)
    {
        radius = r;
        area = 3.14 * r * r;
        // 呼叫基類引發方法
        OnShapeChanged(new ShapeEventArgs(area));
    }

    // 可選:覆寫加入派生類特有邏輯
    protected override void OnShapeChanged(ShapeEventArgs e)
    {
        // 圓形特有處理...
        base.OnShapeChanged(e); // 必須呼叫基類實作
    }

    public override void Draw() => Console.WriteLine("Drawing a circle");
}

public class Rectangle : Shape
{
    private double length, width;
    public Rectangle(double l, double w) { length = l; width = w; area = l * w; }

    public void Update(double l, double w)
    {
        length = l; width = w; area = l * w;
        OnShapeChanged(new ShapeEventArgs(area));
    }

    protected override void OnShapeChanged(ShapeEventArgs e)
    {
        // 矩形特有處理...
        base.OnShapeChanged(e);
    }

    public override void Draw() => Console.WriteLine("Drawing a rectangle");
}

訂閱者:容器統一處理基類事件

public class ShapeContainer
{
    private List<Shape> shapes = new List<Shape>();

    public void AddShape(Shape s)
    {
        shapes.Add(s);
        // 訂閱基類事件,多型處理所有派生類
        s.ShapeChanged += HandleShapeChanged;
    }

    private void HandleShapeChanged(object sender, ShapeEventArgs e)
    {
        Shape s = (Shape)sender;
        Console.WriteLine($"Received event. Shape area is now {e.NewArea}");
        s.Draw(); // 多型呼叫
    }
}

⚠ 絕對禁止:虛擬事件與覆寫事件

// ❌ 錯誤示範 - 編譯器允許但行為未定義
public virtual event EventHandler MyEvent; // 基類
public override event EventHandler MyEvent; // 派生類

後果:C# 編譯器無法正確處理訂閱者是訂閱基類還是派生類事件,導致事件遺漏或重複觸發。

介面事件實作

基本實作

public interface IDrawingObject
{
    event EventHandler ShapeChanged;
}

public class Shape : IDrawingObject
{
    public event EventHandler ShapeChanged; // 隱式實作

    public void ChangeShape()
    {
        // 業務邏輯...
        OnShapeChanged(EventArgs.Empty);
    }

    protected virtual void OnShapeChanged(EventArgs e)
    {
        ShapeChanged?.Invoke(this, e);
    }
}

多介面同名事件:顯式介面實作

當類別實作多個介面包含同名事件,至少一個需顯式實作:

public interface IDrawingObject { event EventHandler OnDraw; } // 繪製前
public interface IShape { event EventHandler OnDraw; }         // 繪製後

public class Shape : IDrawingObject, IShape
{
    // 內部兩個獨立事件
    private event EventHandler PreDrawEvent;
    private event EventHandler PostDrawEvent;
    private readonly object lockObj = new object();

    // 顯式實作 IDrawingObject.OnDraw → 繪製前
    event EventHandler IDrawingObject.OnDraw
    {
        add { lock (lockObj) PreDrawEvent += value; }
        remove { lock (lockObj) PreDrawEvent -= value; }
    }

    // 顯式實作 IShape.OnDraw → 繪製後
    event EventHandler IShape.OnDraw
    {
        add { lock (lockObj) PostDrawEvent += value; }
        remove { lock (lockObj) PostDrawEvent -= value; }
    }

    public void Draw()
    {
        PreDrawEvent?.Invoke(this, EventArgs.Empty);
        Console.WriteLine("Drawing a shape.");
        PostDrawEvent?.Invoke(this, EventArgs.Empty);
    }
}

訂閱者透過介面型別區分事件

var shape = new Shape();

// 訂閱 IDrawingObject.OnDraw (繪製前)
IDrawingObject drawingObj = shape;
drawingObj.OnDraw += (s, e) => Console.WriteLine("Sub1: 繪製前事件");

// 訂閱 IShape.OnDraw (繪製後)
IShape shapeObj = shape;
shapeObj.OnDraw += (s, e) => Console.WriteLine("Sub2: 繪製後事件");

shape.Draw();
// 輸出:
// Sub1: 繪製前事件
// Drawing a shape.
// Sub2: 繪製後事件

設計模式總結對照表

場景模式關鍵點
派生類引發基類事件protected virtual OnEvent建立委派副本、派生類呼叫 base.OnEvent
單一介面事件隱式實作直接宣告 event、編譯器產生 add/remove
多介面同名事件顯式介面實作內部獨立事件欄位、手寫 add/remove、lock 保護
事件引發執行緒安全暫存委派副本var h = MyEvent; h?.Invoke(...)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved