簡化的初始化
面向對象的編程語言通常都擁有流線型的對象創建過程, 畢竟, 在你准備開始使用一個對象時, 不管是通過代碼的直接調用還是工廠方法或者其他的方式你都必須要先創建它. 在C# 2中有少數新的特性讓初始化過程變得簡單了一點. 然而如果要做的無法通過構造器參數完成, 很不幸——你需要創建對象, 然後手工初始化設置各個屬性值.
當你想一次初始化一序列對象的時候這可能會令人有點厭煩, 例如在一個數組或者集合中——沒有一個”單個表達式”的做法可以用來初始化一個對象, 你必須被迫使用局部變量來做臨時的處理, 或者創建一個幫助方法並基於參數來執行適當的初始化.
C# 3的到來提供了多個解決方法.
定義我們的案例類型
在本節中我們將要使用的表達式稱為對象初始化器. 這僅僅是用於指定當一個對象被創建後應該執行的初始化過程. 你可以設置屬性, 或者屬性的屬性, 也可以添加元素到那些可以通過屬性訪問到的集合. 為了演示, 我們將再次使用Person類. 一開始, 這裡依然有我們之前使用過的name和age字段, 其通過可寫的屬性暴露出來. 我們將會同時提供無參構造函數以及只接受name作為參數的另一構造函數. 我們還增加了一個friends的集合以及類型為Location的home屬性, 這兩個都是只讀的, 但依然可以通過處理返回的對象來更新. 另外, Location類提供了Country和Town屬性用於表示Person的家庭地址. 完整的代碼如下:
1: public class Person
2: {
3: public int Age { get; set; }
4: public string Name { get; set; }
5: List<Person> friends = new List<Person>();
6: public List<Person> Friends { get { return friends; } }
7: Location home = new Location();
8: public Location Home { get { return home; } }
9: public Person() { }
10: public Person(string name)
11: {
12: Name = name;
13: }
14: }
15:
16: public class Location
17: {
18: public string Country { get; set; }
19: public string Town { get; set; }
20: }
上面的代碼是相當直觀的, 但其沒有什麼價值. 當Person被創建的時候, Friends和Home都是以一種”空”的方式被創建的, 而不是null. 在以後這是相當重要的——不過先在我們只要先留意表示Person的name和age的屬性值.
設置簡單屬性
現在我們已經擁有了Person類型, 我們希望使用C# 3的一些新的特性來創建實例. 首先我們將先關注Name和Age屬性, 然後再考慮其他的元素.
實際上, 對象初始化器並沒有限制你必須使用屬性. 所有的語法糖同樣適用於字段, 只不過多數的時間你應該使用屬性. 在一個封裝良好的系統中, 你不應該可以直接訪問字段, 除非創建類型實例並使用類型內自己的代碼.
假設我們創建一個Person實例叫做Tom, 4歲. 對於C# 3, 這裡有兩種方式可以完成這個工作:
1: Person tom1 = new Person();
2: tom1.Name = "Tom";
3: tom1.Age = 4;
4: Person tom2 = new Person("Tom");
5: tom2.Age = 4;
第一個版本使用無參構造函數然後給兩個屬性賦值. 第二個版本使用構造函數的一個重載來初始化Name, 直接給Age賦值. 這兩種做法在C# 3中都是可行, 然而, C# 3還提供了另外的一個選擇:
1: Person tom3 = new Person() { Name="Tom", Age=4 };
2: Person tom4 = new Person { Name="Tom", Age=4 };
3: Person tom5 = new Person("Tom") { Age = 4 };
在每行中的大括號之後都是對象初始化器, 再次申明, 這僅僅是編譯器一個花招. 通常, tom3和tom4的IL代碼是一致的, 而且實際上它們與tom1產生的IL也是大致一樣的. 同樣, 可以預見到, tom5的IL代碼與tom2也是基本一致的. 注意, tom4我們省略了構造器後面的括號, 你可以使用這種針對無參擴展函數的快捷方式, 其會在編譯後的代碼當中被調用.
在構造器被調用之後, 相應的屬性將會按照對象初始化器的順序被顯式賦值, 而且你只能針對這些屬性賦值一次——例如你不能對Name屬性賦值兩次(實際上, 你可以這麼做, 調用構造器並且使用一個name參數, 然後再對Name屬性賦值. 這樣做沒有什麼意義, 但是編譯器並不阻止你這樣做.) 那些被作為值賦給屬性的表達式可以是任何本身不是Assignment的表達式——你可以調用方法, 創建新的對象(可能使用對象初始化器), 幾乎可以是任何的表達式. 你可以能奇怪這到底有多少用處——我們已經省掉了1,2 行代碼, 但這並不是一個足夠好的理由而讓語言本身變得更復雜, 不是嗎? 這裡有一個很微妙的觀點, 盡管我們剛剛用一行代碼創建了一個對象——我們使用一個表達式來創建它.這裡的不同之處是非常重要的, 假設你想使用一個預定義的數據創建一個Person類型的數組, 即使不使用隱式數組, 其代碼也是相當簡潔並具有很好的可讀性:
1: Person[] family = new 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="William", Age=1 },
7: new Person { Name="Robin", Age=1 }
8: };
在C# 1 和2 中, 在一個像上述這樣簡單的例子中, 我們也可以編寫一個帶有name和age參數的構造函數, 然後使用它來初始化數組. 然而, 正確的構造函數並不總數存在, 如果一個構造函數帶有多個參數, 通常從它所在的位置並不能明確每一個參數表示什麼意思, 如果一個構造函數帶有5,6個參數, 通常可能要更多的依賴於智能提示, 在這類的例子中, 使用屬性賦值可以帶來更佳的可讀性.
這種形勢的對象構造器可能會是你最常使用的, 然而, 還存在另外兩種其他形式——一個是子屬性的賦值, 另外一個是加入到集合.
對嵌入對象設置屬性值
目前為止我們可以發現對Name和Age屬性賦值是非常簡單的, 但我們不能使用相同的方法對Home進行賦值——因為它是只讀的. 不過, 我們可以首先讀取Home屬性, 然後再其結果值上面設置屬性值. 為了更清楚說明問題, 我們首先看一下在C# 1當中的代碼:
1: Person tom = new Person("Tom");
2: tom.Age = 4;
3: tom.Home.Country = "UK";
4: tom.Home.Town = "Reading";
當我們構建home location的時候, 每一個聲明語句都通過get取得一個Location實例, 然後在其上設置相關的屬性值. 這裡沒有任何新鮮的東西, 但是讓我們慢下來仔細看一下, 這是值得的, 否則, 我們很容易就會錯過幕後所發生的一切.
C# 3中允許我們在一行代碼當中完成同樣的事情, 如下所示:
1: Person tom = new Person("Tom")
2: {
3: Age = 4,
4: Home = { Country="UK", Town="Reading" }
5: };
編譯後這兩個代碼片段是完全一致的. 編譯器完全可以分辨到Home等號後面跟著的是另外一個對象初始化器, 並且會正確的設置相應的屬性值到此嵌入對象上.
這裡在初始化Home部分缺少的new關鍵字是非常重要的. 如果你想知道哪裡編譯器將會創建一個新的對象, 哪裡編譯器又將會是針對已有對象賦值, 只需要觀察一下初始化器部分new是否出現. 每次我們創建一個新的對象, new關鍵字總是會在某一個地方出現的.
我們已經處理了Home屬性——但是Tom的friends呢? 有幾個屬性我們可以直接在List<Person>設置, 但是沒有一個可以用於將entries加入到這個列表當中. 現在是時候來了解一下下一個新的特性——集合初始化器.
集合初始化器
使用一些初始化值來創建一個集合是一個非常平常的任務. 在C# 3之前, 唯一能夠帶來一點幫助的語言特性是數組的創建過程, 而且其在很多情況下是比較笨拙的. C# 3擁有集合初始化器, 這允許你使用與數組初始化器相同的語法, 然而適用於任意的集合並且更加的靈活.
假設我們想構建一系列的包含一些名字的字符串列表, 在C# 2當中我們可以使用如下的做法:
1: List<string> names = new List<string>();
2: names.Add("Holly");
3: names.Add("Jon");
4: names.Add("Tom");
5: names.Add("Robin");
6: names.Add("William");
而在C# 3中, 要達到同樣的目的只需要更少的代碼:
1: var names = new List<string>
2: {
3: "Holly", "Jon", "Tom",
4: "Robin", "William"
5: };
除了減少了幾行代碼之外, 集合初始化器主要帶來了兩個好處:
當你想使用一個集合作為方法的參數或者作為另外一個大的集合的元素的時候, 第一點將變得更加重要. 盡管這相對來說發生的幾率較小——不過對我來說才第二點才是真正殺手級的特性. 如果你觀察一下代碼的右邊, 你可以找到你所需要的信息, 而且每一個部分都只編寫了一次. 變量只使用了一次, 類型只是使用了一次, 麼一個元素也只出現了一次. 所有這一些都極其簡單, 而且比C# 2更加清晰. 另外集合初始化器不僅僅被限制在List部分, 你可以將其用於任何實現了IEnumerable, 並且有適當的公共Add方法以便應用於初始化器當中的每一個元素. 你也可以使用包含多個參數的Add方法, 做法是將其對應值包含在一對大括號中. 例如, 我們想要創建一個映射到name和age的Dictionary, 可以使用下面的代碼:
1: Dictionary<string,int> nameAgeMap = new Dictionary<string,int>
2: {
3: {"Holly", 31},
4: {"Jon", 31},
5: {"Tom", 4}
6: };
在這個例子中, Add(string, int)方法將會被調用3次, 如果有不同的Add重載存在, 不同元素的初始化器會調用對應的重載. 如果沒有找到合適的重載, 編譯將會失敗. 這裡有兩個比較有趣的設計決定:
要求類型必須實現IEnumerable是一個合理的要求 以便確認該類型是某種集合類型, 而使用任何公共的Add方法(而不是要求一個精確的簽名)則允許簡單初始化器的使用(例如前面的例子). 非公開的重載, 包括那些顯式的接口實現, 都沒有被使用. 這和對象初始化器是有點不同的, 其內部屬性也是可見的.(在同一個Assembly中).
在早期的指導說明書(specification)中要求必須實現ICollection<T>, 並實現單一參數的Add方法(因為接口的限制), 而不是多個重載. 這看起來更加純粹, 但實現IEnumerable的類型遠比實現ICollection的類型要多得多——而且使用單一參數的Add方法也是不方便的. 例如, 在我們上面的例子中, 我們將不得不顯式為每個元素的初始化器創建一個KeyValuePair<string,int>的實例. 犧牲一點純正的學院血統使得語言本身在真實世界中更加有用了.
目前為止我們看到的集合初始化器都是以獨立的方式來創建整個集合. 實際上他們也可以和對象初始化器捆綁使用來填充內嵌的集合, 如下所示:
1: Person tom = new Person
2: {
3: Name = "Tom",
4: Age = 4,
5: Home = { Town="Reading", Country="UK" },
6: Friends =
7: {
8: new Person { Name = "Phoebe" },
9: new Person("Abi"),
10: new Person { Name = "Ethan", Age = 4 },
11: new Person("Ben")
12: {
13: Age = 4,
14: Home = { Town = "Purley", Country="UK" }
15: }
16: }
17: };
以上的例子演示了我們提到的關於對象初始化器和集合初始化器的所有特性. 這其中比較有趣的部分是集合初始化器部分, 其本身內部又使用了對象初始化器. 這裡我們並不沒有像我們創建獨立的集合那樣, 我們沒有創建一個新的集合, 而是將元素加入到一個已有的集合中.我們還可以觀察得更遠一些, 指定Friends的Friends, 等等. 使用這種語法不能做的事你不能指定Ben是Tom的friend——因為當你不能訪問一個正在初始化的對象. 這在一些情況下會有些尴尬, 但多數時候不會成為一個問題. 對於集合初始化器當中的每一個元素, 集合的getter操作將會被調用, 然後調用適當的Add方法, 在元素被加入之前該集合不會被清除. 例如, 在使用這個集合初始化器之前已經有些元素被加入, 那麼之後那些額外的(不在集合中)的元素才會被加入到集合中去. 待續!