跳到主要內容

C# 委派(Delegate) (二) - Anonymous Function、Lambda Expression


上一篇中C# 委派(Delegate) (一)介紹了Delegate類基本觀念、宣告方式與使用方式,本篇將照著C#中委派的發展史中,繼續介紹Anonymous Function、Lambda Expression。


  • Anonymous Function



還記得上篇的範例中,我們每宣告一個委派(delegate)的變數(variable)就要寫一個跟委派相同簽章的具名方法對應是吧?

但是這個專用來給委派變數宣告用具名方法,具名不是顯得很多餘嗎,我們似乎不太需要知道這個方法叫什麼名字啊。
        public delegate int MyDelegate(int x);
        static void Main(string[] args)
        {
            //AddOne是專門寫來給委派變數使用的方法,這個具名方法只用一次,從此以後AddOne沒在被用過。
            MyDelegate mydelegate = new MyDelegate(AddOne);
        }
        public static int AddOne(int number)
        {
            return ++number;
        }
所以在C#2.0中貼心的微軟發展了一套專門給委派變數宣告專用的語法糖(Syntactic sugar)叫做匿名方法(Anonymous Function)。
        public delegate int MyDelegate(int x);
        static void Main(string[] args)
        {
            //不需要再寫一個AddOne方法
            MyDelegate mydelegate = delegate (int number) { return number + 1; };
        }
所以在C#2.0中只要如上宣告就完成了委派變數的宣告。匿名方法的變數宣告中,左式必須要使用明確的委派類別來宣告(這邊使用MyDelegate),不能使用var關鍵字,如果堅持要使用var關鍵字宣告,本文最後面會提及。而右式指派的方式為delegate (參數型態 參數名稱) { 方法內容 };,如果沒有回傳型態則不用return,這樣就完成了匿名方法的宣告。

  • Lambda Expression

C#3.0中,隨著Linq被引進,微軟更進一步發展語法更簡潔,功能更強大的匿名方法,這種匿名方法就是著名的Lambda表達式(Lambda Expression),Lambda不只可以完全取代原本的匿名方法,還可以進一步的發展出強大的表達式樹(Expression Tree,表達式樹的應用在整個C#的學習上非常進階,在此先不提及)。

Lambda表達式分為兩種

  1. 運算式Lambda(Expression Lambdas)
  2. 陳述式Lambda(Statement Lambdas)
1.運算式Lambda(input-parameters) => expression
        public delegate int MyDelegate(int x);
        public delegate int MyDelegate2(int x,int y);
        public delegate void MyVoidDelegate(object o);
        static void Main(string[] args)
        {
            MyDelegate mydelegate = x =>x + 1;
            MyDelegate2 mydelegate2 = (x,y) => x + 1;
            MyVoidDelegate myVoidDelegate = o => Console.WriteLine(o);
        }
Lambda運算式的宣告如上,特性就是只能有一行的運算式,而且只有為一個參數的時候參數的括弧可以省略(例:mydelegate),依照委派變數的簽章,運算式Lambda會自動回傳將運算式主體回傳,所以不用加上return,x=>x+1;等效於一個return x+1;的方法。運算式Lambda不只可以用於委派變數的指派,也能用於Expression Tree變數的指派。

2.陳述式Lambda(input-parameters) => { statement; }
不同於運算式Lambda的陳述式Lambda,必須在=>右邊的主體加上大括弧,一個陳述式Lambda可以擁有複數的陳述式,而且根據回傳簽章必須明確的做出回傳的陳述式。
        public delegate int MyDelegate(int x);
        public delegate int MyDelegate2(int x,int y);
        public delegate void MyVoidDelegate(object o);
        static void Main(string[] args)
        {
            MyDelegate mydelegate = x => { return x + 1; };
            MyDelegate2 mydelegate2 = (x,y) => { return x + y + 1; };
            MyVoidDelegate myVoidDelegate = o => { Console.WriteLine(o); };
        }
