iT邦幫忙

0

C# delegate 委派

IT邦第二篇 就獻給委派了

記得當年第一次看到 += 這東西的時候

問問前輩這是什麼

前輩只有跟我說 : 委派 很可怕 不要用~~~

真正深入了解之後才覺得相見恨晚啊!

先從委派最正規的方式寫起(但我幾乎不用這種方式...)

public delegate void DoSomething(int number);

先宣告一個委派的簽章 這個簽章代表晚點要賦予它的方法

必須符合 無回傳值(void) 且 具有一個參數 int

換句話說 如果宣告成如下

public delegate int ParseSomething(string str);

就代表 該方法必須回傳int 且 具有一個參數 string

public static void PrintNumber(int n)
{
    Console.WriteLine(n);
}
public static void SquareAndPrintNumber(int n)
{
    n *= n;
    Console.WriteLine(n);
}
static void Main(string[] args)
{
    var something = new DoSomething(PrintNumber);
    something.Invoke(5);
    something(6);
}

我宣告了兩個方法 PrintNumber 跟 SquareAndPrintNumber

都符合DoSomething的簽章 (無回傳 且具有一個參數int)

在這個範例中 先使用PrintNumber

var something = new DoSomething(PrintNumber);就是建立一個委派 並把PrintNumber傳入

something.Invoke(5);就是真正去執行這個委派 PrintNumber 所以最後會印出 5

something(6); 是執行委派的另外一個方式 所以會印出6

我們修改一下程式碼 將 PrintNumber更換成 SquareAndPrintNumber

static void Main(string[] args)
{
    var something = new DoSomething(SquareAndPrintNumber);
    something.Invoke(5);
    something(6);
}

這時候要執行的方法就會變成SquareAndPrintNumber

所以印出來會是 25 36

我們再做一個實驗 如果有兩個委派 具有相同的簽章 他們是否可以互通呢?

我們修改一下程式碼 新增一個DoSomething1 跟 DoSomething一模一樣 只是名稱不同

再修改一下呼叫委派的方式

public delegate void DoSomething1(int number);

public static void ExeDoSomething(DoSomething something)//呼叫委派的方式
{
    something.Invoke(5);
    something(6);
}

static void Main(string[] args)
{
    {
        var something = new DoSomething(PrintNumber);
        ExeDoSomething(something);
    }
    {
        var something = new DoSomething1(PrintNumber);
        ExeDoSomething(something);//error
    }
}

會發現 就算簽章一樣 只要宣告的委派不同 他們之間是不可以互相轉換的~相當的嚴謹

照上面正規委派寫法 我可能一個方法就必須要建立一個public delegate void DoSomething1(int number);這種委派簽章

又臭又長 使用上又不方便

接下來 就是巨硬的德政了!Action/Func

Action/Func 是泛型的委派(泛型之後會再開一篇來講 不要在這邊亂開副本!)

我如果需要一個無回傳值且具有一個int參數的方法 我不需要從頭宣告一個

public delegate void DoSomething1(int number);來用

我只要寫Action 就可以了

Action                      action1;//無回傳值無參數
Action<int>                 action2;//無回傳值具有一個int參數
Action<int, string>         action3;//無回傳值具有int string參數
Func<int>                   func1;//回傳int 無參數
Func<string,int>            func2;//回傳int 具有一個string參數
Func<int,string,DateTime>   func3;//回傳DateTime具有int string參數

上面這些宣告可以自行體會一下

我們可以很容易地知道 Action就是無回傳值的委派 而Func就是有回傳值的

其餘用法幾乎沒有差異

附帶一提 以前最多支援到8個型別參數 也就是說我最多可以寫

Action<int,string,double,float,Datetime,long,List,bool> //應該沒人會這樣寫..吧?

