程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C# 3.0語言詳解之基本的語言增強

C# 3.0語言詳解之基本的語言增強

編輯:關於C#

Linq項目簡介和開發環境的搭建

9月份,微軟推出了一項名為 “Linq項目”的新技術,用於在.NET語言中集成數據查詢的功能。您 可以從http://msdn.microsoft.com/netframework/future/得到Linq項目的技術 預覽版,其中包括大量的介紹文章(英文)和C# 3.0以及VB 9.0的編譯器。

Linq項目英文全稱為Language Integrated Query,即“語言集成 查詢”。Linq基於.NET Framework 2.0,通過對語言的改進,實現在直接 在語言中通過類似SQL語句的構造對數據源進行查詢,可查詢的數據源從基本的 順序結構(如數組和鏈表)延伸到關系型數據庫(目前是SQL Server,相信將來 可以擴展到幾乎所有的關系型數據庫)和XML。

C# 3.0是C#語言再一次升 級後的版本,率先實現了Linq的概念;同樣實現了Linq的還有VB 9.0。從上面提 到的網址,讀者可以找到兩種語言的編譯器的技術預覽版。在本文中我們將以C# 3.0為中心討論其開發環境的搭建和語言改進。下載得到的是一個名為 “linq preview.msi”(也可能不同)的安裝包,雙擊之後可以和其 他軟件一樣進行安裝,此處不再贅述。該安裝包為Visual Studio 2005 Beta 2 及以後的版本和Visual C# 2005 Beta 2提供了插件(項目模板)和C#編譯器, 該編譯器生成的IL代碼可以直接在.NET Framework 2.0上運行。在安裝了該預覽 版後,在新建項目對話框中項目類型列表的Visual C#節點下,我們可以看到一 個Linq Preview的條目,並可以在右邊的項目模板中選擇一些桌面項目模板 (Linq暫時還不支持Web項目),如下圖所示:

圖1 - 安裝完Linq的Visual Studio 2005 新建項目對話框

只要選中了Linq中 的項目模板,我們就可以像編寫其他應用程序一樣在Visual Studio 2005中開始 編寫C# 3.0應用程序了,在編譯的時候IDE會自動為我們選擇C# 3.0的編譯器。

現在我們已經可以開始編寫C# 3.0應用程序了,後面的章節中我就將為 大家講解C# 3.0所帶來的語言增強。值得說明的一點是,本文是一個系列文章中 的一篇,這一系列文章共三個部分。本文為第一部分,講述C# 3.0種基本的語言 增強,這些語言增強其實是其他兩個部分的基礎;第二部分將講述C# 3.0中的 Lambda表達式,這是匿名方法的自然進化形式,它不僅能將表達式體現為可執行 的方法(委托),也能將表達式體現為可以在運行時進行操作的數據結構 ——表達式樹;最後一部分講述的才是Linq項目中最核心的也是最激 動人心的內容——查詢表達式,這是Linq在C#中的實現形式。同時, Linq還針對SQL查詢和XML查詢對.NET Framework基礎類庫進行了擴充,分別稱作 DLinq和XLinq,這些內容我將在其他系列文章中為大家講述。

在本文的 講述過程中,限於篇幅,僅提供了簡短的代碼段而不是完整的代碼。不過這些代 碼段是從完整的、可正確編譯和運行的代碼中抽取出來的,這些完整代碼可以從 這裡下載,並且在本文的第5部分中有關於完整代碼的介紹。

好了,廢話 太多了,讓我們趕快進入C# 3.0的精彩世界。

具有隱式類型的聲明

在一個帶有初始化器的聲明語句中,待聲明的變量的類型是顯而易見的 ——和初始化表達式的結果類型一致。對於這種情況,在C# 3.0中可 以使用新增的關鍵字var來代替聲明中的類型,而由編譯器根據初始化表達式來 推斷出變量的類型。

var i = 5; // int
var d = 9.0; // double
var s = "Hello"; // string
var a = new int [] { 1, 2, 3, 4, 5 }; // int[]
Console.WriteLine("Type of variable <i>: {0}", i.GetType());
Console.WriteLine ("Type of variable <d>: {0}", d.GetType());
Console.WriteLine("Type of variable <s>: {0}", s.GetType());
Console.WriteLine("Type of variable <a>: {0}", a.GetType());

上面的代碼在C# 3.0中是符合語法規 則的,前四行代碼就是使用了隱式類型的聲明;而後四行代碼用於在運行時驗證 各個變量是否擁有正確的類型。如果在Visual Studio 2005中運行這段代碼(為 了能夠看到結果,請使用Ctrl+F5來編譯並啟動程序),會得到下面的結果:

Type of variable <i>: System.Int32
Type of variable <d>: System.Double
Type of variable <s>: System.String
Type of variable <a>: System.Int32[]

這表明,編譯器在編譯的時候,已經正確地推斷了每個變量的類 型,並將其嵌入到了程序集的元數據中。

這裡有兩個限制,一是具有隱 式類型的聲明只能作用在局部變量上,二是這種聲明必須有初始化器(即等號和 後面的表達式)。如果我們企圖在一個類中聲明一個具有隱式類型的域,就會出 現一個編譯錯誤:Invalid token ’var’ in class, struct, or interface member declaration;而如果聲明中沒有出現初始化器,則會導致另 外一個編譯錯誤:’=’ expected。

除了局部變量外,作用 域為一個塊的數組也可以運用具有隱式類型的聲明,例如:

var ia = new [] { 1, 2, 3, 4, 5 }; // int[]
var da = new [] { 1.1, 2, 3, 4, 5 }; // double[]
var sa = new [] { "Hello", "World" }; // string[]
Console.WriteLine("Type of array <ia>: {0}", ia.GetType());
Console.WriteLine ("Type of array <da>: {0}", da.GetType());
Console.WriteLine("Type of array <sa>: {0}", sa.GetType());

在上面例子的數組聲明中,在運算符new和表示 數組聲明的一對方括號之間省略了類型名字,但這在C# 3.0中仍然是符合語法規 則的。編譯器會通過成員列表中的值的類型來推斷數組的類型。編譯並運行上面 的例子,會得到如下的輸出:

Type of array <ia>: System.Int32[]
Type of array <da>: System.Double[]
Type of array <sa>: System.String[]

除了和具有隱 式類型的局部變量具有相同的約束外,具有隱式類型的數組還有必須尊從這樣一 個規則,即成員列表中的所有值必須是兼容的。也就是說,成員列表中必須存在 這樣一個值,使得其他值可以隱式地轉換為該值的類型。因此,下面的聲明是不 符合語法規則的:

var aa = new [] { 1, "Hello", 2.0, "World" };

如果試圖編譯上面的代碼,會得到 一個編譯錯誤:No array type can be inferred from the initializers。這 是因為編譯器無法根據成員列表中的值來推斷數組的類型。

實際上,盡 管具有隱式類型的聲明使得傳統聲明的編寫方法更加便利,然而引入這種聲明方 式的真正目的並不在於此,而是為了使局部變量和數組能夠訪問這樣一個新的語 言構造:匿名類型。

對象和集合初始化器

1、對象初始化器

在C# 3.0出現之前,如果想在構造一個對象的時候對其成員進行初始化 ,只有通過調用帶參數的構造器來完成。如果一個類型僅提供了無參構造器(默 認構造器)或構造器接受的參數和對象的公共屬性沒有太大關系,對對象的初始 化就只能通過兩個步驟來完成:首先是聲明一個對象,然後對其公共屬性進行賦 值。

C# 3.0引入了對象初始化器的語言構造,可以在聲明對象的時候對 其公共可讀寫屬性進行賦值。例如我們有這樣一個類型:

class Book
{
private string m_name;
private string m_isbn;
private float m_price;
public string Name { get { return m_name; } set { m_name = value; } }
public string Isbn { get { return m_isbn; } set { m_isbn = value; } }
public float Price { get { return m_price; } set { m_price = value; } }
}

那麼,我們就可以用下面的簡單方式來初始化Book類的對象:

var firstBook = new Book
{
Name = "The First Sample Book",
Isbn = "1-111-11111-1",
Price = 88.0f
};
var secondBook = new Book
{
Name = "The Second Sample Book",
Price = 25.0f,
Isbn = "2-222-22222-2"
};

也就是說,只要 在對象聲明語句的末尾添加一對花括號,並在其中對每個屬性進行賦值即可。並 且我們可以看到,第二個聲明中對屬性的賦值語句的順序與Book類中屬性定義的 順序並不相同,但這完全是符合語法規則的,只要明確指定了要賦值的屬性名字 即可。另外,也可以只對一部的屬性進行賦值,如只對Isbn進行賦值,而在稍後 通過查詢等手段為Name和Price屬性賦值。甚至可以保留一個空的屬性賦值列表 ,這時每個屬性都被賦以相應類型的默認值。

實際上,對象初始化器完 成的工作還是我們以前要完成的那兩步,只不過由編譯器代勞了,例如上面對 firstBook的聲明將被編譯器翻譯為形如下面的代碼,之後才進行進一步的編譯 :

