在上一篇的LINQ介紹中,我們已經看到了隱式類型變數var,擴充方法(extension method)和lambda運算式的身影。沒錯,他們正是LINQ技術的基石,是他們讓LINQ的實現成為可能,並且簡化了LINQ運算式的書寫。在這一篇中,我將和大家一一探討C#3.0在語言功能上所作的努力,包括:隱式類型局部變數、自動屬性和匿名型別。
隱式類型局部變數
C#是強型別語言,意味著我們在聲明變數時必須指定變數的具體類型,比如:
static void DeclareExplicitVars()
{
int myInt = 0;
bool myBool = true;
string myString = "Hello, World!";
}
現在C# 3.0為我們提供了一個新的關鍵字var,你可以使用它代替正式的資料類型名(如int, bool, string)。在使用var關鍵字時,編譯器會根據用於初始化局部變數的初始值推斷出變數的資料類型。例如,上面的變數聲明可以改為如下代碼:
static void DeclareImplicitVars()
{
// 隱式類型局部變數的聲明方式: var varName = defaultValue;
var myInt = 0;
var myBool = true;
var myString = "Hello, World!";
}
上面兩種方式是等價的,編譯器可以根據初始值推斷myInt的類型為System.Int32,myBool的類型為System.Boolean,myString的類型為System.String。
除此之外,我們可以對基底類別庫中的所有類型使用隱式類型,包括數組、泛型、自訂類型。
public static void DeclareImplicitVars()
{
// declare implicit variables
var numbers = new int[] { 2, 4, 6, 8 };
var persons = new List<Person>();
var car = new SportsCar();
// verify the data type using reflection
Console.WriteLine("numbers is a: {0}", numbers.GetType().Name);
Console.WriteLine("persons is a: {0}", persons.GetType().Name);
Console.WriteLine("car is a: {0}", car.GetType().Name);
}
輸出結果如下:
var
在foreach
語句中的使用
在foreach迴圈語句中,我們也可以使用隱式類型。正如你希望的那樣,編譯器會推斷出正確的資料類型:
static void VarInForeachLoop()
{
var numbers = new int[] { 2, 4, 6, 8 };
foreach (var item in numbers)
{
Console.WriteLine("Item value: {0}", item);
}
}
隱式類型變數的限制
需要注意的是,使用var關鍵字時會存在多種限制。首先,隱式類型只能應用與方法或者屬性內局部變數的聲明,不能使用var來定義傳回值、參數的類型或類型的資料成員。
其次,使用var進行聲明的局部變數必須賦初始值,並且不能以null作為初始值。其原因在於編譯器必須能夠根據初始值推斷出該變數的實際類型。
隱式類型資料是強型別資料
隱式類型局部變數最終會產生強型別資料。因此,var關鍵字與指令碼語言(如VBScript或Perl)的Variant資料類型是不一樣的,對後者來說,一個變數可以在其生命週期中儲存不同類型的值。
其實,類型推斷保持了C#語言的強型別特性,並且在編譯時間隻影響變數聲明。初始化之後,編譯器就已經為隱式類型變數推斷出了確切的資料類型。如果把不同類型的值賦給變數會導致編譯時間錯誤:
static void ImplicitTypingStrongTyping()
{
// 編譯器知道 s 是System.String類型
var s = "This variable can only hold string data!";
s = "It's OK.";
// 可以調用任何基礎方法
string upper = s.ToUpper();
// 錯誤!不能把數實值型別資料賦給String類型變數
s = 100;
}
隱式類型局部變數的作用
看了上面的介紹,你肯定會奇怪這個結構有什麼用呢。如果只是為了簡單,就不值得了,因為這樣做可能會使其他閱讀代碼的人感到疑惑。但當我們使用LINQ時,var關鍵字的優勢就顯現出來了。它可以動態根據查詢本身的格式來建立結果集,這樣我們就不需要顯示定義查詢可能返回的類型,而且在很多時候我們並不能一眼就看出LINQ的傳回型別。如下例:
public static void QueryOverInts()
{
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 5 };
var subset = from i in numbers where i < 10 select i;
Console.Write("values in subset: ");
foreach (var i in subset)
Console.Write("{0} ", i);
Console.WriteLine();
Console.WriteLine("subset is a: {0}", subset.GetType().Name);
Console.WriteLine("subset is defined in: {0}", subset.GetType().Namespace);
}
輸出:
其實,我們可以認為只有在定義從LINQ查詢返回的資料時才使用var關鍵字。
自動屬性
我們知道.NET語言推薦使用類型屬性來封裝私人資料欄位,而不是 使用GetXXX()和SetXXX()方法。因為.NET基底類別庫總是使用類型屬性而不是傳統的訪問和修改方法,因此使用屬性可以獲得與.NET平台更好的整合性。需要知道的是,在底層,C#屬性會被映射到首碼get_和set_的方法中,即如果定義了Name屬性,C#會自動產生get_Name()和set_Name()方法。
考慮如下的C#類型定義:
class Person
{
private string firstName = string.Empty;
public string FirstName
{
get { return firstName; }
set { firstName = value; }
}
private string lastName = string.Empty;
public string LastName
{
get { return lastName; }
set { lastName = value; }
}
private int level = 0;
public int Level
{
get { return level; }
set { level = value; }
}
}
雖然定義屬性不難,但如果屬性只是賦值和傳回值,對次定義欄位和屬性也很麻煩,特別是在類屬性很多的情況下。為了簡化這種簡單的資料欄位封裝的過程,C# 3.0提供了自動屬性文法。現在,上面的Person可以定義成如下形式:
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Level { get; set; }
}
定義自動屬性時,我們只需要指定存取修飾詞、資料類型、屬性名稱和空的get/set範圍。在編譯時間,會使用自動產生的私人支援欄位以及get/set邏輯的正確實現。
需要注意的是,定義自動屬性時,必須同時提供get和set關鍵字,因此不能定義唯讀或者唯寫的自動屬性。
匿名型別
作為一個物件導向的程式員,我們知道如何定義類來表達一個給定的編程實體。當我需要一個在項目之間重用的類型時,我們通常建立一個C#類,為該類提供必需的一系列屬性、方法和事件等。但有時候,我們可能需要定義類來封裝一些相關資料,而不需要任何相關聯的方法、事件。並且,給類不需要在項目間重用。儘管如此,我們還是得定義一個“臨時”類,雖然工作不是很複雜,但是如果需要定義類來封裝很多資料成員的話,那麼將消耗你大量的勞動時間。我想,大家都不會希望把編程變成一項機械運動吧。
C# 3.0提供的匿名型別正是為了上述任務而生,匿名型別是匿名方法的自然延伸,可以協助我們輕鬆的完成上面的工作。
定義一個匿名型別時,使用新的關鍵字var和之前介紹的對象初始化文法,如下樣本:
static void TestAnonymousType()
{
// 構造一個匿名對象表示一個僱員
var worker = new { FirstName = "Vincent", LastName = "Ke", Level = 2 };
// 顯示並輸出
Console.WriteLine("Name: {0}, Level: {1}", worker.FirstName + "" + worker.LastName, worker.Level);
}
使用上述代碼來構建匿名對象時,C#編譯器會在編譯時間自動產生名稱唯一的類。因為這個類的名字在C#中是不可見的,所以必需使用var關鍵字來使用隱式類型化。另外,我們需要通過對象初始化文法來定義一系列屬性來封裝各個資料。
匿名型別的內部表示
所有的匿名型別都自動繼承自System.Object,我們可以在隱式類型話的worker上面調用ToString()、GetHashCode()、Equals()、GetType()等方法。
我們可以定義如下方法來查看匿名型別的資訊:
static void ReflectAnonymousType(object obj)
{
Console.WriteLine("Type Name: {0}", obj.GetType().Name);
Console.WriteLine("Base Class: {0}", obj.GetType().BaseType);
Console.WriteLine("obj.ToString() = {0}", obj.ToString());
Console.WriteLine("obj.GetHashCode() = {0}", obj.GetHashCode());
}
static void TestAnonymousType()
{
// 構造一個匿名對象表示一個僱員
var worker = new { FirstName = "Vincent", LastName = "Ke", Level = 2 };
ReflectAnonymousType(worker);
}
結果如下:
上例中,worker的類型是<>f__AnonymousType0`3(各版本之中可能會有所不同),匿名型別的類型名完全由編譯器決定。更重要的是,使用對象初始化文法定義的每一個成對的名稱和數值被映射為同名的唯讀屬性以及被封裝的私人資料成員。
方法ToString()
和GetHashCode()
的實現
從上面可以看到,匿名型別直接了System.Object,並且重寫了Equals()、GetHashCode()、ToString()方法。其中ToString()根據每一個成對的名稱和數值,產生並返回一個字串,見。
GetHashCode()的實現使用每一個匿名型別的成員變數來計算散列值。若且唯若兩個匿名型別有相同的屬性別且被賦予相同的值時,就會產生相同的散列值,這樣,匿名型別就可以很好的和Hashtable容器一起工作。
匿名型別的相等語義
編譯器重寫的Equals()在判斷對象時使用了基於值的語義,但編譯器並沒有重載(==和!=)相等運算子,因此使用==比較兩個匿名對象時,是基於引用的語義!”==”比較引用是對所有類的預設行為。如下例:
static void AnonymousTypeEqualityTest()
{
// 構建兩個匿名型別,擁有相同的成對的名稱和數值
var worker1 = new { FirstName = "Harry", SecondName = "Folwer", Level = 2 };
var worker2 = new { FirstName = "Harry", SecondName = "Folwer", Level = 2 };
// Equals測試
if (worker1.Equals(worker2))
Console.WriteLine("worker1 equals worker2");
else
Console.WriteLine("worker1 not equals worker2");
// ==測試
if (worker1 == worker2)
Console.WriteLine("worker1 == worker2");
else
Console.WriteLine("worker1 != worker2");
// Type Name測試
if (worker1.GetType().Name == worker2.GetType().Name)
Console.WriteLine("we are both the same type");
else
Console.WriteLine("we are different types");
}
結果為:
系列部落格導航:
LINQ之路系列部落格導航
LINQ之路 1:LINQ介紹
LINQ之路 2:C# 3.0的語言功能(上)
LINQ之路 3:C# 3.0的語言功能(下)
LINQ之路 4:LINQ方法文法
LINQ之路 5:LINQ查詢運算式
LINQ之路 6:順延強制(Deferred Execution)
LINQ之路 7:子查詢、建立策略和資料轉換
LINQ之路 8:解釋查詢(Interpreted Queries)
LINQ之路 9:LINQ to SQL 和 Entity Framework(上)
LINQ之路10:LINQ to SQL 和 Entity Framework(下)
LINQ之路11:LINQ Operators之過濾(Filtering)
LINQ之路12:LINQ Operators之資料轉換(Projecting)
LINQ之路13:LINQ Operators之串連(Joining)
LINQ之路14:LINQ Operators之排序和分組(Ordering and Grouping)
LINQ之路15:LINQ Operators之元素運算子、集合方法、量詞方法
LINQ之路16:LINQ Operators之集合運算子、Zip操作符、轉換方法、產生器方法
LINQ之路17:LINQ to XML之X-DOM介紹
LINQ之路18:LINQ to XML之導航和查詢
LINQ之路19:LINQ to XML之X-DOM更新、和Value屬性互動
LINQ之路20:LINQ to XML之Documents、Declarations和Namespaces
LINQ之路21:LINQ to XML之產生X-DOM(Projecting)
LINQ之路系列部落格後記