陳述式Lambda只能用於委派變數的指派,不能用於Expression Tree變數的指派,一個運算式Lambda要化為一個陳述式Lambda非常簡單,只要加上大括弧在根據簽章決定要不要回傳即可,除此之外陳述式Lambda也能在參數的部分直接指明型態(int x)=>{return x+1;}

LambdaExpression的出現,更簡潔的語法完全的取代了Anonymous Function,官網中甚至建議在C#3.0以後只要使用Lambda Expression就好,也把Lambda Expression、Anonymous Function一起重新統稱叫做匿名方法Anonymous Function,而delegate (參數型態 參數名稱) { 方法內容 };的方法已經不需要在使用我們只要看得懂、知道觀念、知道這一段發展的歷史就好了。

至此為止,委派的介紹已經快要結束,實際上現在的委派還是不太好用不太直覺,下一篇中將會介紹C#委派的完成體,Func<>、Action<>是如何配合Lambda成為一個C#中強大的功能與顯著的特色。


總結來講:


  1. 微軟為了委派(delegate)變數宣告的方便,在C#2.0中發展出Anonymous Function的語法。
  2. 隨後在C#3.0隨著Linq推出更簡潔的Lambda Expression完全取代Anonymous Function。
  3. 現在Lambda Expression、Anonymous Function一起重新統稱叫做匿名方法Anonymous Function。
  4. Lambda Expression分為運算式Lambda(Expression Lambdas),與陳述式Lambda(Statement Lambdas),兩者都能用於委派的指派但只有運算式Lambda可以用於表達式樹的指派(Expression Tree)。


進階 -- 為什麼不能使用var隱含類型宣告委派


如果今天我們的了一種,不行!不用關鍵字var來宣告一定會死的病。

委派執行個體變數其實也是可以使用var關鍵字來宣告的。

1.直接使用方法名稱直接指派給宣告委派執行個體變數
var del =ToString;
會發現錯誤訊息,方法群組不能指派給隱含類型變數。
因為ToString代表的不只是一個方法,他可能代表著一個或數個方法多載(overload)集合的群組,曾做方法群組,就算右式給的是自訂義的方法,已經確定了沒有多載,編譯器(compiler)還是無法確定我們之後會不會用這個自訂義的方法名稱去擴充多載。
錯誤原因:編譯器不知道我們的委派最後會是方法群組中的哪個方法

2.使用Lambda來指派委派執行個體變數

經歷1.的錯誤後那我們避開使用方法群組來指派var宣告委派,直接使用Lambda表達來指派。
var del = x=>x+1
var del = x=>{return x+1;};

很明顯的可以看出,我們根本無法判斷del後面的委派到底是什麼類型。
錯誤原因:編譯器不知道我們參數類型,或是回傳型態。

3.明確給予參數類型與回傳型態的匿名方法來指派

到這我們就是不信邪,那我們給變數執行個體var直接指派時就告訴他型態怎麼樣?
var del = (int x)=>{return x+1;}
var del = delegate(int x){return x+1;}
結果還是錯誤,我們都已經明確告知型態了怎麼還是不行呢。

最後這一個範例牽扯到的問題比較深跟編譯器運行的設計有關,到這邊的探討為止,我們可以發現前兩種情況是錯的而第三種情況似乎是可行的,但總地而言這三種都是類似的寫法,今天一種類似的寫法可能有時候可行有時不行,如果編譯器要針對這些情況去判讀哪些是可行的而那些是不行的,所花的代價遠遠遠遠遠大於為了讓我們少打幾個字(Ide還會幫我們做),這就是為什麼不建議使用var來宣告委派執行個體的原因。

所以當然爾,假如我們一開始告訴編譯器明確的委派簽章。
            var del = (MyStringDelegate)Console.WriteLine;
            var del2 = (MyDelegate)(x=> x+ 1);
            var del3 = (MyDelegate)(x => { return x + 1; });
            var del4 = (MyDelegate)(delegate(int x){ return x + 1; });
            var del = (Action<string>)Console.WriteLine;
            var del2 = (Func<int, int>)(x => x + 1);
            var del3 = (Func<int, int>)(x => { return x + 1; });
            var del4 = (Func<int, int>)(delegate (int x) { return x + 1; });
