1. 程式人生 > >.NET中那些所謂的新語法之三:系統預定義委託與Lambda表示式

.NET中那些所謂的新語法之三:系統預定義委託與Lambda表示式

開篇:在上一篇中,我們瞭解了匿名類、匿名方法與擴充套件方法等所謂的新語法,這一篇我們繼續征程,看看系統預定義委託(Action/Func/Predicate)和超愛的Lambda表示式。為了方便碼農們,.Net基類庫針對實際開發中最常用的情形提供了幾個預定義好的委託,這些委託可以直接使用,無需再重頭定義一個自己的委託型別。預定義委託在.Net基類庫中使用的比較廣泛,比如在Lambda表示式和平行計算中都大量地使用,需要我們予以關注起來!

/* 新語法索引 */

  自 .NET Framework 3.5 (C# 3.0)以來,各種泛型委託紛湧而至,原先需要我們程式設計師手動定義的一些委託現在我們可以直接使用預定義的委託了,大大提高了開發效率,現在我們就首先來看看這些預定義的泛型委託。

一、無返回型別的內建委託—Action

1.1 初識Action

MSDN給出的定義:封裝一個方法,該方法不具有引數並且不返回值

  可以使用此委託以引數形式傳遞方法,而不用顯式宣告自定義的委託。封裝的方法必須與此委託定義的方法簽名相對應。也就是說,封裝的方法不得具有引數,並且不得返回值。(在 C# 中,該方法必須返回 void)通常,這種方法用於執行某個操作。

  現在,我們來看看如何使用Action委託:

  (1)先看看之前我們是怎麼來使用無返回值委託的例子:

public delegate void ShowValue();

public class Name
{
   
private string instanceName; public Name(string name) { this.instanceName = name; } public void DisplayToConsole() { Console.WriteLine(this.instanceName); } public void DisplayToWindow() { MessageBox.Show(this.instanceName); } } public
class testTestDelegate { public static void Main() { Name testName = new Name("Koani"); ShowValue showMethod = testName.DisplayToWindow; showMethod(); } }
View Code

  可以清楚地看出,我們之前要先顯式聲明瞭一個名為 ShowValue 的委託,並將對 Name.DisplayToWindow 例項方法的引用分配給其委託例項。

  (2)再看看有了Action委託之後我們怎麼來達到上面的效果的例子:

public class Name
{
   private string instanceName;

   public Name(string name)
   {
      this.instanceName = name;
   }

   public void DisplayToConsole()
   {
      Console.WriteLine(this.instanceName);
   }

   public void DisplayToWindow()
   {
      MessageBox.Show(this.instanceName);
   }
}

public class testTestDelegate
{
   public static void Main()
   {
      Name testName = new Name("Koani");
      Action showMethod = testName.DisplayToWindow;
      showMethod();
   }
}
View Code

  可以清楚地看出,現在使用 Action 委託時,不必顯式定義一個封裝無引數過程的委託。

1.2 深入Action

  在實際開發中,我們經常將一個委託例項作為一個方法的引數進行傳遞,於是我們來看一下這個典型的場景,再通過Reflector反編譯工具檢視編譯器到底幫我們做了什麼好玩的事兒!

  (1)首先來看一下在List集合型別的ForEach方法的定義:

        //
        // 摘要:
        //     對 System.Collections.Generic.List<T> 的每個元素執行指定操作。
        //
        // 引數:
        //   action:
        //     要對 System.Collections.Generic.List<T> 的每個元素執行的 System.Action<T> 委託。
        //
        // 異常:
        //   System.ArgumentNullException:
        //     action 為 null。
        public void ForEach(Action<T> action);

  可以看出,ForEach方法的引數是一個Action委託例項,也就是說是一個無返回值的委託例項。

  (2)定義一個實體類,並通過Action委託使用ForEach方法:

    public class Person
    {
        public int ID { get; set; }

        public string Name { get; set; }

        public int Age { get; set; }
    }

    static void ActionDelegateDemo()
    {
         List<Person> personList = GetPersonList();

         personList.ForEach(new Action<Person>(delegate(Person p)
         {
                Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
          }));
     }
View Code

  可以看出,我們為ForEach方法傳遞了一個Action委託的例項,本質上是一個無返回值的方法指標,遍歷輸出了每個Person物件的資訊。

  

  (3)也許有些童鞋看到上面的還是有點不解,只要你瞭解過委託,那麼我們可以通過Reflector反編譯工具去看看編譯器到底做了啥事,Action委託的本質就會一如瞭然:(這裡我們可以先看看沒有Action的做法,是不是需要首先顯式聲明瞭一個無返回值的委託,然後是不是還要頂一個命名的無返回值的方法?

  ①將編譯好的程式集拖動到Reflector中,可以看到以下的情形:

  ②現在分別看看編譯器為我們自動生成的無返回值的委託定義和方法定義:

  可以看出,不管是自動生成的委託還是方法,都是不帶返回值的。

  ③有了上面的分析,我們再來看看執行的語句是怎麼被編譯的:

   可以看出,在編譯後的程式碼裡邊連new Action<Person>()都省掉了,我們也可以知道,在程式碼中可以更加簡化。但是,首先,我們得了解到底編譯器是怎麼識別Action委託的。於是,按照前兩篇的思路,在反編譯後的C#程式碼看不出什麼端倪的時候,切換到IL程式碼一探究竟:

  由IL程式碼可以看出,還是原來的方法,還是原來的味道。委託還是那個委託,執行委託還是執行那個方法。這裡,我們再來看看List型別的ForEach方法是怎麼使用Action委託的:

  現在,我們可以知道,原來所不解的東西現在終於釋懷了:在ForEach會通過一個迴圈遍歷依次呼叫委託所持有的方法,這個方法是一個符合Action委託定義的無返回值方法。至於,為什麼我們可以省略new Action<T>(),則是編譯器為我們提供的一個便利。例如,我們在使用List<Person>物件的ForEach方法時,我們可以這樣寫:

personList.ForEach(delegate(Person p)
{
      Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
});

  首先,由於我們是使用的personList這個物件(List<Person>型別),所以編譯器自動識別了泛型委託的T(即指定型別)為Person。其次,編譯器自動將無返回值的匿名方法轉換為了new Action<Person>物件。當然,如果是有返回值的匿名方法則會轉換為指定型別的new Func<T>()物件,這裡因為ForEach只接受無引數的委託例項或方法,所以如果傳入了有返回值的匿名方法則會報錯。

1.3 你究竟有幾個Action可用?


  從圖中可以看出,.NET Framework為我們提供了多達16個引數的Action委託定義,對於常見的開發場景已經完全夠用了。

二、有返回型別的內建委託—Func

2.1 初識Func

MSDN給出的定義:封裝一個具有一個引數並返回 TResult 引數指定的型別值的方法

  此委託的定義如下:

public delegate TResult Func<in T, out TResult>(T arg)

  (1)in T :此委託封裝的方法的引數型別。

  (2)out TResult :此委託封裝的方法的返回值型別。

  可以使用此委託表示一種能以引數形式傳遞的方法,而不用顯式宣告自定義委託。封裝的方法必須與此委託定義的方法簽名相對應。也就是說,封裝的方法必須具有一個通過值傳遞給它的引數,並且必須返回值。

  2.1.1 沒有Func時的使用

delegate string ConvertMethod(string inString);

public class DelegateExample
{
   public static void Main()
   {
      ConvertMethod convertMeth = UppercaseString;
      string name = "Dakota";
      Console.WriteLine(convertMeth(name));
   }

   private static string UppercaseString(string inputString)
   {
      return inputString.ToUpper();
   }
}
View Code

  2.1.2 有了Func後的使用

public class GenericFunc
{
   public static void Main()
   {
      Func<string, string> convertMethod = UppercaseString;
      string name = "Dakota";

      Console.WriteLine(convertMethod(name));
   }

   private static string UppercaseString(string inputString)
   {
      return inputString.ToUpper();
   }
}
View Code

  當然,我們還可以藉助匿名方法更加便捷地使用:

public class Anonymous
{
   public static void Main()
   {
      Func<string, string> convert = delegate(string s)
         { return s.ToUpper();}; 

      string name = "Dakota";
      Console.WriteLine(convert(name));   
   }
}
View Code

  可以清楚地看出,現在使用 Func 委託時,不必顯式定義一個新委託並將命名方法分配給該委託。

2.2 深入Func

  2.2.1 用法先行:爽一下

  我們已經知道Func委託是帶指定返回值型別的委託,那麼我們來看看在實際開發場景的一幕。還是以剛剛那個資料集合PersonList為例,在很多時候我們需要對從資料庫中讀取的資料集合進行二次篩選,這時我們可以使用List集合的Select方法,我們將一個Func委託例項作為方法引數傳遞給Select方法,就可以返回一個符合我們指定條件的新資料集合。

  (1)先來看看Select方法的定義:

        //
        // 摘要:
        //     將序列中的每個元素投影到新表中。
        //
        // 引數:
        //   source:
        //     一個值序列,要對該序列呼叫轉換函式。
        //
        //   selector:
        //     應用於每個元素的轉換函式。
        //
        // 型別引數:
        //   TSource:
        //     source 中的元素的型別。
        //
        //   TResult:
        //     selector 返回的值的型別。
        //
        // 返回結果:
        //     一個 System.Collections.Generic.IEnumerable<T>,其元素為對 source 的每個元素呼叫轉換函式的結果。
        //
        // 異常:
        //   System.ArgumentNullException:
        //     source 或 selector 為 null。
        public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);

  可以看出,Select方法中的引數採用了Func泛型委託,根據泛型委託的定義TSource和TResult分別代表要傳入的資料型別以及要返回的資料型別。

  (2)再來看看如何在程式中使用Func委託:

  首先定義一個與源資料型別不同的新資料型別作為返回值型別:

    public class LitePerson
    {
        public string Name { get; set; }
    }

  ①標準定義版:

            List<Person> personList = GetPersonList();
            
            IEnumerable<LitePerson> litePersonList = personList.Select<Person, LitePerson>(
                new Func<Person, LitePerson>
                (
                    delegate(Person p)
                    {
                        return new LitePerson() { Name = p.Name };
                    }
                )
            );    

  ②嘻哈簡化版:藉助編譯器提供的自動識別,簡化我們的程式碼

            IEnumerable<LitePerson> litePersonList = personList.Select(
                delegate(Person p)
                {
                    return new LitePerson() { Name = p.Name };
                }
            );

  ③絕逼懶人版:藉助匿名類和泛型可以大大簡化我們的程式碼

            var liteList = personList.Select(delegate(Person p)
            {
                return new { Name = p.Name, AddDate = DateTime.Now };
            });

  (3)除錯執行可以得到以下結果:

  2.2.2 原理為王:探一次

  (1)通過Reflector反編譯,我們再來看看編譯器幫我們生成的東東:

  (2)看看自動生成的委託和方法的定義:

  相信經過上節Action的詳細分析,這裡大家應該也可以舉一反三瞭解編譯器幫我們到底做了什麼事兒了,這裡我就不再贅述了,後面也不會再贅述此方面的東東(為了節省頁面大小)。

  當然,和Action類似,.NET基類庫為我們也提供了多達16個輸入引數的Func委託,但是,輸出引數卻只有1個。

三、返回bool型別的內建委託—Predicate

3.1 初識Predicate

  經過了Func的瞭解,我們可以知道接下來的這兩個Predicate和Comparison其實都屬於有返回值型別的委託,他們不過是兩個具體的特殊例項而已(一個返回bool型別,一個返回int型別)。

MSDN給出的定義:表示定義一組條件並確定指定物件是否符合這些條件的方法

  它的定義很簡單:(這裡就不再對其進行解釋了)

public delegate bool Predicate<in T>(T obj)

  此委託由 Array 和 List<T> 類的幾種方法使用,常用於在集合中搜索元素。

3.2 深入Predicate

  由於Predicate委託常用於在集合中搜索元素,那麼我們就來看看如何使用Predicate委託來進行元素的搜尋。於是,我們將目光轉到List集合的FindAll方法,相信大部分童鞋都用過這個方法。

  (1)先來看看FindAll的定義:

        //
        // 摘要:
        //     檢索與指定謂詞定義的條件匹配的所有元素。
        //
        // 引數:
        //   match:
        //     System.Predicate<T> 委託,用於定義要搜尋的元素應滿足的條件。
        //
        // 返回結果:
        //     如果找到,則為一個 System.Collections.Generic.List<T>,其中包含與指定謂詞所定義的條件相匹配的所有元素;否則為一個空
        //     System.Collections.Generic.List<T>。
        //
        // 異常:
        //   System.ArgumentNullException:
        //     match 為 null。
        public List<T> FindAll(Predicate<T> match);

  (2)再來看看FindAll的實現:

  (3)現在我們來用一下Predicate委託:還是以那個PersonList集合為例,假如我們要篩選出Age>20的Person,我們就可以使用FindAll方法。現在我們來寫一下這個委託:(後面我們會用Lambda表示式來簡寫,那才叫一個爽!)可以看出,關鍵點在於:delegate(Person p) { return p.Age > 20; }這一句上,傳入引數是Person型別的物件,返回的是一個比較結果即bool值。

            List<Person> personList = GetPersonList();

            List<Person> agedList = personList.FindAll(
                new Predicate<Person>(delegate(Person p) 
                    { 
                        return p.Age > 20; 
                    }
                )
            );
View Code

四、返回int型別的內建委託—Comparison

4.1 初識Comparison

MSDN給出的定義:表示比較同一型別的兩個物件的方法

  它的定義也很簡單:

public delegate int Comparison<in T>(T x, T y)

  T是要比較的物件的型別,而返回值是一個有符號整數,指示 x 與 y 的相對值,如下表所示:

含義

小於 0

x 小於 y。

0

x 等於 y。

大於 0

x 大於 y。

  此委託由 Array 類的 Sort<T>(T[], Comparison<T>) 方法過載和 List<T> 類的 Sort(Comparison<T>) 方法過載使用,用於對陣列或列表中的元素進行排序

4.2 深入Comparison

  由於Comparison委託常用於在集合中進行排序,那麼我們就來看看如何使用Comparison委託來進行元素的排序。於是,我們將目光轉到List集合的Sort方法,相信大部分童鞋也都用過這個方法。

  (1)老慣例,還是先看看Sort方法的定義:

        //
        // 摘要:
        //     使用指定的 System.Comparison<T> 對整個 System.Collections.Generic.List<T> 中的元素進行排序。
        //
        // 引數:
        //   comparison:
        //     比較元素時要使用的 System.Comparison<T>。
        //
        // 異常:
        //   System.ArgumentNullException:
        //     comparison 為 null。
        //
        //   System.ArgumentException:
        //     在排序過程中,comparison 的實現會導致錯誤。 例如,將某個項與其自身進行比較時,comparison 可能不返回 0。
        public void Sort(Comparison<T> comparison);

  (2)再來看看Sort方法的實現:

  可以看出,這裡雖然使用Comparison委託但最終還是轉換成了Comparer比較器,再次呼叫過載的Array.Sort靜態方法進行排序。

  (3)現在我們來用一下Comparison委託:還是以那個PersonList集合為例,假如我們要以Age為條件進行降序排列,我們應該怎麼來寫這個委託呢?

List<Person> personList = GetPersonList();

personList.Sort(delegate(Person p1, Person p2)
{
      return p2.Age - p1.Age;
});

personList.ForEach(delegate(Person p)
{
      Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
});
View Code

  實現的效果如下圖所示:

  那麼,如果是要進行升序排列呢?只需要改一下:return p2.Age-p1.Age; 更改一下被減數和減數的位置,即可完成升序和降序的切換。

personList.Sort(delegate(Person p1, Person p2)
{
      return p1.Age - p2.Age;
});
View Code

五、Lambda表示式:[ C# 3.0/.NET 3.x 新增特性 ]

  回顧,發現上面的程式碼,需要傳一個 匿名方法 ,寫起來特別彆扭。於是我們很想知道能否有簡化的語法呢?微軟告訴咱們:Of Course,必須有,它就是Lambda表示式。Lambda表示式是比匿名方法更簡潔的一種匿名方法語法。

Lambda來源:1920年到1930年期間,數學家Alonzo Church等人發明了Lambda積分。Lambda積分是用於表示函式的一套系統,它使用希臘字母Lambda(λ)來表示無名函式。近年來,函數語言程式設計語言(如Lisp)使用這個術語來表示可以直接描述函式定義的表示式,表示式不再需要有名字了。

5.1 初識Lambda表示式lambda  5.1.1 Lambda表示式要點

    ①Lambda表示式中的引數列表(引數數量、型別和位置)必須與委託相匹配

    ②表示式中的引數列表不一定需要包含型別,除非委託有ref或out關鍵字(此時必須顯示宣告);

    ③如果沒有引數,必須使用一組空的圓括號

  5.1.2 Lambda使用示例

        static void LambdaDemo()
        {
            List<Person> personList = GetPersonList();
            Console.WriteLine("--------------------標準預定義委託--------------------");
            Console.WriteLine("Standard Action Delegate Show:");
            personList.ForEach(new Action<Person>(delegate(Person p)
                {
                    Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
                })
            );

            Console.WriteLine("Simple Action Delegate Show:");
            personList.ForEach(delegate(Person p)
            {
                Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age);
            });
            Console.WriteLine("--------------------Lambda表示式--------------------");
            Console.WriteLine("Lambda Expression Show:");
            personList.ForEach(p =>
                Console.WriteLine(p.ID + "-" + p.Name + "-" + p.Age));

            Console.WriteLine("FindAll:");
            var dataList = personList.FindAll(p => p.Age > 20);
            foreach (var item in dataList)
            {
                Console.WriteLine(item.ID + "-" + item.Name + "-" + item.Age);
            }

            Console.WriteLine("Sort:");
            personList.Sort((p1, p2) => p1.Age - p2.Age);

            Console.WriteLine("Select:");
            var selectList = personList.Select(p => new LitePerson() { Name = p.Name });
            foreach(var item in selectList)
            {
                Console.WriteLine(item.Name);
            }
        }
View Code

   除錯執行的結果如下:

  5.1.3 Lambda本質探析

  (