程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> C#教程:關於匿名類型的特性

C#教程:關於匿名類型的特性

編輯:關於.NET

匿名類型

首先讓我們看一個例子, 假設我們並沒有Person類, 並且我們關心的屬性只有Name和Age. 下面的代碼演示了我們如何在沒有聲明類型的情況下來構建一個對象的:

   1: var tom = new { Name = "Tom", Age = 4 };
   2: var holly = new { Name = "Holly", Age = 31 };
   3: var jon = new { Name = "Jon", Age = 31 };
   4: Console.WriteLine("{0} is {1} years old", jon.Name, jon.Age);

可以看到, 初始化一個匿名類與我們之前提到的對象初始化器非常相似——區別僅僅是在new和開始的大括號之間的類型名稱沒有了. 我們正在使用隱式的局部變量, 因為這是我們的唯一選擇——我們沒有任何的類型名可以用於聲明該變量. 從上面的最後一行代碼你可以看到, 類型用於Name和Age屬性, 他們可以被讀取並且其值就是在隱式類型初始化器中被賦予的值, 因此, 該輸出將會是”Jon is 31 years old”. 屬性將會有與初始化器內的表達式一樣的類型——Name是string類型, Age是int類型. 和對象初始化器一樣的是, 方法和構造器也可以被用於隱式對象初始化器中, 用於做任何你想做的是事情.

現在你知道為什麼隱式類型數組為什麼這麼重要了. 假設我們想創建包含整個家庭成員的數組, 然後迭代算出總的年齡. 以下的代碼將會完成這個工作——同時也演示了其他一些有趣的關於匿名類型的特性.

   1: var family = new[]
   2: {
   3:     new { Name = "Holly", Age = 31 },
   4:     new { Name = "Jon", Age = 31 },
   5:     new { Name = "Tom", Age = 4 },
   6:     new { Name = "Robin", Age = 1 },
   7:     new { Name = "William", Age = 1 }
   8: };
   9: int totalAge = 0;
  10: foreach (var person in family)
  11: {
  12:     totalAge += person.Age;
  13: }
  14: Console.WriteLine("Total age: {0}", totalAge);

從前面了解過的隱式類型數組來看, 我們可以推論出一個重要的事情: 所有的家庭成員類型都是一樣的. 因為如果他們使用匿名類型初始化器創建的都是一個新的類型, 那麼明顯數組類型不可能被正確的聲明. 在一個給定的Assembly中, 如果兩個匿名類型擁有同樣數量的屬性, 並且他們有相同的名字和類型, 以及相同的出現順序, 那麼編譯器將會把他們當成同一個類型. 換句話說, 如果我們把其中一個的Name和Age屬性調換一下, 編譯器將會生成一個新的匿名類型——同樣的,如果我們在新行中引入一個而外的屬性, 或者使用long類型代替Age的int類型, 這些統統會導致編譯器生成一個新的匿名類型.

實現細節:如果你曾經看過匿名類的IL代碼, 你應該知道即使兩個匿名對象初始化器擁有同樣的屬性名和出現出現, 但卻使用不同的類型, 那麼編譯器會生成兩個不同的類型, 而它們實際上是通過一個單獨的泛型類來生成的. 該泛型類是參數化的, 但其封閉的構造類型將會因為給與不同初始化器的類型參數不同而不同.

我們可以使用foreach表達式應用於上述的數組, 就像我們用於集合中的一樣. 類型是由編譯器推斷的, person的類型也就是數組的類型. 再次提一下, 我們可以將同樣的變量用於不同的實例中, 因為它們全部都擁有相同的類型.

上述的代碼同樣驗證了Age屬性是強類型的int, 否則我們試圖計算age總和將會導致錯誤. 編譯器了解匿名類型, VS2008更是通過tooltip的方式提供更多的信息. 現在我們已經了解了足夠的關於匿名類型信息, 接下來讓我們看看編譯器實際上為我們做的工作.

匿名類型的成員

