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(...) |