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


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

/* 新語法索引 */

7.系統內置委托 Func / Action
8.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本質探析

  (1)以上述案例中的Sort方法為例:personList.Sort((p1, p2) => p1.Age - p2.Age);

  (2)通過反編譯工具,可以看到其實是聲明了一個Comparison委托實例:

  (3)現在,我們來分析一下具體的步湊:有了前面的基礎,現在再來看就輕松了許多,So Easy!

    ①編譯器自動生成了一個Comparison委托:

    ②編譯器幫我們創建了一個符合Comparison委托簽名的靜態方法:

    ③實例化Comparison委托變量,並將方法指針傳入該委托;

    ④調用List<T>實例的Sort方法,並傳入Comparison委托實例;

    其中,前面兩步①和②可以通過反編譯后的C#代碼獲知,而后面兩步③和④則需要通過IL代碼來分析,前面已經介紹過相關,這里就不再贅述。

5.2 回顧Lambda進化史

  前面了解了Lambda是什么,這里我們來回顧一下Lambda的演化過程。

  從演化過程可以知道,編譯器在越來越智能地幫我們做着更多的事兒,而我們卻在享受着編譯器帶來的便利沉浸在高效的開發效率中,變得越來越“懶”了。

5.3 語句Lambda

  Lambda表達式有兩種類型:一是Lambda表達式,二是語句Lambda。

  那么,語句Lambda和表達式Lambda到底有何區別?

ANSWER:語句Lambda 和 表達式Lambda 的區別在於,前者在 =>右邊有一個語句塊(大括號),而后者只有一個表達式(沒有return 和大括號)。
EXAMPLES:
(1)表達式Lambda:
list.FindAll(d => d.Id > 2);// goes to
list.ForEach(d => Response.Write(d.ToString() + "<br/>"));

(2)語句Lambda:

list.ForEach(d => { if (d.Id > 2) { Response.Write(d.ToString() + "<br/>"); } });

  可以看出,語句Lambda的右側有一個語句塊,在這個大括號內的語句可能會有多條。

參考文章

  (1)金旭亮,《C#面向對象程序設計》,教案6-委托與事件講義:http://download.csdn.net/detail/bitfan/3324733

  (2)MSDN,泛型委托(C#編程指南):http://msdn.microsoft.com/zh-cn/library/sx2bwtw7.aspx

  (3)min,《泛型委托在項目中的應用》:http://www.cnblogs.com/ASPNET2008/archive/2010/04/05/1704405.html

  (4)MSDN,Lambda表達式(C#編程指南):http://msdn.microsoft.com/zh-cn/library/bb397687.aspx

  (5)張龍豪,《Lambda表達式詳解》:http://www.cnblogs.com/knowledgesea/p/3163725.html

附件下載

  NewGrammerDemos v1.2:http://pan.baidu.com/s/1gdxi39D

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM