2025年9月1日 星期一

28.VB.NET (AddressOf)筆記 (進階篇)

VB.NET AddressOf 筆記(進階篇)

VB.NET AddressOf 筆記(進階篇)

AddressOf 用來取得既有方法,並把方法轉成符合指定委派型別的值。它不是直接執行方法,而是把「之後可以呼叫的方法」交給委派、事件、執行緒或其他需要回呼的 API。

常見用途包括事件處理、動態切換處理流程、把工作方法交給 Thread、將多個通知方法串在一起,以及把既有方法指定給 ActionFunc。理解 AddressOf 的關鍵,是先理解委派:委派就是一種「方法格式」。方法參數與回傳值符合委派格式,才能被 AddressOf 指派。

先理解 AddressOf 在做什麼

AddressOf:取得方法並建立委派實例。語法上要寫方法名稱,不加括號,不傳入參數。例如 AddressOf BuildCode 是把方法交出去;BuildCode() 則是立刻執行方法。

AddressOf 的核心觀念

  • 不是呼叫方法:AddressOf MethodName 只是建立可呼叫的委派。
  • 需要目標型別:必須知道要轉成哪一種委派。
  • 簽章要相容:參數與回傳值應符合委派格式。
  • 適合既有方法:方法已經寫好,想交給事件、委派或執行緒使用。
  • 不是萬用替代品:需要臨時包裝參數時,Lambda 通常更自然。

基本語法

VB.NET
Public Delegate Function TextBuilder(source As String) As String

Private currentBuilder As TextBuilder = AddressOf BuildSimpleText

Private Function BuildSimpleText(source As String) As String
    Return source.Trim()
End Function

TextBuilder 是委派型別,代表「接受一個字串,回傳一個字串」的方法格式。BuildSimpleText 符合這個格式,因此可以用 AddressOf 指派給 currentBuilder

寫法 意思 結果
AddressOf BuildText 取得方法並轉成委派。 方法尚未執行。
BuildText("A") 直接呼叫方法。 方法立即執行並回傳結果。
AddHandler ... AddressOf ... 把方法掛到事件。 事件發生時才執行。
New Thread(AddressOf ...) 把方法交給執行緒啟動。 Start 後由新執行緒執行。

委派變數:動態切換要執行的方法

場景一:貨架標籤格式切換

同一筆貨架代碼可能需要輸出成不同格式。這個範例用委派變數保存目前選擇的格式方法,按下按鈕時只呼叫目前委派,不需要在計算按鈕中重複寫多段判斷。

需要的主控項
  • TextBoxShelfCode:輸入貨架代碼。
  • ComboBoxFormat:選擇格式。
  • ButtonBuildLabel:產生標籤。
  • LabelShelfResult:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Public Delegate Function ShelfFormatter(source As String) As String

    Private currentFormatter As ShelfFormatter

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ComboBoxFormat.Items.Clear()
        ComboBoxFormat.Items.Add("一般標籤")
        ComboBoxFormat.Items.Add("盤點標籤")
        ComboBoxFormat.Items.Add("出貨標籤")
        ComboBoxFormat.SelectedIndex = 0
    End Sub

    Private Sub ComboBoxFormat_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBoxFormat.SelectedIndexChanged
        Select Case ComboBoxFormat.SelectedIndex
            Case 0
                currentFormatter = AddressOf BuildNormalLabel
            Case 1
                currentFormatter = AddressOf BuildCheckLabel
            Case 2
                currentFormatter = AddressOf BuildShippingLabel
        End Select
    End Sub

    Private Sub ButtonBuildLabel_Click(sender As Object, e As EventArgs) Handles ButtonBuildLabel.Click
        Dim sourceCode As String = TextBoxShelfCode.Text.Trim()

        If sourceCode = String.Empty Then
            LabelShelfResult.Text = "請輸入貨架代碼"
            Return
        End If

        If currentFormatter Is Nothing Then
            LabelShelfResult.Text = "尚未選擇格式"
            Return
        End If

        LabelShelfResult.Text = currentFormatter(sourceCode)
    End Sub

    Private Function BuildNormalLabel(source As String) As String
        Return "貨架:" & source.ToUpper()
    End Function

    Private Function BuildCheckLabel(source As String) As String
        Return "盤點-" & source.ToUpper() & "-請確認數量"
    End Function

    Private Function BuildShippingLabel(source As String) As String
        Return "SHIP/" & source.ToUpper()
    End Function