var firstBook = new Book;
firstBook.Name = "The First Sample Book";
firstBook.Isbn = "1-111- 11111-1";
firstBook.Price = 88.0f;

2、集合初始 化器

我們知道,在C# 1.x和C# 2.0中,可以通過一個包含在花括號之間 的值列表來初始化一個剛剛聲明的數組,如:

int[] intArray = new int[] { 1, 2, 3};

然而,對於和數組有著類似功能的其他 集合(如List)就不能享受這種方便的語法了,只能在聲明完集合對象後將值一 個一個地添加到集合中。而C# 3.0提出了集合初始化器的語言構造,可以用初始 化數組的便捷語法來初始化任何一個實現了ICollection<T>接口的對象。 這裡以C# 2.0中引入的著名的泛型類型List<T>為例:

var intList = new List<int> { 1, 2, 3, 4, 5 };
Console.WriteLine("A list of int:");
foreach(var i in intList)
Console.WriteLine("{0} ", i);

運 行上面這段代碼,可以得到下面的結果:

A list of int:
1
2
3
4
5

前面提到了,這種語法可以 用來初始化任何實現了ICollection<T>接口的對象,這實際上是編譯器的 功勞。編譯器針對這種語法自動生成了循環調用ICollection.Add方法的代碼, 將列表中的值一個一個地添加到正在聲明的集合中。例如:

var intList = new List<int>();
intList.Add(1);
intList.Add(2);
intList.Add(3);
intList.Add(4);
intList.Add(5);

另外,也必須是實現了 ICollection<T>的對象才能用這種語法進行初始化,其他具備Add方法但 未實現ICollection<T>的對象不能這樣初始化;同時在聲明中還必須明確 指定類型參數T所代表的實際類型。

匿名類型

在很多情況下,我 們需要一種能夠臨時將一批具有一定關聯的數據存放起來的對象;或者在某些情 況下,我們對僅一個對象的“形狀”(如屬性的名字和類型等)比較 感興趣。例如前面我們提到的Book,當它和其他商品放在一起進行查詢時,我們 可能僅對其名稱和價格感興趣,並且希望將這兩種屬性放在另外一個單獨的臨時 對象中以備今後使用。這時,我們關注的僅僅是這個臨時對象具有Name和Price 的屬性感興趣,至於它究竟是什麼類型就無關緊要了。然而,為了使這樣一個對 象得以存在,我們不得不為這個無關緊要的類型寫上一大堆“樣本代碼 ”,無非就是定義一個如BookAsGood的類,其中無非也就是形如m_name和 m_price的私有域和名為Name與Price的公共可讀寫方法。

而在C# 3.0中 ,我們無須為這些無關緊要的類型浪費時間。通過使用“匿名類型” ,只要在需要一個這樣的對象時使用沒有類型名字的new表達式,並用前面提到 的對象初始化器進行初始化即可。如:

var b1 = new { Name = "The First Sample Book", Price = 88.0f };
var b2 = new { Price = 25.0f, Name = "The Second Sample Book" };
var b3 = new { Name = "The Third Sample Book", Price = 35.00f };
Console.WriteLine(b1.GetType());
Console.WriteLine (b2.GetType());
Console.WriteLine(b3.GetType());

首 先,前面三行聲明並初始化了三個具有匿名類型的對象,它們都將具有公共可讀 寫屬性Name和Price。我們可以看到,匿名類型的屬性連類型都省掉了,完全是 由編譯器根據相應屬性的初始化表達式推斷出來的。這三行稱作“匿名類 型對象初始化器”,編譯器在遇到這樣的語句時,首先會創建一個具有內 部名稱的類型(所謂的“匿名”只是源代碼層面上的匿名,在最終編 譯得到的元數據中還是會有這樣一個名字的),這個類型擁有兩個可讀寫屬性, 同時有兩個私有域用來存放屬性值;然後,和對待對象初始化器一樣,編譯器產 生對象聲明代碼,並依次為每個屬性賦值。

上面代碼的最後三行用來檢 驗匿名類型在運行時的類型,如果嘗試編譯並運行上述代碼,會得到類似下面的 輸出:

lover_P.CSharp3Samples.Ex03.Program+<Projection>f__ 0
lover_P.CSharp3Samples.Ex03.Program+<Projection>f__1
lover_P.CSharp3Samples.Ex03.Program+<Projection>f__0

這表明編譯器的確為匿名類型對象創建了實際的類型,並且該類型在代碼中 是不可訪問的,因為類型的名字不符合C#語言命名規則(其中出現了+、<、 >等非法字符)。