這時候,我們最終就能以var完成委派執行個體的宣告。
各種var宣告委派執行個體變數一次Show。



小小心得

若有謬誤

歡迎底下留言指正


上一篇:C# 委派(Delegate) (一)



參考:
https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions

留言

這個網誌中的熱門文章

C# 委派(Delegate) (一)

今天要介紹的是C#的委派 Delegate Delegate的發展由函數指標( funtion pointer )而來,可以說Delegate是函數指標中的語法糖( Syntactic sugar )也不為過,在Java中則是Sam類。早期在C# 1.0的時候,匿名函數與Lambda表達式還沒有被發展出來,C與C++有函數指標來負責打包函數( function ),那C#呢?我們都知道物件導向程式OOP中所有的東西都該是物件,所以C#就創造了一個類別專門來打包函數。 本篇開始會依序從C#的發展順序介紹委派 Delegate、Anonymous Function、 Lambda  Expression、Func<>、Action<> 委派含括很大的範圍,故可能會花上幾篇的篇幅來一一介紹。   1.Delegate 首先來介紹的是最初的委派( Delegate )。月亮是外國的圓,我們先來看看微軟( MicroSoft )怎麼介紹自家的的委派-- A delegate is a reference type that can be used to encapsulate a named or an anonymous method -- 一種用來封裝具名或匿名方法的參考 類型 。至於為什麼封裝函數叫做委派呢,不如試想,原本有一個流程方法(Sop)是要由A公司去完成的,但我們將他外包給委派給B公司去做,把方法流程外包的行為就叫做委派,這樣不難理解吧。 廢話不多說,立刻來看委派是如何被宣告( declare )以及使用的。 委派的宣告: public delegate int MyDelegete(int x); 我們可以注意到一個委派類型的宣告簽章( Signatures ),就像是類似宣告一個方法般,給他回傳型態、名稱、參數就完成宣告了,所以我們先做的是宣告一個回傳型態為int、委派類型名稱為Mydelegate、參數型態為int的委派宣告。好啦,宣告完成拉接下來是封裝方法。既然委派是一種類型,所以我們必須將他實作( implement )。 挖哩怎麼報紅了,原來委派類型變數的宣告必須在建構子參數中給與這個符合此委派類型簽章的方法,於是我們必須相應而生出一個回傳型態、參數型態皆

Reference Type & Value Type

相信有仔細看過前一篇文章後 對於C#中Value Type 與Ref  Type有了一些初步的了解(才怪) 今天直接 Passing By Value 與 Passing By Reference來探討Value Type 與Ref  Type的差別 話不多說舉個例子 1.參考型別(Reference Type) 假設某個學校出了一個模範生小明,老師希望以後所有的學生都像小明一樣聰明 所以把小明克隆了一個新的取名叫做小華 看以下程式碼 public static void Main() { var student1 = new Stutent(); student1.Name = "阿明"; var student2 = student1; student2.Name = "小華"; Console.WriteLine(student1.Name); } class Stutent { public string Name{get;set;} } 猜猜Output出什麼呢 答案是小華 什麼? 為什麼? 靠北不是把令了一個新的變數student2來裝student1了嘛 那為什麼把新生出來的student2的名字取為小華 導致原本的小明也變成小華了呢 老師感到疑惑,小明到底跑哪去了呢,聰明的老師立刻打開電腦 Google了Msdn才發現   阿~!  小華原來就是小明 還裝?! 因為  Class Student是一個 參考型別 參考型別的變數 承載的是記憶體的位置 將一個新的座位,指派給小明, 並把新座位的主人改名為小華, 然後在請原本小明座位上的人大聲喊出自己的名字 小明 當然就會說 "我是小華" 囉 仔細看看這段程式碼 var student1 = new Stutent(); 再把它中文翻譯 我宣告了一個變數student1 並指派為一個新的學生 口語邏輯上