【IT168 技术文档】
查询综合
查询综合提供了一个和SQL非常类似的语言集成语法来进行查询,不但和VB融合的很好,而且和新的.net语言集成查询框架平滑结合。
熟悉SQL实现的人就会发现,在当前的.NET框架的序列操作符中,许多关系代数的操作符如发射,选择,叉乘,分组和排序这些查询处理中的查询符号。
因为查询综合的语义是通过把它们转化为序列操作符来定义的,这些操作符将会和任何在此范围内的序列操作符绑定。这暗示了通过引进一个特殊的实现,查询综合语法可以有效地被用户重新绑定。特别的,查询综合可以和一个使用Dlinq基础结构的序列操作符实现重新绑定,或者和试图分布式的在多个本地或者远程的数据源执行查询的一个本地查询优化器绑定。这种序列操作符的重新绑定和经典的COM提供者模型在本质上类似,这样相同接口上的不同实现可以无须修改底层应用代码来支持多个操作和部署。
基本的Select...From...Where...综合过滤出所有满足Where子句中条件的值,以下是一个例子显示如何查找到人口少于100万的国家:
Dim SmallCountries = Select Country _
From Country In Countries _ Where Country.Population < 1000000
在一个序列操作符中,操作符It和当前"行"绑定。It的成员如Me是自动进入这个范围的。It的概念和XQuery的上下文项"."相关联,它可以像SQL中"*"中一样被使用。比如,我们想使用如下的查询返回所有的国家和其首都的集合对象:
Dim CountriesWithCapital = _
Select It _
From Country In Countries, City In Capitals _
Where Country.Name = City.Country
这个本地声明的推断类型是IEnumerable(Of { Country As Country, City As City }).
使用Order By子句,我们可以根据任何数量的排序关键字对查询结果排序。下面的例子中,查询语句按照它们的经度升序和人口降序的方式返回所有的国家名字列表:
Dim Sorted = Select Country.Name _
From Country In Countries, City In Capitals _
Where Country.Name = City.Country
Order By City.Longtitude Asc, Country.Population Desc
集合操作符如Min, Max, Count, Avg, Sum...操作集合对象并"聚合"它们成一个单值。我们可以计算所有的小国家:
Dim N As Integer = _
Select Count(Country) _
From Country In Countries _
Where Country.Population < 1000000
像SQL一样,我们提供了特殊的聚合语法,这对同时进行多个聚合操作是非常方便的。比如,我们可以使用一条语句,统计所有的小国家数目,并且计算它们的平均人口密度:
Dim R As { Total As Integer, Density As Double } = _
Select New { .Total = Count(Country), _
.Density = Avg(Country.Population/Country.Area) } _
From Country In Countries _
Where Country.Population < 1000000
这种形式的聚合对应用一个编译器产生的聚合函数对没有进行聚合的自然结果集合的结果是非常方便的。聚合函数一般经常出现在分割源集合对象时使用。举例来说,我们可以根据是否处于热带来对所有国家进行分组,然后统计每个组的数目。为了这样做,我们引进一下Group By子句。IsTropical是对判断国家是否处于热带的一个封装函数:
Partial Class Country
Function IsTropical() As Boolean
Return TropicOfCancer =< Me.Latitude _
AndAlso Me.Latitude >= TropicOfCapricorn
End Function
End Class
通过这个函数,我们可以使用和上面一样的聚合,但是首先得按照Country.IsTropical得结果是否一样来对输入得Country和Capital对的集合对象进行分割。在这个例子里有两个这样的分类:一个包含热带国家帕劳群岛,伯利兹城和马达加斯加岛,另一个包含非热带国家摩纳哥。
关键字 国家 城市
Country.IsTropical() = True Palau Koror
Country.IsTropical() = True Belize Belmopan
Country.IsTropical() = True Madagascar Antanarivo
Country.IsTropical() = False Monaco Monaco
然后,我们通过计算总数和平均密度来对这些组进行值的聚合。返回类型是一个对Total As Integer和 Density As Double的集合对象:
Dim CountriesByClimate _
As IEnumerable(Of Total As Integer, Density As Double }) =_
Select New { .Total = Count(Country), _
.Density = Avg(Country.Population/Country.Area) } _
From Country In Countries, City In Capitals _
Where Country.Name = City.Country
Group By Country.IsTropical()
以上的查询隐藏了相当复杂的操作,因为Group By子句的结果实际上是一个组值的集合对象,其类型是IEnumerable(Of Grouping(Of { Boolean, { Country As Country, City As City })),和上面讨论的很相象。每个这样的Grouping项包含一个从key提取表达式Country.IsTropical()衍生的Key成员和一个包含单个key提取表达式值相同的国家和城市的集合对象。VB编译器合成用户定义的聚合函数,即给定一个组,通过聚合每个分割来计算所要的结果。
注意前面的例子里每个Group同时包含Country和Capital,我们只需要Country来计算最后的查询结果。Group By子句允许这些组的一个预先选择。比如,我们可以根据国家的气候对国家名进行分类:
Dim ByHemisphere As IEnumerable(Of Grouping(Of Boolean, String)) = _
Select It _
From Country In Countries, City In Capitals _
Where Country.Name = City.Country
Group Country.Name By City.Latitude >= 0
这会返回集合对象{ New Grouping { .Key = False, .Group = { "Madagascar", "Belize" }}, New Grouping { .Key = True, .Group = { "Palau" }}。
查询综合在Visual Basic 8.0里是完全组合式的,意思就是说查询综合可以任意的嵌套。组合特性使得通过每个孤立的子表达式来理解大的查询变得很容易。组合特性也使得定义语义和语言的规则变得更加容易。组合特性,作为一个设计原则,和SQL的设计原则有所不同。SQL语言不是完全组合的,程序员还必须通过日积月累的社区里的经验积累来处理很多个例。由于却反完全的组合特性,所以通过个体片断来理解一个复杂的SQL查询总体上来说是不可能的。
SQL缺乏组合特性的原因是关系数据模型就不是组合式的。比如,表可能不包含子表;也就是说,所有的表都必须是平面的。结果,SQL程序员只能写单片的结果为平面表的表达式来适应SQL的数据模型,而不是将大的表达式分解为小的。引用Jim Gray的话来说"计算机科学里任何不是递归的东西都不是好的"因为Visual Basic是基于CLR类型系统,并没有限制哪种类型作为其他类型的组件出现。除去静态类型规则,并没有限制表达式作为其他类型的组件出现。因此,不但是行,对象,XML,还有活动目录,文件,注册表项等等都是查询源和查询结果的"一等公民"。
扩展方法
很多.net框架标准查询结构的强大能力来源于扩展方法。事实上编译器将所有的查询综合直接转换为定义在此范围内的命名空间里的标准的查询操作符扩展方法。扩展方法是标记了定制属性的共享方法,并且它们被允许从实例方法语法中调用。事实上,扩展方法使用增加的方法扩展了已有的类型和构造的类型。
由于扩展方法主要是为库设计者准备的,Visual Basic并未提供直接的语言语法来支持声明它们,而是让作者直接在模块和成员上附上所需的定制属性来标明它们作为扩展方法,以下的例子定义了一个集合对象上的扩展方法Count:
<System.Runtime.CompilerServices.Extension> _
Module MyExtensions <System.Runtime.CompilerServices.Extension> _
Function Count(Of T)([Me] As IEnumerable(Of T)) As Integer
For Each Dim It In [Me]
Count += 1
Next
End Function
End Module
方括号是一个关键字转义,允许Me作为一个变量名存在。因为扩展方法是一个模仿实例方法的共享方法,使用标识符Me作为输入是很方便的,就好像我们在一个真实的实例方法里,但是它是一个关键字必须使用方括号转义,因此并不能真的被使用在共享方法里。扩展方法只是普通的共享方法,因此我们可以像Visual Basic中调用其他共享函数一样调用Count函数,只是提供所要操作的实例集合对象:
Dim TotalSmallCountries = _
MyExtensions.Count(Select Country _
From Country In Countries _
Where Country.Population < 1000000)
扩展方法通过普通的Import语句进行使用,这些扩展方法将会作为由第一个参数确定的类型的增加的方法出现:
Imports MyExtensions
Dim TotalSmallCountries = _
(Select Country _
From Country In Countries _
Where Country.Population < 1000000).Count()
扩展方法比普通的实例方法的优先级低,如果一个调用表达式的正常过程发现没有可用的实例方法,编译器将会试着将调用理解为一个扩展方法的调用。
使用这种查询的最自然的方法是,使用聚合语法,和我们前面看到的一样:
Dim TotalSmallCountries = _
Select Count(Country) _
From Country In Countries _
Where Country.Population < 1000000
嵌套函数
许多标准的查询操作符如Where,Select,SelectMany等,都是定义为扩展方法,并且使用类型为Func(Of S,T)的委托作为参数。为了使编译器将综合转化为查询操作符,或者为了VB程序员能够直接调用查询操作符,有必要更加容易的创建委托。特别的,我们需要能够创建closure,能够捕捉它们的上下文环境。Visual Basic创建closure的机制是通过嵌套本地函数和子程序声明保证的。
为展示嵌套式函数的用法,我们将会调用定义在System.Query命名空间里的查询操作符。其中一个扩展方法是TakeWhile函数,它从一个序列里提取元素只需要测试结果为真,然后跳开序列里剩下的部分。
<Extension> _
Shared Function TakeWhile(Of T) _
(source As IEnumerable(Of T), Predicate As Func(Of T, Boolean)) _
As IEnumerable(Of T)
OrderByDescending操作符对它的参数集合对象进行排序,并且按照排序关键字进行降序排序:
<Extension> _
Shared Function OrderByDescending (T, K As IComparable(Of K)) _
(Source As IEnumerable(Of T), KeySelector As Func(Of T, K)) _
As OrderedSequence(Of T)
另外一个查找到所有小国家的方法是,首先通过人口对它们进行排序,然后使用TakeWhile来挑选出人口少于100万的国家:
Function Population(Country As Country) As _
Integer Return Country.PopulationEnd FunctionFunction LessThanAMillion_
(Country As Country) As Boolean Return Country.Population <_
1000000End FunctionDim SmallCountries = _
Countries.OrderBy(AddresOf Population) _
.TakeWhile(AddresOf LessThanAMillion)
尽管不需要查询综合,VB可以支持匿名函数和子程序的直接语法(称为"lambda表达式"),编译器将此转化为本地函数声明。