C# 委派(delegate) 介紹

委派(delegate) 到底是什麼? 在微軟官方文件上的那些說明勸退了一堆想學習的新手,就算啃完了還是不太明白...這篇努力使用最簡單的方式說明。

委派(delegate) 其實就是能夠將方法作為參數傳遞給其他方法,將呼叫方法和實作方法內容分開,最主要的目的在於降低耦合。

我們先來看看一段小程式,這是在 WinForms 中動態建立一個按鈕(Button),並在按鈕被按下時使用 MessageBox 顯示訊息:
    

Button button = new();
button.Text = "顯示對話框";
button.Click += (sender, args) =>
{
    MessageBox.Show("Hello World");
};
this.Controls.Add(button);
    

上面的重點是第 3 行,我們將按鈕的點擊事件(Click) 綁定了一個匿名方法(Anonymous Methods), Click 事件是一個 EventHandler ,而 EventHandler 就是我們要介紹到的委派(delegate)。

在寫按鈕(Button)這個元件的微軟工程師並不知道按鈕按下去後我們會要執行什麼動作,他也不需要把按鈕按下去後要執行的所有動作寫完,就只是呼叫 Click 這個事件而已,至於誰要去接這個事件就不是那位工程師需要擔心的了。 而我們也不用去了解按鈕怎麼觸發 Click 事件,我們只要知道要決定 Click 事件發生時要做什麼事情即可,這就是 delegate 的用途。

還是有點不太懂?沒關係我們先來寫寫看。一樣是剛剛的例子,這次假設換我們要將按鈕點擊後的資料處理並輸出,但是我們不知道具體的輸出邏輯,有下一個軟體工程師小明會負責這件事情,我們只要把要輸出的資料給他,剩下的不用管。若是以前我們會寫一個空的方法給他:
    

    public Form1()
    {
        InitializeComponent();

        Bitmap bitmap = Resources.ruyut;
        this.Icon = Icon.FromHandle(bitmap.GetHicon());


        Button button = new();
        button.Text = "顯示對話框";
        button.Click += (sender, args) =>
        {
            Output("Hello World");
        };
        this.Controls.Add(button);
	}
    
    void Output(string message)
    {
        // todo: Hi 小明工程師, 我就幫你到這了,剩下的邏輯我也不知道,你自己想辦法
    }
    

但是以後小明工程師每次要修改程式碼都會動到我們的類別,不過我們現在會使用 delegate 了,他可以在他的類別使用我們的 Output 即可,就像是我們使用 button 的 Click 一樣。
新的寫法:
    

    public Form1()
    {
        InitializeComponent();

        Bitmap bitmap = Resources.ruyut;
        this.Icon = Icon.FromHandle(bitmap.GetHicon());


        Button button = new();
        button.Text = "顯示對話框";
        button.Click += (sender, args) =>
        {
            Output?.Invoke("Hello World");
        };
        this.Controls.Add(button);
	}

    public delegate void OutputHandler(string output);

    public OutputHandler Output;
    

那位負責處理輸出的小明工程師就可以像我們使用 Click 一樣直接使用 Output方法:
    

        Output += (message) =>
        {
            MessageBox.Show($"OutputHandler: {message}");
        };
    

看完了之後大概懂了,下面整理一下寫法

宣告 delegate 和會觸發的方法:
    

public delegate void OutputHandler(string output); // 我們呼叫 OutputHandler

public OutputHandler Output; // 小明工程師從 Output 得知事件
    

我們呼叫 OutputHandler 的方式:
    

Output?.Invoke("小明工程師會取得的訊息");
    

小明工程師綁定的幾種方式:
使用具名方法:
    

    public Form1()
    {
    	// 省略前面我們寫的方式
        
        Output += new OutputHandler(ConsoleOutput);
    }
    
    public void ConsoleOutput(string output)
    {
        Console.WriteLine(output);
    }
    

使用匿名方法:
    

        Output += (message) =>
        {
            MessageBox.Show($"OutputHandler: {message}");
        };
    

另外也可以使用 +, +=, =, -, -= 來指定會被呼叫的方法,也就是小明工程師想要我們的 Output 一次做很多件事情時使用 += 就可以達成,如果某天想要所有事情都被取消時,直接指派為 null 即可 (Output = null)
註:若其中一個發生例外,後面的會被中止執行

補充:
上面委派使用時的程式碼可以省略 Invoke ,直接簡寫為:
    

// Output?.Invoke("小明工程師會取得的訊息");
Output("小明工程師會取得的訊息");
    

但是在找不到委派實例的時候會拋出例外:
    

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
    

所以還是建議使用 ?.Invoke 的方式,在執行前先檢查是否為空。

延伸閱讀: C# 進階委派(delegate)介紹: Action 、 Func 和 Predicate

參考資料:
Microsoft.Learn - Delegates (C# Programming Guide)
Microsoft.Learn - Handle and raise events
StackOverflow - Is using Action.Invoke considered best practice?

留言