另外,我們還發現一個有趣的現象,由於b1和b2在 初始化的時候其屬性的順序和推斷出來的類型完全一致,它們的運行時類型也是 一樣的;而b2因為屬性出現的順序不同於另外兩個對象,因此具有不同的運行時 類型。通過下面的代碼,我們可以驗證這一事實:

// 正確的賦值 ,b1和b3具有相同的類型
b1 = b3;
// 錯誤的賦值,b1和b2的類型 不同
b1 = b2;

如果嘗試編譯這段代碼,對於第二個賦值我 們會得到一條編譯錯誤:Cannot implicitly convert type ’lover_P.CSharp3Samples.Ex03.Program.<Projection>f__1&rsqu o; to ’lover_P.CSharp3Samples.Ex03.Program.<Projection>f__0&rsqu o;。

這實際上是C# 3.0編譯器固有的特性,在同一個程序集中,編譯器 將為屬性出現順序和類型完全相同的匿名類型對象生成唯一的一個類型。而一旦 屬性的出現順序或類型有所不同,編譯器就會生成不同的類型。另外,在兩個程 序集之中,即使屬性出現的順序和類型一致,編譯器也可能會生成不同的類型, 因此具有匿名類型的對象是不能跨程序集訪問的。

擴展方法

擴展 方法是一種特殊的靜態方法,它定義在一個靜態類中,但可以在其他類的對象上 像調用實例方法那樣進行調用。因此,通過擴展方法,我們就可以在不修改一個 類型的前提下對一個類型進行功能上的擴充;同時,也可以將一些近似的類型中 近似的功能同一實現在一個類中,便於閱讀和維護。

另外,擴展方法的 引入並非只是簡單地為了擴展現有類型,擴展方法的使用還是有一定限制的(這 將在稍後談到)。擴展方法更大的意義在於它為以後將要介紹的查詢表達式、查 詢表達式模式和標准查詢運算符的實現奠定了基礎,而這些實現正是Linq項目的 核心所在。

1、擴展方法的定義和調用

擴展方法和一般靜態方法 的定義方法類似,唯一的區別是在第一個參數的前面要加上關鍵字this作為修飾 符;同時,第一個參數的類型也決定了擴展方法可以擴展的類型。

為了 介紹擴展方法的定義和使用方法,首先我們定義下面這樣一個簡單的類作為被擴 展對象:

class SampleClass
{
int m_val = 10;
public int Val { get { return m_val; } set { m_val = value; } }
public void Func()
{
Console.WriteLine("Hey! I’m myself, and my value is {0}.", m_val);
}
}

這個類擁有一個公共可讀寫屬性Val,並有一個私有域m_val用於 存放這個屬性的值。另外,這個類自身還擁有一個公共方法Func,用來在屏幕上 顯示以行信息,說明該方法被調用了。

然後,我們定義一個靜態類型 SampleExtensions(這個名字是隨意的,只有將擴展方法作為普通的靜態方法進 行調用時才會用到這個名字),其中定義一個用於擴充SampleClass類型的擴展 方法ExFunc:

static class SampleExtensions
{
public static void ExFunc(this SampleClass s)
{
Console.WriteLine("Aha! I’m going to modify the SampleClass!");
s.Val = 20;
s.Func();
}
}

注意這個方法的第一個參數(也是僅有的一個參數)的類型前面 多了一個修飾符this,這表明該方法用來擴展SampleClass類型,也就是說可以 在SampleClass類型的對象上像調用實例方法那樣調用ExFunc方法。該方法首先 告訴用戶它正在被調用,然後修改SampleClass類型的對象的屬性,並調用它的 實例方法。

接下來,我們在Main方法中創建SampleClass類型的一個實例 ,並嘗試調用其實例方法和上面定義的擴展方法:

SampleClass s = new SampleClass();
Console.WriteLine("Calling the instance method:");
s.Func();
Console.WriteLine();
Console.WriteLine("Calling the extension method:");
s.ExFunc();

我們可以看到,對ExFunc的調用形式和對Func方 法完全一樣,然而從上面的類型定義可以明確地知道,Func是定義在 SampleClass類型中的實例方法而ExFunc則是定義在SampleExtensions類型中的 擴展(靜態)方法。嘗試編譯和運行上面的代碼,可以得到下面的結果:

Calling the instance method:
Hey! I’m myself, and my value is 10.
Calling the extension method:
Aha! I’m going to modify the SampleClass!
Hey! I’m myself, and my value is 20.

當然,由於擴展方法只是靜態方法的一種 特例,我們同樣可以像用調用一般靜態方法那樣來調用擴展方法:

SampleExtensions.ExFunc(s);

這會得到完全一樣 的結果。而且事實上,編譯器也正是將擴展方法的調用翻譯為了一般形式的靜態 方法調用,然後才進行進一步的編譯。

擴展方法不僅能擴展同一個程序 集中的類型,同時也能擴展不同程序集甚至是已經發布了的程序集中的類型。下 面我們就在SampleExtensions中再添加一個擴展方法,用來擴展.NET Framework 的內建類型String(這個例子摘錄自C# 3.0語言規范,版權歸微軟公司所有。) :

public static int ToInt32(this string s)
{
return Int32.Parse(s);
}

然後,我們就可以象下面這 樣方便地將一個字符串轉換為一個整型了:

string sval = "20";
Console.WriteLine("String ’20’ means integer: {0}.", sval.ToInt32());

嘗試運行這段 代碼,會得到如下結果:

String ’20’ means integer: 20.

簡單地浏覽一下.NET Framework的文檔就會發現, System.String類型中的確沒有定義ToInt32方法,這說明我們的擴展方法在.NET Framework內建類型上仍然有效。

2、擴展方法的導入和權限

前面 我們探討了如何在同一個程序集中定義和調用擴展方法,那麼如果一個擴展方法 是定義在其他程序集中,我們又如何享用這些擴展方法所帶來的功能呢?事實非 常簡單,C# 3.0語言規范中規定,當我們使用using語句導入一個名命名空間時 ,就會同時導入該命名空間中所有靜態類型中定義的所有匿名方法。

3、 重載抉擇問題

看了上面的介紹我們不難發現一個問題:如果一個類型中 的某個實例方法與擴展方法的簽名等價(這裡說“等價”是因為擴展 方法與調用形式一樣的實例方法相比,要多一個表示被擴展類型的參數,也就是 第一個有this修飾符的參數),那麼當在被擴展類型的對象上調用方法時,就會 產生沖突。我們將這種沖突稱為重載抉擇問題。C# 3.0語言規范擴展了重載抉擇 ,將對擴展方法的調用也納入到重載抉擇的范疇之內,並且規定擴展方法擁有最 低的優先級。也就是說,對於一組特性類型、特定順序的參數列表,只有當被擴 展類型中沒有得以匹配的方法時,才考慮從擴展方法中選擇一個最合適的方法進 行調用。

現在,我們為上面的SampleExtensions類再添加一個用於擴展 SampleClass類型的擴展方法Func:

public static void Func (this SampleClass s)
{
s.Val = -1;
Console.WriteLine ("Am I appearing?");
}

如果用調用實例方法 的語法調用這個擴展方法,則其調用形式與調用無參的實例方法Func完全一致。 再次編譯並運行原來的程序,輸出的結果並沒有改變,也就是說這個擴展方法根 本沒有被調用,實際被調用的方法是實例方法Func。當然,如果將這個擴展方法 作為普通的靜態方法進行調用是沒有問題的。

另外如果兩個靜態類中為 同一個類型定義了簽名一致的靜態方法,則最後定義的靜態方法具有較高的優先 級;而同一程序集中定義的靜態方法優先級高於用using語句從其他命名空間中 導入的擴展方法;最後,如果兩個命名空間中包含簽名一致的擴展方法,則最後 引入的命名空間中的擴展方法優先級較高。

示例代碼簡介和小結

本文的代碼包括一個解決方案”CSharp3Sample1“,其中包括4個項 目Ex01~Ex04,分別對應於第1~4小節中的示例。您可以文章開始處的連接下載源 代碼。如果需要運行某一個示例,請在Visual Studio 2005的Solution Explorer窗口中右擊對應的項目,並選擇”Set as Startup Project“菜單項;然後按Ctrl+F5鍵運行示例,這裡建議按Ctrl+F5而不是 只按F5鍵來運行示例,是因為這樣能夠在運行結束後暫停,方便觀察結果(由於 個人原因我不願意在代碼中加入類似Console.ReadLine這樣的代碼來暫停程序的 運行)。 另外請注意要運行這些代碼需要首先正確安裝Visual Studio 2005 Beta2和Linq Preview。

本文通過一系列可以執行並能夠看到結果的簡單 代碼介紹了C# 3.0中基本的語言增強——具有隱式類型的對象和數組 聲明、對象和集合初始化器、匿名類型和擴展方法。與C# 2.0之於C# 1.x不同, C# 3.0的這些語言增強不僅僅是為了是語言變得更加強大和優雅,更重要的是為 了後面的Lambda表達式和查詢表達式奠定了實現基礎。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved