C#教程:关于匿名类型的特性
匿名类型
首先让我们看一个例子, 假设我们并没有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属性, 不过我们已经在使用另外一个类型了.