前言
前面介紹了C#中的委托,事件的很多部分都與委托類似。實際上,事件就像是專門用于某種特殊用途的簡單委托,事件包含了一個私有的委托,如下圖所示:

有關事件的私有委托需要了解的重要事項如下:
1、事件提供了對它的私有控制委托的結構化訪問。我們無法直接訪問該委托。
2、事件中可用的操作比委托要少,對于事件我們只可以添加、刪除或調用事件處理程序。
3、事件被觸發時,它調用委托來依次調用調用列表中的方法。
有關事件的概念
發布者(Publisher)
:發布某個事件的類或結構,其他類可以在該事件發生時得到通知。
訂閱者(Subscriber)
:注冊并在事件發生時得到通知的類或結構。
事件處理程序(event handler)
:由訂閱者注冊到事件的方法,在發布者觸發事件時執行。
觸發(raise)事件
:調用(invoke)或觸發(fire)事件的術語。當事件觸發時,所有注冊到它的方法都會被依次調用。
示例
簡單示例
現在我們先來看一下最最原始的事件示例。其結構如下所示:

委托類型聲明
:事件和事件處理程序必須有共同的簽名和返回類型,它們通過委托類型進行描述。
事件處理程序聲明
:訂閱者類中會在事件觸發時執行的方法聲明。它們不一定有顯示命名的方法,還可以是匿名方法或Lambda表達式。
事件聲明
:發布者類必須聲明一個訂閱者類可以注冊的事件成員。當聲明的事件為public時,稱為發布了事件。
事件注冊
:訂閱者必須訂閱事件才能在它被觸發時得到通知。
觸發事件的代碼
:發布者類中”觸發“事件并導致調用注冊的所有事件處理程序的代碼。
現在我們可以照著這個思路去寫示例代碼。
首先聲明一個自定義的委托類型:
public delegate void MyDelegate();
該委托類型沒有參數也沒有返回值。
然后再寫一個發布者類:
public class Publisher
{
public event MyDelegate MyEvent;
public void DoCount()
{
for(int i = 0; i < 10; i++)
{
Task.Delay(3000).Wait();
if(MyEvent != null)
{
MyEvent();
}
}
}
}
事件聲明:
public event MyDelegate MyEvent;
事件聲明在一個類中,它需要委托類型的名稱,任何注冊到事件的處理程序都必須與委托類型的簽名和返回類型匹配。它聲明為public,這樣其他類和結構可以在它上面注冊事件處理程序。不能使用對象創建表達式(new表達式)來創建它的對象。
一個常見的誤解就是把事件認為是類型,事件其實不是類型,它和方法、屬性一樣是類或結構的成員。
由于事件是成員,所以我們不能在一段可執行的代碼中聲明事件,它必須聲明在類或結構中,和其他成員一樣。
事件成員被隱式自動初始化為null。
事件聲明的圖解如下所示:

觸發事件:
if(MyEvent != null)
{
MyEvent();
}
也可以這樣寫:
if(MyEvent != null)
{
MyEvent().Invoke();
}
這兩者是等效的,MyEvent();
直接調用事件的委托,MyEvent().Invoke()
使用顯式調用委托的 Invoke
方法。
現在再看看訂閱者類:
public class Subscriber
{
public void EventHandler()
{
Console.WriteLine($"{DateTime.Now}執行了事件處理程序");
}
}
訂閱者類中有一個EventHandler方法,與前面定義的委托類型的簽名與返回值類型一致。
在看下主函數:
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
publisher.MyEvent += subscriber.EventHandler;
publisher.DoCount();
}
publisher.MyEvent += subscriber.EventHandler;
就是在訂閱事件,對應上面結構圖中的事件注冊,將subscriber類的EventHandler方法注冊到publisher類的MyEvent事件上。
也可以通過:
publisher.MyEvent -= subscriber.EventHandler;
取消訂閱事件。
運行結果如下所示:

本示例全部代碼如下所示:
internal class Program
{
public delegate void MyDelegate();
public class Publisher
{
public event MyDelegate MyEvent;
public void DoCount()
{
for(int i = 0; i < 3; i++)
{
Task.Delay(3000).Wait();
if(MyEvent != null)
{
MyEvent();
}
}
}
}
public class Subscriber
{
public void EventHandler()
{
Console.WriteLine($"{DateTime.Now}執行了事件處理程序");
}
}
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
publisher.MyEvent += subscriber.EventHandler;
publisher.DoCount();
}
}
以上就根據上面的結構圖寫出了一個使用事件的示例,但是本示例還有需要改進的地方。
上面我們觸發事件檢查空值是這樣寫的:
if(MyEvent != null)
{
MyEvent();
}
C# 6.0 引入了空條件操作符之后,現在也可以這樣做空值檢查:
MyEvent?.Invoke();
同時也不是一上來就檢查空值,而是先將MyEvent賦給第二個委托變量localDelegate:
MyDelegate localDelegate = MyEvent;
localDelegate?.Invoke();
這個簡單的修改可確保在檢查空值和發送通知之間,如果一個不同的線程移除了所有MyEvent訂閱者,將不會引發NullReferenceException異常。
標準 .NET 事件模式
以上我們以一個簡單的例子介紹了C#中的事件,但是大家可能會覺得有點模式,跟我們平常在winform中使用的事件好像不太一樣,那是因為 .NET 框架提供了一個標準模式,接下來我將以winform中的button按鈕點擊事件為例進行介紹。
頁面很簡單,只有一個button按鈕:

然后button按鈕點擊事件的代碼如下:
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Hello World");
}
現在我們再根據下面這張事件結構圖,來看一看標準的 .NET 事件模式:

事件注冊
打開解決方案中的Form1.Designer.cs文件:

看到button1相關內容:

button1.Click += button1_Click;
就是在訂閱事件,對應上面圖中的事件注冊。
委托類型聲明
右鍵查看定義:
public event EventHandler? Click
{
add => Events.AddHandler(s_clickEvent, value);
remove => Events.RemoveHandler(s_clickEvent, value);
}
發現Click事件中的委托類型是EventHandler,再查看EventHandler的定義:
public delegate void EventHandler(object? sender, EventArgs e);
這一步對應上面事件結構圖中的委托類型聲明。
EventHandler是 .NET中預定義的委托,專門用來表示不生成數據的事件的事件處理程序方法應有的簽名與返回類型。
第一個參數是sender,用來保存觸發事件的對象的引用。由于是object?
類型,所以可以匹配任何類型的實例。
第二個參數是e,用于傳遞數據。但是EventArgs
類表示包含事件數據的類的基類,并提供用于不包含事件數據的事件的值。也就是說EventArgs
設計為不能傳遞任何數據。它用于不需要傳遞數據的事件處理程序,通常會被忽略。如果我們想要傳遞數據,必須聲明一個派生自EventArgs
的類,使用合適的字段來保存需要傳遞的數據。
盡管EventArgs
類實際上并不傳遞數據,但它是使用EventHandler
委托模式的重要部分。不管參數使用的實際類型是什么,object類和EventArgs類總是基類,這樣EventHandler
就能提供一個對所有事件和事件處理器都通用的簽名,只允許兩個參數,而不是各自都有不同簽名。
事件聲明
public event EventHandler? Click
{
add => Events.AddHandler(s_clickEvent, value);
remove => Events.RemoveHandler(s_clickEvent, value);
}
Click
事件在Control
類中定義,Button
類繼承自ButtonBase
類,而ButtonBase
類繼承自Control
類。
public event EventHandler? Click;
對應上面結構圖中的事件聲明。
觸發事件的代碼
查看Button類的定義,找到OnClick方法的定義:
protected override void OnClick(EventArgs e)
{
Form? form = FindForm();
if (form is not null)
{
form.DialogResult = _dialogResult;
}
AccessibilityNotifyClients(AccessibleEvents.StateChange, -1);
AccessibilityNotifyClients(AccessibleEvents.NameChange, -1);
if (IsAccessibilityObjectCreated)
{
AccessibilityObject.RaiseAutomationPropertyChangedEvent(UiaCore.UIA.NamePropertyId, Name, Name);
AccessibilityObject.RaiseAutomationEvent(UiaCore.UIA.AutomationPropertyChangedEventId);
}
base.OnClick(e);
}
去掉無關部分,保留相關部分便于理解:
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
}
這里的base指的是Button
類的基類ButtonBase
類:

