2024年5月18日 星期六

3.VB.NET 基礎篇 筆記 - 委派 (Delegate)

VB.NET 委派(Delegate)筆記(基礎篇)

VB.NET 委派(Delegate) 筆記(基礎篇)

一般按鈕事件常會直接寫死處理內容,例如按下按鈕後固定顯示成功訊息,或固定計算折扣金額。這種寫法很直覺,但當同一個流程需要切換不同處理方式時,程式容易變成一堆 IfSelect Case

Delegate 的用途,是讓某個處理步驟先保留彈性。流程本身先寫好,真正要執行哪一個方法,之後再指定。換句話說,委派可以把「要執行的方法」變成一個可替換的設定。

委派原理分析

用按鈕流程理解 Delegate

假設有一個按鈕,按下後都要把結果顯示到 Label1。不同情境下,顯示內容可能是成功訊息、錯誤訊息或提醒訊息。若每種情境都寫一套完整流程,重複程式會變多。

委派的做法,是把「顯示訊息」這個步驟抽成可替換方法。按鈕流程只負責執行指定的方法,不必把所有情境都寫死在按鈕事件裡。

  • 委派定義規格:規定可放進來的方法需要接收哪些資料。
  • 方法提供實際處理:例如顯示成功、顯示錯誤、顯示提醒。
  • 流程負責執行:流程不固定方法名稱,只執行目前交給委派的方法。

委派可以先理解成三句話:

  1. Delegate 先規定「可以接收哪一種方法」。
  2. AddressOf 把某個方法交給委派。
  3. 呼叫委派時,實際執行被交進來的那個方法。

為什麼需要 Delegate

Delegate 適合用在「流程大致相同,但其中某個步驟可能換掉」的情境。若流程完全固定,直接呼叫方法會比較簡單。若流程要保留替換空間,委派會比較乾淨。

常見情境

  • 同一個按鈕流程,切換不同訊息處理:成功、失敗、警告使用不同方法顯示。
  • 同一個結帳流程,切換不同折扣算法:原價、九折、滿額折抵都可用不同方法處理。
  • 同一個作業完成後,通知多個地方:更新畫面、寫入紀錄、加入清單。
  • 理解事件語法:事件處理程序背後也和方法參考、委派概念有關。

宣告方式與基本語法

Windows Forms 物件配置

以下範例以 Windows Forms 為主。請於 Form1 放置下列控制項:

  • Button1:執行範例。
  • Label1:顯示文字結果。
  • ListBox1:多播委派範例使用,用來顯示多個通知步驟。

場景一:用 Delegate 切換 Label 顯示方式

此範例先示範最直覺的委派用途:同樣是顯示訊息,但實際顯示格式可以替換。

範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Private Delegate Sub MessageDelegate(ByVal text As String)

    Private Sub ShowSuccess(ByVal text As String)
        Label1.Text = "成功:" & text
    End Sub

    Private Sub ShowWarning(ByVal text As String)
        Label1.Text = "提醒:" & text
    End Sub

    Private Sub RunMessage(ByVal handler As MessageDelegate, ByVal content As String)
        handler(content)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        RunMessage(AddressOf ShowSuccess, "資料已儲存")
    End Sub
End Class
畫面輸出結果(Label1.Text)
成功:資料已儲存
邏輯解析
  • MessageDelegate 規定可接收的方法必須有一個 String 參數,且不回傳值。
  • ShowSuccessShowWarning 都符合這個規格,因此都可以交給 MessageDelegate
  • AddressOf ShowSuccess 代表把 ShowSuccess 這個方法交給流程。
  • RunMessage 不固定呼叫哪個顯示方法,只執行傳入的 handler

場景二:改換另一個方法,不改主流程

此範例保留完整程式碼,方便直接複製測試。重點是 RunMessage 完全沒有改,只改變按鈕事件傳入的方法。

範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Private Delegate Sub MessageDelegate(ByVal text As String)

    Private Sub ShowSuccess(ByVal text As String)
        Label1.Text = "成功:" & text
    End Sub

    Private Sub ShowWarning(ByVal text As String)
        Label1.Text = "提醒:" & text
    End Sub

    Private Sub RunMessage(ByVal handler As MessageDelegate, ByVal content As String)
        handler(content)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        RunMessage(AddressOf ShowWarning, "庫存數量偏低")
    End Sub
End Class
畫面輸出結果(Label1.Text)
提醒:庫存數量偏低
邏輯解析
  • RunMessage 是固定流程,負責執行傳入的訊息處理方法。
  • ShowSuccessShowWarning 都符合 MessageDelegate 的規格。
  • 場景一傳入 AddressOf ShowSuccess,本場景改成傳入 AddressOf ShowWarning
  • 主流程不需要重寫,只要替換交進去的方法,即可改變畫面結果。

場景三:Function 委派處理折扣計算

前兩個場景使用 Delegate Sub,適合做顯示或通知。若處理後需要回傳結果,就要使用 Delegate Function

範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Private Delegate Function DiscountDelegate(ByVal amount As Decimal) As Decimal

    Private Function NoDiscount(ByVal amount As Decimal) As Decimal
        Return amount
    End Function

    Private Function TenPercentOff(ByVal amount As Decimal) As Decimal
        Return amount * 0.9D
    End Function

    Private Sub ShowFinalPrice(ByVal rule As DiscountDelegate, ByVal originalPrice As Decimal)
        Dim finalPrice As Decimal = rule(originalPrice)
        Label1.Text = "結帳金額:" & finalPrice.ToString("N0")
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ShowFinalPrice(AddressOf TenPercentOff, 1200D)
    End Sub
End Class
畫面輸出結果(Label1.Text)
結帳金額:1,080
邏輯解析
  • DiscountDelegate 規定可接收的方法必須接收一個 Decimal,並回傳一個 Decimal
  • NoDiscountTenPercentOff 都符合這個規格。
  • ShowFinalPrice 固定負責顯示結帳金額,但折扣規則由外部傳入。
  • 更換 AddressOf NoDiscountAddressOf TenPercentOff,即可改變計算結果。

多播委派機制

一次執行多個通知動作

多播委派可以把多個方法接在同一條呼叫清單中。呼叫委派時,清單中的方法會依序執行。這種做法很適合「一件事情完成後,需要通知多個地方」的情境。

場景四:作業完成後更新多個面板

範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Private Delegate Sub NotifyDelegate(ByVal message As String)

    Private Sub UpdateDashboard(ByVal message As String)
        ListBox1.Items.Add("儀表板更新:" & message)
    End Sub

    Private Sub WriteLog(ByVal message As String)
        ListBox1.Items.Add("寫入紀錄:" & message)
    End Sub

    Private Sub ShowAlert(ByVal message As String)
        ListBox1.Items.Add("提醒通知:" & message)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ListBox1.Items.Clear()

        Dim notifier As NotifyDelegate = AddressOf UpdateDashboard
        notifier = CType([Delegate].Combine(notifier, New NotifyDelegate(AddressOf WriteLog)), NotifyDelegate)
        notifier = CType([Delegate].Combine(notifier, New NotifyDelegate(AddressOf ShowAlert)), NotifyDelegate)

        notifier("月結流程已完成")
    End Sub
End Class
畫面輸出結果(ListBox1)
儀表板更新:月結流程已完成 寫入紀錄:月結流程已完成 提醒通知:月結流程已完成
邏輯解析
  • notifier 一開始只指向 UpdateDashboard
  • Delegate.Combine 會把 WriteLogShowAlert 接到同一條呼叫清單。
  • 呼叫 notifier("月結流程已完成") 時,三個方法依序執行。
  • 多播委派適合通知型流程,因為每個方法都只負責做一個動作。

補充提醒:多播委派較適合 Sub。若多播的是 Function,即使多個方法都會執行,呼叫端通常只會取得最後一個方法的回傳值。

使用限制與常見注意事項

簽名不一致無法指派

委派不是只看方法名稱,而是看方法簽名。簽名包含參數數量、參數型別、參數順序、傳遞方式與回傳型別。只要其中一項不符合,就不能指派給該委派。

場景五:ByVal 與 ByRef 的差異

ByValByRef 也屬於方法簽名的一部分。即使參數型別看起來一樣,只要傳遞方式不同,也不能視為相同簽名。

範例程式碼
VB.NET / Console
Module Program
    Public Delegate Sub ValueDelegate(ByVal number As Integer)

    Private Sub ShowLocalValue(ByVal number As Integer)
        number += 1
        Console.WriteLine("方法內部數值:" & number)
    End Sub

    Private Sub ChangeOriginalValue(ByRef number As Integer)
        number += 1
        Console.WriteLine("原始數值已變更:" & number)
    End Sub

    Sub Main()
        Dim source As Integer = 15
        Dim handler As ValueDelegate = AddressOf ShowLocalValue

        handler(source)
        Console.WriteLine("方法外部數值:" & source)

        ChangeOriginalValue(source)
        Console.WriteLine("ByRef 呼叫後數值:" & source)

        ' 下列寫法無法成立,因為 ByRef 與 ByVal 簽名不同
        ' Dim invalidHandler As ValueDelegate = AddressOf ChangeOriginalValue
    End Sub
End Module
主控台輸出結果
方法內部數值:16 方法外部數值:15 原始數值已變更:16 ByRef 呼叫後數值:16
邏輯解析
  • ValueDelegate 規定方法必須以 ByVal 接收一個 Integer
  • ShowLocalValue 修改的是傳入值的副本,因此外部的 source 仍然是 15。
  • ChangeOriginalValue 使用 ByRef,會直接改動外部變數,因此呼叫後 source 變成 16。
  • 因為 ByValByRef 是不同簽名,所以 ChangeOriginalValue 不能指派給 ValueDelegate

使用委派時應注意:

  • 簽名必須一致:參數數量、型別、順序、傳遞方式與回傳型別都要符合委派定義。
  • 流程固定時不必使用委派:若處理方法不需要替換,直接呼叫方法會更清楚。
  • 多播 Function 要小心:多個方法都回傳值時,呼叫端通常只取得最後一個方法的結果。
  • 事件和委派概念相關:委派描述可呼叫的方法規格;事件則提供對外訂閱與觸發通知的語法模型。

委派適用情境分析

比較項目 直接方法呼叫 委派呼叫
呼叫方式 直接指定方法名稱,例如 ShowSuccess() 先把方法交給委派,再透過委派呼叫。
流程彈性 流程較固定,適合單純邏輯。 方法可替換,適合需要切換行為的流程。
理解成本 較低,程式閱讀較直接。 較高,需要理解簽名、AddressOf 與間接呼叫。
適用情境 固定流程、單一功能、沒有動態替換需求。 訊息顯示切換、折扣規則切換、作業完成通知、事件處理。

重點整理

  1. Delegate 可讓流程中的某個方法呼叫保留替換空間。
  2. AddressOf 用來把方法交給委派,不是立即執行方法。
  3. 委派最重要的規則是簽名必須一致,包含參數、順序、傳遞方式與回傳值。
  4. Delegate Sub 適合訊息顯示、紀錄、通知等不回傳值的動作。
  5. Delegate Function 適合折扣、計算、轉換等需要回傳結果的處理。
  6. 多播委派可一次執行多個方法,常用於作業完成後同步通知多個處理程序。
  7. 若流程固定且沒有替換需求,直接呼叫方法會比委派更簡單。