但剛剛看了一下程式碼.Net6 已經可以支援到16個參數型別了呢!(灑花?

回歸正題

修改一下程式碼 將 ExeDoSomething的參數從 DoSomething 更換成 Action

public static void ExeDoSomething(Action<int> something)
{
    something.Invoke(5);
    something(6);
}

static void Main(string[] args)
{
    ExeDoSomething(PrintNumber);
    ExeDoSomething(SquareAndPrintNumber);
}

因為 PrintNumber 跟 SquareAndPrintNumber 都是符合 無回傳值 有一個int參數的方法簽章

所以他們都可以當作參數傳入ExeDoSomething

執行結果會是 5, 6, 25, 36

喔~ 可是還是好煩阿 要把方法當參數傳入 我必須要建立一個方法才能這樣做

想名字應該是程序猿永遠的痛吧..

還好還好 巨硬的德政 匿名委派 我們可以這樣寫

上面那兩個方法可以砍掉了(PrintNumber/SquareAndPrintNumber)

static void Main(string[] args)
{
    ExeDoSomething((int n) => 
    {
        Console.WriteLine(n);
    });
    ExeDoSomething((int n) =>
    {
        n *= n;
        Console.WriteLine(n);
    });
}

(int n) 這邊可以想成就是方法後面的參數簽章 前面沒有名字 => 後面是方法主體

這種lambda寫法在巨硬很常使用,建議要習慣一下!

喔喔喔喔!! 不用想名字了真好~ 剛剛光想PrintNumber/SquareAndPrintNumber 就花了我兩小時呢!

修但幾累!

巨硬表示 我覺得這樣看起來還是有點蠢

因為ExeDoSomething 裡面的參數就已經知道 Action的參數是int 為什麼你外面還要寫一次?

    ExeDoSomething((n) => 
    {
        Console.WriteLine(n);
    });

巨硬表示 : 我覺得只有一個參數n還要加括號有點蠢

    ExeDoSomething(n => 
    {
        Console.WriteLine(n);
    });

巨硬表示 : 程式本體只有一行 應該可以再精簡吧?

ExeDoSomething(n => Console.WriteLine(n));

流浪漢表示 : (n)後面沒加分號 (這裡不用加啊!!!!!

這種寫法在巨硬稱做lambda表達式

上面這種寫法 有幾個限制

第一 參數要一個才能省略參數的括號

第二 如果沒參數的畫 一定要有括號

第三 程式碼如果只有一行 可以省略該行的分號以及程式本體的大括號({})(分號 大括號必須同時存在/省略)

參數型別都可以省略

Func<int> func = () => 1;//因為無參數 所以都是回傳1;
Func<int,int> func2 = x => x+1;//具有參數 回傳 x+1

委派基礎就到這邊!

下一篇預計會開委派的實作

因為知道委派但是不知道怎麼去活用它還是沒用阿!!!

------3/8號補充-----

本來一直記得要講這個重要的東西

但居然忘記了...Orz(這年代還有人用這個嗎?

委派的 閉包問題

先上Code

static void Main(string[] args)
{
    Action action = null;

    for(int i = 0; i < 10; i++)
    {
        action += () => Console.WriteLine(i);
    }
    action();
}

會印出什麼?

0 1 2 3 4 5 6 7 8 9 ??

不對

會是

10 10 10 10 10 10 10 10 10 10

Why?

因為委派的方法是 印出i

但我只是去設定委派內容

真正在執行的是 action(); 這一行

而i跑完迴圈 for(int i = 0; i < 10; i++) 跳離迴圈時會是10

所以真正執行的時候 是印出 10

那我真正要印出0~9怎麼辦?

給他一個暫存變數即可

static void Main(string[] args)
{
    Action action = null;

    for(int i = 0; i <10;i++)
    {
        var temp = i;
        action += () => Console.WriteLine(temp);
    }
    action();
}

這樣 其實記憶體會產生10個temp 而每個temp分別就是 0-9

最後執行的時候就是執行印出各自的temp

使用委派要特別注意執行的時機跟變數的值喔!


2 則留言

0
入坑到棄坑
iT邦新手 5 級 ‧ 2021-03-05 15:59:18

下一篇預計會開委派的實作

坐等大大開講 /images/emoticon/emoticon08.gif

0
koro_michael
iT邦新手 4 級 ‧ 2021-03-05 17:27:00

學過 JS GO 或任何一級函式語言的開發者應該可以輕鬆理解

C# 的委派換句話說就是想要把函式當作變數傳遞

我要留言

立即登入留言