前面的文章中,大家已經對C#3.0的新特性有了一個基本的了解,比如強大的LINQ語言和擴展方法的應用,今天給大家介紹的是C#3.0中添加的另一個重要的新特性:匿名方法。
1. 匿名溯源
匿名的歷史可謂由來已久,在C#2.0中匿名方法就已經大量使用在委托(delegate)的應用場景中。下面我舉幾個例子大家可以簡單回顧一下:
1) 當我們需要調用一個回調方法時,不需要構建委托對象,只需要將回調方法名傳入,CLR會替我們完成委托對象的創建工作。
//example 1
public static void CallBackWithoutANewDelegateObject()
{
//這裡QueueUserWorkItem方法需要一個委托作為參數,
//但我們僅僅傳遞給回調方法名,CLR可以自動為我們構造出委托對象的代碼
ThreadPool.QueueUserWorkItem(SomeAsyncTask, 5);
}
private static void SomeAsyncTask(object o)
{
Console.WriteLine(o);
}
這個例子中我們沒有構造出任何委托對象,而僅僅傳遞了回調方法名稱,CLR幫我構建了創建委托對象的代碼。
2)我們甚至不用顯式定義回調方法,只需要使用delegate關鍵字內聯的寫出方法代碼執行調用。
//example 2
public static void CallbackWithoutNewingADelegateObject()
{
//我們在這兒以內聯形式寫出回調方法體,而沒有定義任何的回調方法
ThreadPool.QueueUserWorkItem(
delegate(object o) { Console.WriteLine(o); },
5);
}
這個例子中我們並沒有定義上面的方法SomeAsyncTask,而是內聯寫出方法體。
3)我們甚至可以不寫出調用方法的參數,CLR也會為我們生成正確的委托對象。
//example 3
this.BT_LOGIN.Click +=
delegate { MessageBox.Show("Button Login has been clicked!"); };
這是匿名方法最常見的使用場景,即在添加一個控件的事件處理函數時直接使用delegate寫出方法代碼而不需要另外定義方法函數。當我們的事件處理函數很簡短時(比如上面的代碼我們僅僅彈出一個對話框顯示一條信息而已。)我們就可以使用這種方法,如果另外定義一個函數就會顯得很累贅。而且方法的參數也可以省略(比如這裡的Object sender, EventArgs e)
2. C#3.0中的匿名方法
1) 隱式類型變量 (Implicitly typed local variables)
var var1 = 1;
var var2 = 2;
var var3 = var1 + var2;
var var4 = "I'm a string.";
這裡我們可以看到我們並沒有指定變量的類型(int , string, …),但編譯器會幫我們完成這一點。熟悉腳本語言的朋友們可能會對此次語法感到驚喜,但要注意的是,C#仍然是強類型的語言,所有類型都會在編譯期確定,而不是像腳本那樣等到運行時,下面這張圖很清楚的說明了這一點:
大家可以看到在VS編輯器的智能提示中,編譯器已經找到了變量的實際類型。這個特性在結合LINQ語言進行數據查詢時顯得格外有用:
比如我們現在有一個UserInfo類,它包含了一個用戶的許多信息,姓名,年齡,住址等等。
class UserInfo
{
public string firstname;
public string lastname;
public int age;
public string address;
//...
}
我們現在要根據年齡對數據進行一些檢索,但我們希望檢索結果只需要包含用戶的姓名就夠了,也就是firstname,和lastname這兩個字段。這時候,匿名方法就可以派上用場了。
var result = from userinfo in infoList
where userinfo.age > 20 && userinfo.age < 35
select new { userinfo.firstname, userinfo.lastname };
大家可以看到,我們並沒有返回UserInfo的整個類型,而是返回了一個只包含firstname和lastname的數據類型,編譯器能夠自動為我們識別出result的類型。下面我們只需要一個foreach語句就可以把數據打印出來。
foreach (var var_info in result)
{
Console.WriteLine(var_info.firstname + " " + var_info.lastname);
}
這裡也同樣用到了匿名類型(var var_info),下面這張圖可以看出編譯器可以識別出var_info的實際型別。
有些文章在介紹C#3.0特性時會把它作為單獨的特性,但我覺得這個特性也是屬於編譯器自動探測類型的范疇,所以仍然將它歸類到匿名類型中來。
2) Lambda表達式
前面提到在C#2.0中,我們用一個delegate關鍵字匿名地調用了一個委托方法,簡化了程序員的工作,但同時我們也發覺程序的可讀性降低了不少。下面我用一個例子來說明Lambda表達式是如何增強代碼的可讀性的。
還是以上面的UserInfo查詢作為例子,下面的代碼同樣取出年齡在20-35之間的用戶群。
var result1 = infoList.FindAll(p => (p.age > 20 && p.age < 35));
這裡僅僅只用了一行代碼就完成了查詢,是不是很神奇呢!我們來分析一下這句話的語義:首先是調用了infoList的FindAll方法,這個方法的原型如下:
// Summary:
// Retrieves all the elements that match the conditions defined by the specified predicate.
//
// Parameters:
// match:
// The System.Predicate<T> delegate that defines the conditions of the elements
// to search for.
//
public List<T> FindAll(Predicate<T> match);
由於篇幅原因,我刪掉一部分注解,大家可以注意到它的參數Predicate<T>毫無疑問是一個委托類型,這就證明了我們前面所說的Lambda表達式確實是一個委托的簡化。接著在參數中第一個字母是p,指示了我們返回的數據,這裡編譯器可以通過前面infoList的類型判斷出p的類型來。下面是指示符”=>”,表示返回的數據集合要符合後面的查詢條件。
這裡再舉一個復雜一點的例子加以說明:
var groupBooksNumAndClicks =
from book in books.Tbl_books
from type in books.Tbl_types
where book.Type_id == type.Id
group book by type.Name into g
select new { type = g.Key, booksSum = g.Count(), clicksSum = g.Sum(p => p.Clicks) };
如果大家還有印象的話,這是我以前在介紹LINQ語言時寫的一個數據庫查詢的例子,Tbl_books這張表包含了書籍的所有信息(書名,價格,等等),Tbl_types這張表包含了所有的書籍目錄(比如數學類,計算機類等等), 我們要取出相同類別的書籍並計算每一類書籍總的點擊量,有興趣的朋友可以自行研究一下。
3. 匿名方法機理
上面介紹了我們應當怎樣使用匿名類型,下面我們通過閱讀一些IL代碼來看看編譯器究竟為我們做了哪些工作。下面的代碼演示了一個簡單的匿名類型和匿名類型變量的調用,我們來看看編譯器是怎麼處理的。
namespace AnonymousTest
{
class UserInfo
{
public string firstname;
public string lastname;
public int age;
public string address;
//...
}
class Program
{
static void Main(string[] args)
{
List<UserInfo> infoList = new List<UserInfo>();
var result = from userinfo in infoList
where userinfo.age > 20 && userinfo.age < 35
select new { userinfo.firstname, userinfo.lastname };
foreach (var var_info in result)
{
Console.WriteLine(var_info.firstname + " " + var_info.lastname);
}
}
}
}
很簡單的一段代碼,首先我們定義了一個UserInfo類,其中定義了一些屬性,firstname,lastname等等,這裡為了簡單,我們直接使用公有變量,而不使用屬性來表示了。接下來就是我們的Program類,在這個類中我們定義了集合變量,由於這裡僅僅為了做演示,所以實際上我並沒與往集合中添加成員,但已足夠讓CLR生成它的結構然後我們定義了result變量去取得它的檢索結果,注意我們返回的是一個匿名類型。接著我們使用freach循環將其打印出來。
下面我們打開VS的命令行窗口,輸入命令ildasm,在打開的窗口中,選擇File->Open打開我們編譯好的文件,大家會看到如下界面:
大家可以看到,CLR在IL層實際上是為我們新建了一個特殊的類,其中包含了firstname, secondname的屬性,當然與之對應的還定義了一些set,get方法,此外,CLR還為這個類額外重載了以下方法,像Equals,ToString等等。這樣我們定義的一些匿名類型就能夠被編譯器所識別出來。
以上只是對匿名類型做了一個簡單的介紹,有興趣的朋友們可以從兩方面繼續研究它,一方面是工程上的應用,一方面繼續探索IL代碼的密碼,無論是哪一個方面,我想你都會有很大收獲的。