再查看ButtonBase類中OnClick方法的定義:
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
OnRequestCommandExecute(e);
}
發現也有一個base.OnClick(e);
,這里的base指的是ButtonBase
類的基類Control
:

再查看Control類中OnClick方法的定義:
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void OnClick(EventArgs e)
{
((EventHandler?)Events[s_clickEvent])?.Invoke(this, e);
}
終于找到了觸發事件的代碼。
事件處理程序
這個想必大家并不陌生,雙擊button按鈕就可以看到:
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Hello World");
}
這對應上面結構圖中的事件處理程序。該事件處理程序方法的簽名與返回值類型與EventHandler委托類型一致。
使用泛型版本的標準 .NET事件模式
接下來我會舉一個例子,說明如何使用泛型版本的標準 .NET事件模式。
第一步,自定義事件數據類,該類繼承自EventArgs
類:
public class MyEventArgs : EventArgs
{
public string? Message { get; set; }
public DateTime? Date { get; set; }
}
擁有兩個屬性Message與Date。
第二步,寫發布者類:
public class Publisher
{
public event EventHandler<MyEventArgs>? SendMessageEvent;
public void SendMessage()
{
for(int i = 0; i < 3; i++)
{
Task.Delay(3000).Wait();
MyEventArgs e = new MyEventArgs();
e.Message = $"第{i+1}次觸發事件";
e.Date = DateTime.Now;
EventHandler<MyEventArgs>? localEventHandler = SendMessageEvent;
localEventHandler?.Invoke(this, e);
}
}
}
public event EventHandler<MyEventArgs>? SendMessageEvent;
聲明了事件。
EventHandler<MyEventArgs>? localEventHandler = SendMessageEvent;
localEventHandler?.Invoke(this, e);
觸發了事件。
第三步,寫訂閱者類:
public class Subscriber
{
public void EventHandler(object? sender,MyEventArgs e)
{
Console.WriteLine($"Received Message:{e.Message} at {e.Date}");
}
}
包含事件處理程序,該方法與EventHandler<MyEventArgs>
委托類型的簽名與返回值類型一致。
第四步,寫主函數:
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
publisher.SendMessageEvent += subscriber.EventHandler;
publisher.SendMessage();
}
publisher.SendMessageEvent += subscriber.EventHandler;
訂閱事件。
運行結果如下所示:

包含了我們自定義的事件數據。
補充
上面說自定義的事件數據類要繼承自EventArgs
類,但其實在 .NET Core 的模式較為寬松。 在此版本中,EventHandler<TEventArgs>
定義不再要求 TEventArgs
必須是派生自 System.EventArgs
的類。
因此我在.NET 8 版本的示例中去掉繼承自EventArgs
類,該示例依舊能正常運行。
異步事件訂閱者
一個關于異步事件訂閱者的例子如下:
public class EventPublisher
{
public event Func<string, Task>? MyEvent;
public async Task RaiseEventAsync(string message)
{
Func<string, Task> localEvent = MyEvent;
await localEvent?.Invoke(message);
}
}
public class AsyncEventSubscriber
{
public async Task HandleEventAsync(string message)
{
Console.WriteLine($"Received event with message: {message}");
await Task.Delay(3000);
Console.WriteLine("Event handling complete.");
}
}
class Program
{
static async Task Main(string[] args)
{
var publisher = new EventPublisher();
var subscriber = new AsyncEventSubscriber();
publisher.MyEvent += subscriber.HandleEventAsync;
await publisher.RaiseEventAsync("Hello, world!");
Console.ReadLine();
}
}
運行結果如下所示:

總結
本文先是介紹了一些C#中事件的相關概念,然后通過幾個例子介紹了在C#中如何使用事件。
?轉自https://www.cnblogs.com/mingupupu/p/17946032
該文章在 2025/3/5 8:40:23 編輯過