匿名類型是由編譯器創建並其包含在編譯後的Assembly當中, 其方式與匿名方法和iterator block的創建方式是一致的. CRL把它們都當成普通的類型, 實際上他們就是普通的類型——如果你將其從匿名類型更改成為一個普通類型, 並且手工編寫所有行為的代碼, 我們不會看到有任何的改變. 匿名類型包含以下的成員:

  • 一個負責所有初始化值的構造器, 其參數將會是與匿名對象初始化器當中出現的順序和類型一致, 同樣名稱也是一樣的.
  • 公共只讀的屬性
  • 私有的只讀字段, 用於支持屬性
  • 重載了Equals, GetHashCode和ToString

這就是全部了, 沒有實現任何借口, 沒有克隆和序列化能力——僅僅是一個構造器, 一些屬性和幾個來自於object的平常的方法.

構造器和屬性完成都是一些顯而易見的事情. 兩個來自於相同匿名類型的實施是否相等, 通過輪流比較所有屬性類型的Equals方法來決定.hash代碼生成也是一類似的, 通過調用每個屬性的GetHashCode然後再合並結果. 將多個hash code合並成為一個組合, 其方法並沒有被指定, 因此你不能在你的代碼中依靠它——你唯一有信心的就是, 兩個相等的對象實例必然返回相同的hash值, 而兩個不等的實例可能會返回不等的hash值. 當然所有這些也只有當屬性類型實現的GetHashCode和Equals也遵循常規規則的時候才有效.

注意因為屬性是只讀的, 因此匿名類是不可變的, 因此屬性的類型也是不可變的. 由於這個不可變性, 你不用擔心屬性在賦值後會被改變, 跨線程自然也沒有問題.

發散性初始化器(projection initializers)

目前我們看到的匿名對象初始化器中我們一直使用簡單的name / value對——Name=”Jon”, Age=31, 雖然有時候我們確實是這麼做, 但是更多時候在真實的編程中, 我們可能會從一個已有的對象中來拷貝屬性值. 有時我們會通過一些方式來讀取值, 不過更經常的是拷貝就足夠了.

沒有LINQ, 要給出令人信服的例子有點困難, 不過讓我們回到我們的Person類, 假設我們有一個很好的理由我們想將一個Person實例的集合轉換到另外一個類似的集合, 其元素包含有一個name和一個flag指示該person是否是一個成年人. 給定一個合適的變量, 我們可以使用下面的代碼:

   1: new { Name = person.Name, IsAdult = (person.Age >= 18) }

該代碼當然能夠工作, 如果僅僅是設置一個單一的name屬性使用此方法當然還不算笨拙——但如果你要拷貝很多的屬性, 你應該考慮其他的嘗試. C# 3提供了一個快捷方式: 如果你沒有指定屬性名稱, 而僅僅使用表達式去對值進行評估, 那麼編譯器將會用表達式的最後一部分作為屬性名. 這被稱為發散性初始化器. 這意味著我們可以將上面的代碼改寫為:

   1: new { person.Name, IsAdult = (person.Age >= 18) }

將一個匿名對象初始化器變為一個發散性初始化器是很常見的——通常當你想從一個對象拷貝一些屬性到另外一個對象的時候發生, 而經常作為join操作的一部分. 一下顯示了完整的代碼, 使用了List.ConvertAll方法和匿名代理:

   1: List<Person> family = new List<Person>
   2: {
   3:     new Person {Name="Holly", Age=31},
   4:     new Person {Name="Jon", Age=31},
   5:     new Person {Name="Tom", Age=4},
   6:     new Person {Name="Robin", Age=1},
   7:     new Person {Name="William", Age=1}
   8: };
   9: var converted = family.ConvertAll(delegate(Person person)
  10: { 
  11:     return new { person.Name, IsAdult = (person.Age >= 18) }; }
  12: );
  13: foreach (var person in converted)
  14: {
  15:     Console.WriteLine("{0} is an adult? {1}",
  16:     person.Name, person.IsAdult);
  17: }

上述的代碼初始了使用了發散性初始化器, 我們還展示了匿名方法和代理類型推論的價值, 沒有它們, 我們無法保持轉換後的類型依然是強類型的. 因為我們無法指定TOutput參數在轉換器當中的類型. 在完成轉換之後, 我們可以使用一個迭代來遍歷整個List並訪問Name和IsAdult屬性, 不過我們已經在使用另外一個類型了.

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