【IT168技术文档】
在过去的2个月里,我发表了一系列贴子,讨论作为Visual Studio和.NET框架Orcas版本一部分发布的一些新的语言特性。这里是这个系列里前4个贴子的链接:
今天的贴子讨论我这个语言系列的最后一个新特性:匿名类型。
什么是匿名类型(Anonymous Types)?
匿名类型是C#和VB的方便语言特性,它允许开发人员在代码内简明地定义行内CLR类型,而不用显式地对类型定义一个正式的类声明。
匿名类型在使用LINQ做查询,转换/投影/构形数据时尤其有用。
匿名类型的例子
在我以前的查询句法贴子里,我示范了你可以通过投影来转换数据。这个LINQ的强有力的特性允许你对一个数据源(不管这个数据源是数据库,XML文件还是内存中的集合)做查询操作,然后对查询数据的结果构形成与原先数据源不同的结构或格式。
在我以前的查询句法贴子里,我定义了一个用来代表我转换过后的产品数据的MyProduct类。通过显式地定义MyProduct类,我就有了一个正式的CLR类型契约,我可以很容易地用它来把我自定义结构的产品结果在web服务间或我的应用解决方案中的多个类和程序集间传递。
但有的时候,我只想要在我当前的代码范围内查询和操作数据,我不想要另外正式地定义一个类来代表我的数据,才可以操作数据。在这种情形下,匿名类型非常有用,因为它们允许你在你的代码内,简明地定义一个新类型在行内使用。
例如,假设我使用Orcas中的LINQ到SQL对象关系映射器设计器对Northwind数据库建模,生成下列的类:
然后我就可以使用下列代码来对数据库里的产品数据进行查询,使用LINQ的投影/转换功能将数据结果定制构形成与上面的Product类有所不同的东西。但不是用一个显式定义的MyProduce类来代表从数据库获取的数据行,而是用匿名类型的特性来隐式地定义一个含4个属性的新类型来代表我定制构形的数据,象这样:
在上面的代码里,作为LINQ表达式select子句的一部分,我声明了一个匿名类型,然后由编译器自动生成带4个属性(Id, Name, UnitPrice 和 TotalRevenue)的匿名类型,这些属性的名称和类型是从查询的构形中推断出来的。
然后我使用了C#中的var这个新关键词来指代从LINQ表达式返回的这个匿名类型的 IEnumerable<T> 序列,还在后面代码的foreach语句里,对这个序列进行循环时,用var来指代其中的每个匿名类型实例。
尽管这个句法给了我动态语言一样的灵活性,我还保留了强类型语言的好处 - 包括 Visual Studio中的编译时检查和代码intellisense支持。例如,注意上面,我是如何对返回的产品序列做foreach的,对从LINQ查询推断出的带自定义属性的匿名类型,我还能得到完整的代码intellisense和编译检查。
理解var关键词
Orcas中的C#引进了var这个新关键词,在声明局部变量时可用于替代类型名。
在第一次看见var这个新关键词时,大家常有的一个错误认识是,这是个后期绑定或者无类型的变量引用(譬如,Object类型的引用或象Javascript中后期绑定的对象引用)。这并不正确,var关键词总是生成强类型的变量引用。不是要求开发人员显式地定义变量的类型,var这个关键词而是告诉编译器在变量最先声明时,从用来初始化变量的表达式推断出变量的类型。
var这个关键词可以用来引用C#的任何类型(意即它可用于匿名类型和显式定义的类型)。实际上,理解var这个关键词的最容易的方法是看一下几个将其用于常见显式类型的例子。譬如,我可以象下面这样使用var这个关键词来声明三个变量:
编译器会根据初始赋值推断出name,age和male变量的类型,在这个例子中,分别是字符串,整数和布尔值。这意味着,编译器会生成与下面代码完全一样的IL:
实际上,CLR根本不知道你使用了var这个关键词,从它的角度来看,上面2个代码例子绝对没有区别。第一个版本只不过是由编译器提供的节省开发人员几下键击的语法糖而已,让编译器做苦力推断出和声明类型名称。
除了使用var这个关键词替代内置的数据类型外,很明显地,你也可以将它用于你定义的任何自定义类型。例如,回到我以前博客贴子中的LINQ查询投影例子,这个投影使用了用来数据构形的显式的MyProduct类型,我可以用var这个关键词将其改写为:
重要注意事项:虽然我在上面使用了var这个关键词,我并没将其用于匿名类型。我的LINQ查询还是使用了MyProduct这个类型来对返回的数据做了构形,这意味着var products声明是IEnumerable<Product> products的速记而已。同样地,在foreach语句中我定义的var p变量不过是MyProduct p的速记而已。
var关键词的重要规则
因为var这个关键词产生强类型的变量声明,编译器需要能够根据它的用法推出其类型。这意味着,在用它来声明变量时,你总是需要做个初始赋值。编译器会产生一个编译错误,如果你不这么做的话:
声明匿名类型
至此,我们介绍了var这个关键词,我们可以开始用它来指代匿名类型了。
C#中的匿名类型是使用与我语言系列第一个博客贴子里讨论过的对象初始化句法同样的句法来定义的。其区别是,不是作为初始化语法的一部分来声明类型名称,而是在实例化匿名类型时,你将new关键词后面的类型名称省略掉:
编译器会分析上面的句法,自动定义一个带有4个属性的新的标准CLR类型。这4个属性的类型是根据赋给的初始值的类型来决定的。例如,在上面的例子中,Id属性被赋值了一个整数,所以编译器将生成一个类型为整数的属性。
匿名类型的实际CLR名称是由C#编译器自动生成的。CLR本身并不知道匿名类型和非匿名类型间的区别,所以两者的运行时语义是绝对完全一样的。Bart De Smet在这里写有一篇很好的博客贴子,对此做了详细讨论,如果你想知道匿名类型的确切类命名模式以及生成的IL的话。
注意上面,当你在匿名类型上键入"product." 时,你依然在Visual Studio中得到编译时检查和完整的intellisense。还注意一下,intellisense描述是如何表明它是个AnonymousType(匿名类型)的,但依然提供了完全的属性声明信息,如红线圈出的文字所示。
使用匿名类型做分层构形
匿名类型可以带来便利的一个强有力的场景是,可以用最小量的代码来轻易地对数据做分层构形投影。
例如,我可以编写下面这样的LINQ表达式,对Northwind数据库中价格大于50美元的所有产品进行查询,然后将返回的产品用以产品的ReorderLevel(库存重订购水平)来排序的一个分层结构来构形(使用了LINQ查询句法支持的group into子句):
将上面的代码在ASP.NET中运行时,我将得到浏览器显示的下列输出:
同样地,我也可以根据JOIN结果来做分层构形。例如,下面的代码生成了一个新匿名类型,它带有几个标准的产品字段属性,以及一个含有客户对特定产品所做的最新5个订单的分层的子集合属性:
注意到我是如何利落地访问分层数据的。在上面,我对产品查询进行循环,然后细钻到每个产品的最新5个订单的。你可以看到,到处都有完整的intellisense和编译时检查,即使是匿名类型上订单细节的嵌套子集合中的对象的属性上也都有。
数据绑定匿名类型
就象我在贴子前面提到的那样,从CLR的立场来说,匿名类型和显式定义的类型间绝对毫无区别。匿名类型和var关键词纯属节省代码量的“语法糖”,其运行时语义跟显式定义的类型是完全一样的。
此外,这意味着,所有的标准.NET类型反射特性,对匿名类型也是工作的,即意味着,象绑定到UI控件的特性同样工作。例如,假如我要显示我前面的分层LINQ查询的结果,我可以象下面这样在一个.aspx网页里定义一个 <asp:gridview> 控件:
上面的.aspx 含有一个gridview,它有2个标准的boundfield列,一个含有嵌套的 <asp:bulletedlist> 控件的模板字段列,我将用这个嵌套控件来显示产品的分层订单细节的子结果集。
然后,我可以编写下面这个LINQ代码来对数据库做一个分层查询,然后将定制构形的数据结果绑定到GridView来显示:
因为GridView支持对任何 IEnumerable<T> 序列的绑定,使用反射获取属性值,它对我上面使用的匿名类型依然工作。
在运行时,上面的代码会产生一个产品细节以及产品最新的订单数量的分层列表的简单网格,象这样:
很明显地,你可以把这个报表做得更丰富,更漂亮,但希望你能从中了解到,现在对数据库做分层查询是多么的容易,可以对返回的结果做你要的构形,然后对结果用编程手法操作或将其绑定到UI控件上。
结语
匿名类型是个很方便的语言特性,允许开发人员在代码内简明地定义行内CLR类型,而不用提供一个正式的类定义声明。虽然它们可以在很多场合下使用,但在使用LINQ查询和转换/构形数据时尤其有用。
这个贴子结束了我5个部分的Orcas语言系列。以后,我会发表更多的LINQ贴子,来示范如何在实战上使用所有这些新语言特性来做常见的数据访问操作(定义数据模型,查询,更新,使用存储过程,验证等等)。但我想先把这个5个部分的语言系列完成,这样我们在我将来的贴子里深入探讨时,你才会真正理解所用的底层语言构造。
希望本文对你有所帮助,
Scott