C#教程:简化的初始化
简化的初始化
面向对象的编程语言通常都拥有流线型的对象创建过程, 毕竟, 在你准备开始使用一个对象时, 不管是通过代码的直接调用还是工厂方法或者其他的方式你都必须要先创建它. 在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方法是通过名字被发现的——没有任何的接口要求
要求类型必须实现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方法, 在元素被加入之前该集合不会被清除. 例如, 在使用这个集合初始化器之前已经有些元素被加入, 那么之后那些额外的(不在集合中)的元素才会被加入到集合中去. 待续!