End Class
畫面輸出結果(ShelfCode = b12,格式 = 出貨標籤)
SHIP/B12
邏輯解析
  • ShelfFormatter 規定方法必須接收字串並回傳字串。
  • AddressOf BuildShippingLabel 把既有方法指定給委派變數。
  • currentFormatter(sourceCode) 才是真正呼叫目前方法。

事件綁定:AddHandler 與 RemoveHandler

AddHandler:在執行期間把事件和方法連起來。Handles 適合固定綁定;AddHandler 適合動態綁定、批次綁定或依條件綁定。

場景二:多個快捷按鈕共用同一個事件方法

多個按鈕執行相似邏輯時,可以在表單載入時用 AddHandler 批次綁定同一個方法,再從 sender 判斷是哪個按鈕觸發。

需要的主控項
  • ButtonMorning:早班。
  • ButtonAfternoon:午班。
  • ButtonNight:晚班。
  • LabelShiftResult:顯示選擇結果。
範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        AddHandler ButtonMorning.Click, AddressOf SelectShift
        AddHandler ButtonAfternoon.Click, AddressOf SelectShift
        AddHandler ButtonNight.Click, AddressOf SelectShift
    End Sub

    Private Sub SelectShift(sender As Object, e As EventArgs)
        Dim clickedButton As Button = CType(sender, Button)
        LabelShiftResult.Text = "已選擇班別:" & clickedButton.Text
    End Sub
End Class
畫面輸出結果(按下 ButtonNight,文字為 晚班)
已選擇班別:晚班
邏輯解析
  • Button.Click 需要符合 Sub(sender As Object, e As EventArgs) 格式的方法。
  • AddressOf SelectShift 把方法綁到三個按鈕的 Click 事件。
  • sender 代表觸發事件的來源控制項。

場景三:暫停與恢復即時檢查

動態事件綁定也需要能移除。若同一個事件被重複 AddHandler,同一個方法可能被呼叫多次,因此通常會搭配旗標管理綁定狀態。

需要的主控項
  • TextBoxScanCode:輸入掃描碼。
  • ButtonEnableCheck:啟用檢查。
  • ButtonDisableCheck:停用檢查。
  • LabelScanResult:顯示檢查結果。
範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Private checkRegistered As Boolean = False

    Private Sub ButtonEnableCheck_Click(sender As Object, e As EventArgs) Handles ButtonEnableCheck.Click
        If checkRegistered Then
            LabelScanResult.Text = "即時檢查已啟用"
            Return
        End If

        AddHandler TextBoxScanCode.TextChanged, AddressOf ScanCodeChanged
        checkRegistered = True
        LabelScanResult.Text = "即時檢查已啟用"
    End Sub

    Private Sub ButtonDisableCheck_Click(sender As Object, e As EventArgs) Handles ButtonDisableCheck.Click
        If Not checkRegistered Then
            LabelScanResult.Text = "即時檢查尚未啟用"
            Return
        End If

        RemoveHandler TextBoxScanCode.TextChanged, AddressOf ScanCodeChanged
        checkRegistered = False
        LabelScanResult.Text = "即時檢查已停用"
    End Sub

    Private Sub ScanCodeChanged(sender As Object, e As EventArgs)
        Dim codeText As String = TextBoxScanCode.Text.Trim().ToUpper()

        If codeText.StartsWith("A-") Then
            LabelScanResult.Text = "格式:A 類掃描碼"
        Else
            LabelScanResult.Text = "格式:待確認"
        End If
    End Sub
End Class
畫面輸出結果(啟用後輸入 A-1028)
格式:A 類掃描碼
邏輯解析
  • AddHandler 使用 AddressOf 加入事件處理方法。
  • RemoveHandler 使用同一個方法移除事件處理方法。
  • checkRegistered 避免同一個處理器被重複加入。

ThreadStart:把方法交給執行緒

場景四:背景檢查完成後通知畫面

Thread 建構函式需要一個可執行的方法入口。AddressOf RunTicketCheck 會建立符合 ThreadStart 的委派;真正開始執行的時間點是呼叫 Start

需要的主控項
  • ButtonStartTicketCheck:開始背景檢查。
  • LabelThreadResult:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading

Public Class Form1
    Private Sub ButtonStartTicketCheck_Click(sender As Object, e As EventArgs) Handles ButtonStartTicketCheck.Click
        LabelThreadResult.Text = "票券檢查中..."

        Dim worker As New Thread(AddressOf RunTicketCheck)
        worker.IsBackground = True
        worker.Start()
    End Sub

    Private Sub RunTicketCheck()
        Thread.Sleep(800)

        If LabelThreadResult.IsHandleCreated AndAlso Not LabelThreadResult.IsDisposed Then
            LabelThreadResult.BeginInvoke(New MethodInvoker(AddressOf ShowTicketFinished))
        End If
    End Sub

    Private Sub ShowTicketFinished()
        LabelThreadResult.Text = "票券檢查完成"
    End Sub
End Class
畫面輸出結果(LabelThreadResult.Text)
票券檢查中... 票券檢查完成
邏輯解析
  • New Thread(AddressOf RunTicketCheck) 只是指定背景方法。
  • worker.Start() 之後,背景執行緒才開始執行。
  • 背景執行緒不能直接更新 Windows Forms 控制項,因此用 BeginInvoke 回到 UI 執行緒。

多重委派:一次通知多個方法

場景五:入庫完成後同時更新紀錄

多重委派可以把多個 Sub 串在同一個委派中。呼叫委派時,會依序執行委派清單中的方法。這類寫法適合通知、紀錄與狀態更新。

需要的主控項
  • TextBoxReceiveCode:輸入入庫代碼。
  • ButtonReceiveItem:完成入庫。
  • ListBoxReceiveLog:顯示紀錄。
  • LabelReceiveStatus:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Public Delegate Sub ReceiveNotice(message As String)

    Private receiveHandlers As ReceiveNotice

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        receiveHandlers = New ReceiveNotice(AddressOf AddReceiveLog)
        receiveHandlers = CType([Delegate].Combine(receiveHandlers,
                                                   New ReceiveNotice(AddressOf ShowReceiveStatus)),
                                ReceiveNotice)
    End Sub

    Private Sub ButtonReceiveItem_Click(sender As Object, e As EventArgs) Handles ButtonReceiveItem.Click
        Dim receiveCode As String = TextBoxReceiveCode.Text.Trim().ToUpper()

        If receiveCode = String.Empty Then
            LabelReceiveStatus.Text = "請輸入入庫代碼"
            Return
        End If

        receiveHandlers.Invoke("入庫完成:" & receiveCode)
    End Sub

    Private Sub AddReceiveLog(message As String)
        ListBoxReceiveLog.Items.Add(DateTime.Now.ToString("HH:mm:ss") & " " & message)
    End Sub

    Private Sub ShowReceiveStatus(message As String)
        LabelReceiveStatus.Text = message
    End Sub
End Class
畫面輸出結果(ReceiveCode = r-308)
入庫完成:R-308
邏輯解析
  • ReceiveNotice 規定通知方法必須接收一個字串且不回傳值。
  • Delegate.Combine 把多個通知方法串成同一個委派。
  • 多重委派若有回傳值,通常只會取得最後一個方法的回傳結果,因此通知型 Sub 更直覺。

Action、Func 與 Lambda 的選擇

寫法 適合情境 注意事項
自訂 Delegate + AddressOf 需要有明確業務名稱的方法格式。 較清楚,但要先宣告委派型別。
Action + AddressOf 沒有回傳值的方法。 名稱較通用,語意可能不如自訂委派。
Func + AddressOf 有回傳值的方法。 參數多時可讀性會下降。
Lambda 需要臨時包裝參數或內嵌短邏輯。 過長會降低可讀性。

場景六:取號文字格式化

Func 是內建委派型別,適合簡單方法格式。這個範例用 Func(Of Integer, String) 保存不同的取號格式方法。

