理解C#中的事件
C# 中的事件是許多開發者使用的重要概念,但在建立自己的事件方面,可能沒有完全理解。 Tim Corey 提供了一個全面的指南,講解如何建立和使用事件,探討在其影片"C# Events - Creating and Consuming Events in Your Application"中介紹的最佳實踐和進階功能。
在本文中,我們將探討 C# 事件,重點放在其語法、如何定義以及使用 Tim Corey 的影片例子來解決常見編程問題。 了解 C# 中的事件處理對構建響應式應用程式至關重要,而本文將幫助您掌握自定義錯誤日誌記錄、異常處理和事件驅動編程的核心概念。
事件簡介
在 C# 和編程語言中,事件在事件驅動編程中發揮著關鍵作用,使應用程式能夠對使用者交互或系統變更等操作做出響應。 與其他編程語言相比,C# 提供了結構化的事件處理方法,非常適合快速且小型的應用程式。 C# 中的事件由特定操作觸發,這些操作調用相應的事件處理程式來執行所需的任務。
Tim 首先解釋說,大多數開發者對 C# 中的事件很熟悉,但可能不知道如何建立自定義事件。 影片的目的是介紹事件,示範建立自定義事件的過程,討論其特性,並概述最佳實踐。
應用程式演示概述
Tim 使用一個使用 Windows Forms (WinForms) 構建的簡單銀行應用程式來演示事件。 該應用程式模擬一個客戶的基本銀行操作,包括查看餘額和記錄交易。
- 應用程式概述:
- 該應用程式有兩個窗體:一個用於顯示帳戶餘額和交易,另一個用於記錄新交易。
- 它包括用於模擬信用卡購買和通過從存款轉賬到支票賬戶來處理透支的按鈕。
- 主窗體:
- 顯示客戶的姓名、支票和存款帳戶餘額。
- 顯示兩個帳戶的交易列表。
- 包含一個按鈕以打開交易記錄窗體。
- 交易窗體:
- 允許使用者輸入交易金額。
- 模擬購買並通過從存款轉賬到支票來處理透支。
演示應用程式背後的程式碼
Tim 解釋了演示銀行應用程式的後端程式碼:
- 客戶類:
- 代表具有客戶姓名和兩個帳戶(支票和存款)屬性的客戶。
- 客戶類非常簡單但能很好地作為了解如何管理多個帳戶的初始點。
- 帳戶類:
- 管理銀行帳戶的詳細資訊,包括帳號名稱、餘額和交易列表。
- Balance 屬性是 decimal 型別,適用於精確的貨幣計算。
- Transactions 屬性是只讀列表,以防止外部修改。
- 處理存款和付款:
- AddDeposit 方法:將存款新增到帳戶,更新餘額並記錄交易。
- MakePayment 方法:處理取款,包括檢查是否有足夠的資金,並在必要時通過從備用帳戶轉賬來管理透支保護。
- 透支保護:
- 該應用程式包含處理透支的邏輯。 如果支票帳戶餘額不足以完成交易,應用程式會檢查存款帳戶以彌補差額。 如果合併餘額足夠,它會將所需金額轉至支票帳戶並完成交易。
對於程式碼範例,您可以直接參考 Tim Corey 的影片,他在 3:18 到 18:27 之間解釋程式碼。
事件:按鈕點擊
在本節中,Tim Corey 深入研究 Windows Forms 中按鈕點擊事件的運作方式,說明這些事件如何連接並如何運行。
理解按鈕點擊事件
Tim 首先解釋了 Windows Forms 應用程式中大家熟知的按鈕點擊事件的概念。 當您在設計器中雙擊按鈕時,Visual Studio 會自動為點擊事件生成一個事件處理方法。
- 事件處理方法:
每當按鈕被點擊時,就會調用此方法。 這是您放置應響應按鈕點擊而執行的程式碼的地方。
private void recordTransactionButton_Click(object sender, EventArgs e) { // Code to handle the button click event }private void recordTransactionButton_Click(object sender, EventArgs e) { // Code to handle the button click event }
- 連接事件:
事件處理方法在窗體的設計文件 (FormName.Designer.cs) 中被連接到按鈕點擊事件。 這是通過使用 += 運算符將事件處理程式新增到按鈕的點擊事件來完成的。
this.recordTransactionButton.Click += new System.EventHandler(this.recordTransactionButton_Click);this.recordTransactionButton.Click += new System.EventHandler(this.recordTransactionButton_Click);
建立和調用自定義事件
Tim 解釋如何在 C# 中建立自定義事件,首先從 Account 類開始,這是事件將被觸發的地方。
- 定義事件:
使用 event 關鍵字定義事件。 它看起來類似於公共變數,但專門用於事件。
public event EventHandler<string> RaiseTransactionApprovedEvent;public event EventHandler<string> RaiseTransactionApprovedEvent;
- 觸發事件:
使用 Invoke 方法觸發事件,通常在定義事件的類內部。 Invoke 方法接受兩個參數:發送者(通常為
this)和事件資料(在此情況下為字串)。private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }
- 連接和處理事件:
使用 += 運算符訂閱事件並定義當事件被觸發時的處理方法。
account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent; private void Account_RaiseTransactionApprovedEvent(object sender, string e) { // Code to handle the event }account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent; private void Account_RaiseTransactionApprovedEvent(object sender, string e) { // Code to handle the event }
使用事件來更新 UI
Tim 解釋如何使用事件在發生某些操作(例如交易被批准時)時自動更新瀏覽器 UI。
- 觸發事件:
在交易被批准後觸發自定義事件。
public void AddDeposit(decimal amount, string depositName) { // Code to add deposit RaiseTransactionApprovedEvent?.Invoke(this, depositName); }public void AddDeposit(decimal amount, string depositName) { // Code to add deposit RaiseTransactionApprovedEvent?.Invoke(this, depositName); }
- 在 UI 中訂閱事件:
在主窗體中訂閱事件,以便每當一個新交易被批准時更新交易列表。
public MainForm() { InitializeComponent(); account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent; } private void Account_RaiseTransactionApprovedEvent(object sender, string e) { // Update the UI with the new transaction }public MainForm() { InitializeComponent(); account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent; } private void Account_RaiseTransactionApprovedEvent(object sender, string e) { // Update the UI with the new transaction }
Event?.Invoke() 的解釋
Tim Corey 解釋了當在 C# 中觸發事件時使用 ?.Invoke() 語法。 這種現代方法簡化了程式碼並確保了執行緒安全。
問號運算符
問號(?.)在 Invoke 前為空條件運算符。 它在調用之前先檢查事件處理程式是否為空,以防止潛在的例外。
- 傳統方法:
以前,開發者使用多行程式碼檢查事件處理程式是否為空,然後調用它。
if (RaiseTransactionApprovedEvent != null) { RaiseTransactionApprovedEvent(this, depositName); }if (RaiseTransactionApprovedEvent != null) { RaiseTransactionApprovedEvent(this, depositName); }
- 現代方法:與 ?.Invoke():
空條件運算符簡化了這一過程,在單行中進行空值檢查並調用事件。
RaiseTransactionApprovedEvent?.Invoke(this, depositName);RaiseTransactionApprovedEvent?.Invoke(this, depositName);- 如果
RaiseTransactionApprovedEvent為空,則不會進行調用,有效避免任何例外。
- 如果
- 優勢:
- 簡化程式碼:減少了安全調用事件所需的程式碼數量。
- 執行緒安全:通過在單一步驟中檢查空值並調用事件,消除競態條件。
監聽和編寫事件程式碼
Tim 解釋如何在 Windows Forms 應用程式中監聽自定義事件並相應地更新 UI。
- 訂閱事件:
使用 += 運算符訂閱自定義事件。
customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent;customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent;
- 事件處理方法:
定義一個方法來處理事件。 此方法根據事件資料更新 UI。
private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Update the UI with the new transaction checkingTransactionsDataSource.DataSource = null; checkingTransactionsDataSource.DataSource = customer.CheckingAccount.Transactions; checkingBalanceLabel.Text = customer.CheckingAccount.Balance.ToString("C2"); }private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Update the UI with the new transaction checkingTransactionsDataSource.DataSource = null; checkingTransactionsDataSource.DataSource = customer.CheckingAccount.Transactions; checkingBalanceLabel.Text = customer.CheckingAccount.Balance.ToString("C2"); }
建立自定義事件:實際運行和回顧
Tim 示範了整個建立、觸發和處理自定義事件的過程。
- 觸發事件:
在交易被批准或餘額變更後觸發事件。
private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }
- 處理多個事件:
確保應用程式能夠監聽多個事件,例如交易和餘額變動,讓 UI 實時更新。
public MainForm() { InitializeComponent(); customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent; customer.SavingsAccount.TransactionApprovedEvent += SavingsAccount_TransactionApprovedEvent; } private void SavingsAccount_TransactionApprovedEvent(object sender, string e) { // Update the UI with the new savings transaction savingsTransactionsDataSource.DataSource = null; savingsTransactionsDataSource.DataSource = customer.SavingsAccount.Transactions; savingsBalanceLabel.Text = customer.SavingsAccount.Balance.ToString("C2"); }public MainForm() { InitializeComponent(); customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent; customer.SavingsAccount.TransactionApprovedEvent += SavingsAccount_TransactionApprovedEvent; } private void SavingsAccount_TransactionApprovedEvent(object sender, string e) { // Update the UI with the new savings transaction savingsTransactionsDataSource.DataSource = null; savingsTransactionsDataSource.DataSource = customer.SavingsAccount.Transactions; savingsBalanceLabel.Text = customer.SavingsAccount.Balance.ToString("C2"); }
- 運行應用程式:
- Tim 運行該應用程式,演示當交易觸發的事件自動更新 UI 的運作。
事件引數資訊:除錯
Tim 示範如何除錯事件並檢查隨其傳遞的資訊。
- 設定斷點:
在事件處理方法中設置斷點,以檢查事件資料。
private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Breakpoint here }private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Breakpoint here }
- 除錯:
- 運行應用程式並執行交易以觸發事件。 在除錯器中檢查事件引數。
- 發送者參數提供引發事件的執行個體,而 e 參數包含事件資料。
建立另一個自定義事件(透支事件)
Tim Corey 通過建立另一個自定義事件來處理透支情境,擴展了演示。 當透支發生時,該事件將被觸發並通知使用者透支金額。
定義透支事件
Tim 首先在 Account 類中定義了一個新的針對透支情境的事件:
- 事件聲明:
使用事件關鍵字定義事件,設為 decimal 型別以傳遞透支金額。
public event EventHandler<decimal> OverdraftEvent;public event EventHandler<decimal> OverdraftEvent;
- 觸發事件:
在程式碼中成功發生透支的部分觸發事件。
if (overdraftSuccessful) { OverdraftEvent?.Invoke(this, overdraftAmount); }if (overdraftSuccessful) { OverdraftEvent?.Invoke(this, overdraftAmount); }
在儀表板側訂閱透支事件
- 連接事件:
在儀表板窗體中訂閱透支事件。
customer.CheckingAccount.OverdraftEvent += CheckingAccount_OverdraftEvent;customer.CheckingAccount.OverdraftEvent += CheckingAccount_OverdraftEvent;
- 處理事件:
定義一個事件處理方法,在發生透支時顯示資訊。
private void CheckingAccount_OverdraftEvent(object sender, decimal e) { errorMessage.Text = $"You had an overdraft protection transfer of {e:C2}"; errorMessage.Visible = true; }private void CheckingAccount_OverdraftEvent(object sender, decimal e) { errorMessage.Text = $"You had an overdraft protection transfer of {e:C2}"; errorMessage.Visible = true; }
- 觸發並顯示事件:
當發生透支時,事件被觸發並更新 UI 以通知使用者。
You had an overdraft protection transfer of $20.44
在多個地方監聽事件
Tim 解釋如何在多個語言和形式中監聽同一事件,顯示事件的靈活性。
- 向另一個窗體新增標籤:
向次級窗體新增標籤以顯示透支消息。
<asp:Label ID="errorMessage" runat="server" Visible="false" /><asp:Label ID="errorMessage" runat="server" Visible="false" />HTML
- 訂閱事件:
在次級窗體中訂閱透支事件。
customer.CheckingAccount.OverdraftEvent += SecondaryForm_OverdraftEvent;customer.CheckingAccount.OverdraftEvent += SecondaryForm_OverdraftEvent;
- 處理事件:
定義一個事件處理方法,以在次級窗體上顯示透支消息。
private void SecondaryForm_OverdraftEvent(object sender, decimal e) { errorMessage.Visible = true; }private void SecondaryForm_OverdraftEvent(object sender, decimal e) { errorMessage.Visible = true; }
- 同時事件處理:
Tim 示範事件可以在多個窗體中同時處理,確保應用程式所有相關部分都正確響應事件。
Both the main form and the secondary form display the overdraft message when the event is triggered.
從記憶體中移除事件監聽器
Tim 指出,清理事件監聽器以防止記憶體洩漏並確保應用程式性能正常的重要性。
- 取消訂閱事件:
在銷毀類執行個體或關閉窗體之前取消訂閱事件至關重要。
customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;為什麼這很重要:
- 未能取消訂閱事件可能會導致記憶體洩漏,因為監聽事件的物件可能無法被正確垃圾回收。
- 使用命名方法:
避免使用匿名函式作為事件處理方法,因為這使得取消訂閱事件變得困難。
// Good practice: using named methods for event handlers// Good practice: using named methods for event handlers
泛型 EventHandler:為 T 傳遞類別
Tim Corey 解釋了通過事件傳遞資料的最佳實踐,特別是使用類別而非簡單資料型別(如字串或 decimal)的好處。
為什麼使用類別作為事件資料
Tim 先討論為什麼事件中使用簡單資料型別較少見,以及使用類別通常是更好的做法:
- 靈活性和擴展性:
- 使用類別允許您通過事件傳遞多個相關的資料。 如果您需要稍後新增更多資料,只需擴展類別即可,而不必更改事件簽名。
- EventArgs 繼承:
- 雖然過去任何通過事件傳遞的物件必須繼承自 EventArgs,但現在不再是必需的。 但是,繼承自 EventArgs 可以對於一致性和明確性有益。
建立透支 EventArgs 類別
Tim 示範如何為透支事件建立自定義 EventArgs 類別。
- 定義類別:
建立一個繼承自 EventArgs 的新類別,並包含要通過事件傳遞的資料屬性。
public class OverdraftEventArgs : EventArgs { public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; } public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo) { AmountOverdrafted = amountOverdrafted; MoreInfo = moreInfo; } }public class OverdraftEventArgs : EventArgs { public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; } public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo) { AmountOverdrafted = amountOverdrafted; MoreInfo = moreInfo; } }
- 在事件中使用類別:
更新事件聲明以使用自定義 EventArgs 類別。
public event EventHandler<OverdraftEventArgs> OverdraftEvent;public event EventHandler<OverdraftEventArgs> OverdraftEvent;
- 觸發事件:
在調用事件時傳遞自定義 EventArgs 類別的執行個體。
OverdraftEvent?.Invoke(this, new OverdraftEventArgs(amountNeeded, "Additional info"));OverdraftEvent?.Invoke(this, new OverdraftEventArgs(amountNeeded, "Additional info"));
唯讀屬性的重大性
Tim 強調在 EventArgs 類別中使用唯讀屬性以防止意外修改事件資料的重要性。
- 避免修改:
- 如果 EventArgs 類別中的屬性有公共設置器,任何事件處理方法都可以修改資料,這可能導致意外行為。
- 使用私有設置器並通過構造函式初始化屬性以確保事件資料保持一致。
- 問題範例:
Tim 示範如何在事件處理程式中的一個方法中修改事件資料如果屬性不是唯讀的,可能會影響其他處理程式。
private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { e.AmountOverdrafted = 1000; // This modification affects all handlers }private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { e.AmountOverdrafted = 1000; // This modification affects all handlers }
- 解決方案:
使用私有設置器並通過構造函式傳遞資料以使屬性為唯讀。
public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; }public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; }
什麼情況下使用公共設置(59:29)
Tim Corey 突出了一個重要的例外,即使用事件資料屬性的私有設置器規則。 這個例外是當您需要允許事件監聽器修改事件資料時,如在基於某些條件可能取消交易的情況下。
範例:取消交易
Tim 提供了一個例子,其中一個事件處理程式可能需要取消交易。 這是通過在自定義 EventArgs 類別中新增一個 CancelTransaction 屬性來實現的。
- 定義屬性:
向 EventArgs 類別新增具有 getter 和 setter 的公共屬性。
public class OverdraftEventArgs : EventArgs { public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; } public bool CancelTransaction { get; set; } = false; public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo) { AmountOverdrafted = amountOverdrafted; MoreInfo = moreInfo; } }public class OverdraftEventArgs : EventArgs { public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; } public bool CancelTransaction { get; set; } = false; public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo) { AmountOverdrafted = amountOverdrafted; MoreInfo = moreInfo; } }
- 在事件處理程式中設置屬性:
在儀表板中,事件處理程式可以設置此屬性以取消交易。
private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { if (denyOverdraft.Checked) { e.CancelTransaction = true; } errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}"; errorMessage.Visible = true; }private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { if (denyOverdraft.Checked) { e.CancelTransaction = true; } errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}"; errorMessage.Visible = true; }
- 在源方法中檢查屬性:
在觸發事件的方法中,檢查 CancelTransaction 屬性是否設為 true,在繼續之前。
if (args.CancelTransaction) { return false; // Transaction is canceled }if (args.CancelTransaction) { return false; // Transaction is canceled }
使應用程式更具互動性
Tim 進一步完善應用程式,使其更具互動性和使用者友好性。
- 新增透支控制的核取方塊:
在表單中新增一個核取方塊,使使用者可以啟用或禁用透支保護。
private void InitializeComponent() { this.denyOverdraft = new System.Windows.Forms.CheckBox(); // Initialize other controls this.denyOverdraft.Text = "Stop Overdrafts"; this.denyOverdraft.CheckedChanged += new System.EventHandler(this.denyOverdraft_CheckedChanged); }private void InitializeComponent() { this.denyOverdraft = new System.Windows.Forms.CheckBox(); // Initialize other controls this.denyOverdraft.Text = "Stop Overdrafts"; this.denyOverdraft.CheckedChanged += new System.EventHandler(this.denyOverdraft_CheckedChanged); }
- 處理核取方塊狀態:
在核取方塊的事件處理方法中,更新邏輯以便在決定取消交易時考慮核取方塊狀態。
private void denyOverdraft_CheckedChanged(object sender, EventArgs e) { if (denyOverdraft.Checked) { // Logic to stop overdraft transactions } }private void denyOverdraft_CheckedChanged(object sender, EventArgs e) { if (denyOverdraft.Checked) { // Logic to stop overdraft transactions } }
- 更新事件處理程式:
確保事件處理程式考慮到核取方塊狀態以允許或拒絕透支。
private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { if (denyOverdraft.Checked) { e.CancelTransaction = true; } errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}"; errorMessage.Visible = true; }private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { if (denyOverdraft.Checked) { e.CancelTransaction = true; } errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}"; errorMessage.Visible = true; }
總結
Tim Corey 通過總結在 C# 中使用事件的關鍵點和最佳實踐來結束教程。
- 移除事件監聽器:
- 始終在銷毀物件之前移除事件監聽器,以防止記憶體洩漏。
使用 -= 運算符取消訂閱事件。
customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;
- 使用 EventArgs 繼承:
- 儘管不是強制的,但繼承自 EventArgs 對於一致性以及使用內建功能(如 EventArgs.Empty)可能是有益的。
- 讀取屬性的私有設置器:
- 使用私有設置器以防止無意的事件資料修改。 僅在必要時允許公共設置器,例如對可取消的交易。
- 事件處理程式語法:
- 使用 EventHandler 代理來定義事件,為傳遞事件資料提供清晰一致的模式。
- 空條件運算符:
- 使用空條件運算符 (?.Invoke()) 以安全地調用事件而不冒空引用例外的風險。
結論
Tim Corey 的C# 事件綜合教程提供了有價值的見解和實用範例,幫助您有效地建立、處理和管理事件。 遵循這些最佳實踐,開發者可以建立更具互動性和響應的應用程式。