需要的主控項
  • TextBoxQueueNo:輸入號碼。
  • ComboBoxQueueStyle:選擇格式。
  • ButtonFormatQueueNo:格式化。
  • LabelQueueResult:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Private queueFormatter As Func(Of Integer, String)

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ComboBoxQueueStyle.Items.Clear()
        ComboBoxQueueStyle.Items.Add("一般號碼")
        ComboBoxQueueStyle.Items.Add("客服號碼")
        ComboBoxQueueStyle.SelectedIndex = 0
    End Sub

    Private Sub ComboBoxQueueStyle_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBoxQueueStyle.SelectedIndexChanged
        If ComboBoxQueueStyle.SelectedIndex = 0 Then
            queueFormatter = AddressOf FormatNormalQueueNo
        Else
            queueFormatter = AddressOf FormatServiceQueueNo
        End If
    End Sub

    Private Sub ButtonFormatQueueNo_Click(sender As Object, e As EventArgs) Handles ButtonFormatQueueNo.Click
        Dim queueNo As Integer

        If Not Integer.TryParse(TextBoxQueueNo.Text.Trim(), queueNo) OrElse queueNo <= 0 Then
            LabelQueueResult.Text = "請輸入大於 0 的號碼"
            Return
        End If

        LabelQueueResult.Text = queueFormatter(queueNo)
    End Sub

    Private Function FormatNormalQueueNo(queueNo As Integer) As String
        Return "NO-" & queueNo.ToString("000")
    End Function

    Private Function FormatServiceQueueNo(queueNo As Integer) As String
        Return "SERVICE-" & queueNo.ToString("0000")
    End Function
End Class
畫面輸出結果(QueueNo = 17,格式 = 客服號碼)
SERVICE-0017
邏輯解析
  • Func(Of Integer, String) 代表接收整數並回傳字串的方法格式。
  • AddressOf FormatServiceQueueNo 指向既有格式化方法。
  • 若委派用途有強烈業務語意,自訂委派名稱通常更清楚。

Lambda 適用情境:當事件方法需要額外固定參數時,AddressOf 不能直接帶參數,通常改用 Lambda 包裝。例如 AddHandler ButtonQuick.Click, Sub(sender, e) ApplyDiscount(50)

實務判斷與常見誤區

常見問題整理

  • AddressOf Method() 當成語法:AddressOf 後面放方法名稱,不加括號。
  • 把 AddressOf 當成立即執行:它只是建立委派,呼叫委派時才會執行方法。
  • 忽略簽章相容性:方法參數與回傳值應符合目標委派。
  • 重複 AddHandler:同一個處理器可能被呼叫多次,應避免重複註冊。
  • 忘記 RemoveHandler:動態綁定後若不再需要,應適時移除。
  • 用 AddressOf 硬包參數:需要額外參數時,Lambda 通常更合適。
  • 把耗時工作放進事件處理器:AddressOf 只是指定方法,不會自動讓方法變成背景工作。
需求 建議寫法 原因
固定事件處理 Handles 最簡潔,適合設計階段就固定的事件。
動態事件綁定 AddHandler ... AddressOf ... 可在執行期間依條件加入處理器。
動態切換方法 自訂委派或 Func / Action 可把目前策略保存成變數。
需要額外固定參數 Lambda AddressOf 不能直接附帶參數。
背景執行緒入口 New Thread(AddressOf Method) Thread 需要符合格式的方法入口。

重點整理

  1. AddressOf 用來把既有方法轉成委派,不會立即執行方法。
  2. AddressOf 後面放方法名稱,不加括號,也不傳入參數。
  3. 方法必須和目標委派的參數與回傳值相容。
  4. 自訂委派適合語意明確的業務流程。
  5. Action 適合無回傳值方法,Func 適合有回傳值方法。
  6. AddHandler 搭配 AddressOf 可在執行期間動態綁定事件。
  7. RemoveHandler 可移除已加入的事件處理方法,避免不必要的重複觸發。
  8. 需要臨時包裝參數或內嵌短邏輯時,Lambda 通常比 AddressOf 更合適。