摘要:本文概述支持数据密集型编程的新的 Visual Basic 语言特性和新的语言扩展。
简介 | |
开始使用 Visual Basic 9.0 | |
隐式类型本地变量 | |
对象和集合初始值设置项 | |
匿名类型 | |
深层次的 XML 支持 | |
查询综合 | |
扩展方法 | |
嵌套函数 | |
空类型 | |
宽松的委托 | |
动态接口(或强“鸭子类型”) | |
动态标识符 | |
小结 |
“代号为 Orcas 的 Visual Basic”(Visual Basic 9.0) 引入了一些语言扩展功能,以统一的方式支持数据密集型编程 — 创建、更新以及查询关系型数据库、XML 文档和对象图形,这些语言扩展均基于“代号为 Whidbey 的Visual Basic”而构建。除此之外,Visual Basic 9.0 还引入一些新的语言特性,用于支持可能的静态输入和必要的动态输入,以增强 Visual Basic 的特有功能。这些新特性是:
• | 隐式类型本地变量 |
• | 查询综合 |
• | 对象初识值设定项 |
• | 匿名类型 |
• | 与 Linq 框架的完全集成 |
• | 深层次的 XML 支持 |
• | 宽松的委托 |
• | 空类型 |
• | 动态接口 |
• | 动态标识符 |
本文对这些新特性进行简要概述。要获得包括 Visual Basic 语言定义和编译器预览在内的更多信息,请访问 Visual Basic 开发人员中心 (http://msdn.microsoft.com/vbasic/default.aspx)。
为了领会这些语言特性在实际应用中的作用,我们举一个真实示例 — CIA World Factbook 数据库。该数据库包罗万象,涵盖了世界各国的地理、经济、社会和政治信息。就本文示例而言,我们采用一个包含各国国名、首都、总面积和人口的架构。在 Visual Basic 9.0 中,我们以下面这个类来表示该架构:
Class Country Public Property Name As String Public Property Area As Float Public Property Population As Integer End Class
以下是国家数据库的一个小子集,我们将以其作为运行示例:
Dim Countries = _ { new Country{ _ .Name = "Palau", .Area = 458, .Population = 16952 }, _ new Country{ _ .Name = "Monaco", .Area = 1.9, .Population = 31719 }, _ new Country{ _ .Name = "Belize", .Area = 22960, .Population = 219296 }, _ new Country{ _ .Name = "Madagascar", .Area = 587040, .Population = 13670507 } _ }
给定该列表后,通过以下查询综合,我们就能够查询所有人口小于一百万的国家:
Dim SmallCountries = Select Country _ From Country In Countries _ Where Country.Population < 1000000 For Each Country As Country In SmallCountries Console.WriteLine(Country.Name) Next
因为只有马达加斯加岛 (Madagascar) 拥有超过一百万的居民,所以编译并运行以上程序时,将打印以下国家名称列表:
Palau Monaco Belize
Visual Basic 9.0 提供的特性使编程完成这项查询相当简单,让我们通过研究该程序来理解这些特性。首先,Countries 变量的声明
Dim Countries = _ { new County { .Name = "Palau", .Area = 458, .Population = 16952 }, _ ... _ }
使用了新的对象初始值设定项语法,即 new Country {..., .Area = 458, ...},该语法类似于现有的 With 语句,通过这种简洁的、基于表达式的语法可以创建复杂的对象实例。
它还说明了隐式类型的本地变量 的声明方法,即编译器从声明右侧的初始化表达式推断本地变量 Countries 的类型。以上声明完全等效于显式类型的 Country() 类型的本地变量声明。
Dim Countries As Country() = {...}
需要强调的是,这仍然是强类型声明;编译器已经自动推断出本地声明右侧的类型,因此程序员无需手动将其输入到程序中。
SmallCountries 本地变量的声明通过类似于 SQL 风格的查询综合进行初始化,以筛选出居民总数不足一百万的所有国家。与 SQL 的异曲同工并非有意的,其目的是使已经了解 SQL 的程序员能够更迅速地了解 Visual Basic 的查询语法。
Dim SmallCountries = Select Country _ From Country In Countries _ Where Country.Population < 1000000
请注意,我们还有另一个隐式输入的应用程序:编译器将 SmallCountries 的类型推断为 IEnumerable(Of Country)。编译器本身将查询综合翻译成标准的查询操作符。在这种情况下,翻译如以下语句一样简单:
Function F(Country As Country) As Boolean Return Country.Population < 1000000 End Function Dim SmallCountries As IEnumerable(Of Country) = _ Countries.Where(AddressOf F)
扩展语法将编译器生成的本地函数作为委托 AddressOf F 传递到扩展函数 Where 中,在标准的查询操作符库中,该函数定义为 IEnumerable(Of T) 接口的扩展。
既然我们已经了解了 Visual Basic 9 中的一些新特性,那么就让我们进一步研究其中的细节吧。
在隐式类型的本地变量声明中,本地变量的类型从本地声明语句右侧的初识化表达式中推断。例如,编译器将推断以下所有变量声明的类型:
Dim Population = 31719 Dim Name = "Belize" Dim Area = 1.9 Dim Country = New Country{ .Name = "Palau", ...}
因此,这些声明完全等效于以下显式类型声明:
Dim Population As Integer = 31719 Dim Name As String = "Belize" Dim Area As Float = 1.9 Dim Country As Country = New Country{ .Name = "Palau", ...}
因为在默认情况下推断本地变量声明的类型,所以无论 Option Strict 的设置为何,总是早期绑定对于这些变量的访问。在 Visual Basic 9.0 中,程序员必须按以下方式将变量显式声明为类型 Object,从而显式指定晚期绑定:
Dim Country As Object = new Country{ .Name = "Palau", ... }
要求使用显式晚期绑定是为了防止意外使用晚期绑定,但更重要的是,这样做可以有力地扩展它对诸如 XML 等新数据类型的晚期绑定,这一点我们稍后讨论。有一个可选的项目级开关可以切换现有的行为。
For...Next 或 For Each...Next 语句中的循环控制变量也可以是一个隐式类型变量。在 For Dim I = 0 To Count 或 For Each Dim C In SmallCountries 中指定循环控制变量时,标识符将定义一个新的隐式类型本地变量,其类型从初始值设定项或集合表达式中推断,并作用于整个循环。在 For 右面使用 Dim 与隐式类型的循环控制变量一样,都是 Visual Basic 9.0 引入的新特性。
通过该类型引用应用程序,我们能够按以下方式重写打印所有小国的循环:
For Each Dim Country In SmallCountries Console.WriteLine(Country.Name) Next
Country 的类型推断为 SmallCountries 的元素类型 Country。
在 Visual Basic 中,With 语句无需多次指定目标表达式,即可简化对一个聚合值的多个成员的访问。在 With 语句块内,对以句点 (“.”) 开头的成员访问表达式进行求值,就好像句点前面带有 With 语句的目标表达式一样。例如,以下语句初始化一个新的 Country 实例,随后又将该实例的字段初始化为所需的值:
Dim Palau = New Country() With Palau .Name = "Palau" .Area = 458 .Population = 16952 End With
新的 Visual Basic 9.0 对象初始值设定项采用的是一种基于表达式的 With 格式,用于以简明的方式创建复杂的对象实例。通过对象初识值设定项,我们能够将以上两条语句捕获为一条(隐式类型的)本地声明,如下所示:
Dim Palau = New Country { _ .Name = "Palau", _ .Area = 458, _ .Population = 16952 }
这种在表达式中进行的对象初始化对于查询而言非常重要。一条查询语句通常就像由等号右侧的 Select 子句初始化的对象声明。由于 Select 子句返回一个表达式,因此我们必须能够以一条表达式初始化整个对象。
我们已经看到,对象初始值设定项对于创建复杂对象的集合也是很方便的。任何支持 Add 方法的集合都能够通过一个集合初始值设定项 表达式进行初始化。例如,给定城市声明作为部分类,
Partial Class City Public Property Name As String Public Property Country As String Public Property Longitude As Float Public Property Latitude As Float End Class
我们能够按以下方式创建一个示例国家的首都城市列表:
Dim Capitals = New List(Of City){ _ { .Name = "Antanarivo", _ .Country = "Madagascar", _ .Longitude = 47.4, _ .Lattitude = -18.6 }, _ { .Name = "Belmopan", _ .Country = "Belize", _ .Longitude = -88.5, _ .Latitude = 17.1 }, _ { .Name = "Monaco", _ .Country = "Monaco", _ .Longtitude = 7.2, _ .Latitude = 43.7 }, _ { .Country = "Palau", .Name = "Koror", _ .Longitude = 135, _ .Latitude = 8 } _ }
该示例还使用嵌套对象初始值设定项,嵌套初始值设定项的构造函数从上下文中推断。在这种情况下,每个嵌套初始值设定项完全等效于完整形式的 New City{...}。
通常,我们希望在查询结果中移除或抛出 一类特定成员。例如,我们也许只想知道所有热带首都城市的 Name 和 Country,虽然也使用源数据中的 Latitude 和 Longitude 列标识热带,但是在结果中抛出了这两列。在 Visual Basic 9.0 中要实现这个功能,我们可以为每个纬度位于巨蟹座热带和摩羯座热带之间的城市 C 创建一个新的对象实例,而不用命名类型:
Const TropicOfCancer = 23.5 Const TropicOfCapricorn = -23.5 Dim Tropical = Select New{ .Name = City.Name, .Country = City.Country } _ From City In Capitals _ Where TropicOfCancer =< City.Latitude _ AndAlso City.Latitude >= TropicOfCapricorn
本地变量 Tropical 的推断类型是一种匿名类型实例的集合,即 IEnumerable(Of { Name As String, Country As String })。Visual Basic 编译器将创建一个系统生成的新类,例如 _Name_As_String_Country_As_String_,该类的成员名和成员类型按以下方式从对象初始值设定项中推断:
Class _Name_As_String_Country_As_String_ Public Property Name As String Public Property Country As String Public Default Property Item(Index As Integer) As Object ... End Class
编译器会合并同一个程序内的相同匿名类型。如果两个匿名对象初始值设定项以相同的顺序指定相同名字和类型的属性序列,那么它们将产生同一种匿名类型的实例。表面上,Visual Basic 生成的匿名类型会删除到 Object 中,这样编译器就可以统一将匿名类型作为函数的参数和结果传递。在 Visual Basic 代码内部使用时,编译器将用特殊的自定义属性修饰生成的类,从而记住类型 _Name_As_String_Country_As_String_ 实际上代表匿名类型 { Name As String, Country As String }。
因为匿名类型通常用于抛出一种现有类型的成员,所以 Visual Basic 9.0 允许使用简写投影标记 New { City.Name, City.Country } 作为长格式 New { .Name = City.Name, .Country = City.Country } 的缩写。在查询综合的结果表达式中使用这种简写投影标记时,我们甚至能够按以下方式进一步简化投影初始值设定项:
Dim Tropical = Select City.Name, City.Country _ From City In Capitals _ Where TropicOfCancer =< City.Latitude _ AndAlso City.Latitude >= TropicOfCapricorn
注意,对于前面的长格式而言,这两种缩写形式意义相同。
XLinq 是一个内存中的新 XML 编程 API,它是为利用最新的 .NET Framework 功能(例如语言集成查询框架)而专门设计的。Visual Basic 9.0 通过 XML 文字和 XML 之上的晚期绑定 提供针对 XLinq 的深层次支持,就像查询综合在底层标准 .NET Framework 查询操作符上添加熟悉且方便的语法一样。
为了解释 XML 文字,我们将查询实质上为水平关系的数据源 Countries 和 Capitals,以构造一个具有层次结构的 XML 模型,该模型将每个国家的首都作为一个子元素嵌套,并计算人口密度作为一个属性。
为了查找某一给定国家的首都,我们将每个国家的国名成员和每个城市的国家成员联接 起来。给定了一个国家及其首都,我们就能够将计算出的值填入嵌入表达式空白值 处,从而很容易地构造出 XML 片断。我们可以通过括号将表示名称属性的空白值括起来,例如 Name=(Country.Name),还可以通过从 ASP.NET 借用而来的尖括号将子元素括起来,例如
Dim CountriesWithCapital As XElement = _<%= Select _ From Country In Countries, City In Capitals _ Where Country.Name = City.Country %> <%= City.Name %> <%= City.Longitude %> <%= City.Latitude %>
注意,类型 XElement 在声明中可以省略;如果省略,它将像任何其他本地声明一样进行推断。为了后文立论方便,我们在本示例中保留显式类型。
我们希望,在此声明中,Select 查询的结果在
编译并运行以上查询时,将返回以下 XML 文档(格式稍作调整以减少空行):
Koror 135 8 Monaco 7.2 3.7 Belmopan -88.5 17.1 Antananarivo 47.4 -18.6
Visual Basic 9.0 将 XML 文字编译为正常的 System.Xml.XLinq 对象,以确保 Visual Basic 和其他使用 XLinq 的语言之间的完全互操作。就本示例查询而言,由编译器生成的代码为(假设我们可以看到):
Dim CountriesWithCapital As XElement = _ New XElement("Countries", _ Select New XElement("Country", _ New XAttribute("Name", Country.Name), _ New XAttribute("Density", Country.Population/Country.Area), _ New XElement("Capital", _ New XElement("Name", City.Name), _ New XElement("Longitude", City.Longitude), _ New XElement("Latitude", City.Latitude))) From Country In Countries, City In Capitals _ Where Country.Name = City.Country)
Visual Basic 9.0 除了构造 XML 之外,还通过 XML 上的晚期绑定简化了访问 XML 结构的步骤,换言之,Visual Basic 代码中的标识符在运行时就被绑定到相应的 XML 属性和元素。例如,我们能够按以下方式打印出所有示例国家的人口密度:
1. |
通过子轴 CountriesWithCapital.Country 从 CountriesWithCapital XML 结构中获得所有“Country”元素; |
2. |
通过属性轴 Country.@Density 获得 Country 元素的“Density”属性; |
3. |
通过后代轴 Country...Latitude(在源代码中以三个点顺序书写)获得所有 Country 元素的“Latitude”子元素,无论这些子元素在层次结构中出现的深度如何;以及 |
4. |
通过 IEnumerable(Of T) 上的扩展索引器 选择结果排序中的第一个元素。 |
如果将这些步骤综合到一起,代码如下所示:
For Each Dim Country In CountriesWithCapital.Country Console.WriteLine("Density = "+ Country.@Density) Console.WriteLine("Latitude = "+ Country...Latitude(0)) Next
当声明、赋值或初始化的目标表达式是 Object 类型而非某一种更具体的类型时,编译器“懂得”在正常对象上使用晚期绑定。同样,在目标表达式为 XElement、XDocument、XAttribute 类型或集合时,编译器也“懂得”使用 XML 之上的晚期绑定。
编译器按以下方式翻译 XML 之上的晚期绑定结果:
• | 子轴表达式 CountriesWithCapital.Country 翻译为原始 XLinq 调用 CountriesWithCapital.Elements("Country"),该调用返回 Country 元素的所有名为“Country”的子元素集合; |
• | 属性轴表达式 Country.@Density 转换为 Country.Attribute("Density"),它返回 Country 的名为“Density”的一个子属性;而且 |
• | 后代轴表达式 Country...Latitude(0) 转换为组合形式 ElementAt(Country.Descendants(Latitude),0),它返回 Country 下任意深度的所有命名元素集合。 |
查询综合 提供一种类似于 SQL 的查询语言集成语法,一方面它与 Visual Basic 的外在风格相吻合,另一方面它与新的 .NET 语言集成查询框架无缝集成。
熟悉 SQL 实现的人都可以认出底层 .NET Framework 顺序操作符中许多关系代数的组合操作符,例如代表查询处理器内查询计划的投影、选择、矢量积、组合和排序。
查询综合的语义通过将其转换为顺序操作符进行定义,所以基本操作符将绑定到任何属于顺序操作符范围内的操作符。这意味着如果导入某一个特定的实现,用户将能够有效地重新绑定查询综合的语法。特别是,查询综合能重新绑定到一个使用 DLinq 基础结构或本地查询优化器的顺序操作符实现,该本地查询优化器将尝试在几个本地或远程数据源之间分发查询执行。这种底层顺序操作符的重新绑定与典型的 COM 提供程序模型的实质相似,通过此功能,同一接口的不同实现能够支持千变万化的操作和部署选择而不用覆盖应用程序代码。
基本的 Select...From...Where... 综合筛选出满足 Where 子句中谓词条件的所有值。前面的一个示例说明如何查找所有居民总数少于一百万的国家:
Dim SmallCountries = Select Country _ From Country In Countries _ Where Country.Population < 1000000
在顺序操作符内,标识符 It 绑定到当前“行”。像 Me 一样,It 的成员自动归属到作用域内。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 封装了对一个 Country 是否具有热带气候的测试:
Partial Class Country Function IsTropical() As Boolean Return TropicOfCancer =< Me.Latitude _ AndAlso Me.Latitude >= TropicOfCapricorn End Function End Class
给定该辅助函数后,我们就可以使用与前面完全相同的聚合函数,但首先,要将 Country 和 Capital 对的输入集合按 Country.IsTropical 是否相同划分成组。本示例可以划分为两组:一组包含热带国家 Palau、Belize 和 Madagascar;另一组包含非热带国家 Monaco。
关键字 | 国家 | 城市 |
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() 和一个 Group 中派生,该 Group 包含具有相同关键字提取表达式值的唯一的国家和城市集合。Visual Basic 编译器合成用户定义的聚合函数,给定这样一个分组,通过聚合每个分组,可以计算出所需的结果。
注意,前面示例中的每个 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 9.0 中的查询综合 是完全复合的,这意味着它可以任意嵌套,而且只受静态类型规则的限制。采用复合使理解复杂的查询更容易,因为只要理解每个单独的子表达式即可。同时能更轻松地明确地定义语言的语义和类型规则。作为一种设计原则,它与作为 SQL 设计基础的那些原则截然不同。SQL 语言不是完全复合的,它有许多针对特殊用例的专门设计,这些特殊用例是由数据库在广泛应用中日积月累的经验演变而来的。但是,正是由于缺少完全复合性,使得只了解独立的片断,不可能从整体上理解复杂的 SQL 查询。
SQL 缺少复合性的一个原因是,底层的关系数据模型本身就不是复合的。例如,表不可能包含子表,换句话说,所有的表必须是平面型的。因此,为了与 SQL 的数据模型保持一致,SQL 编程人员只能编写结果为平面表的单一复杂表达式,而不能将其分解成更小的单元。引用 Jim Gray 的一句话,“在计算机科学中,只有递归才是好的。”Visual Basic 基于 CLR 类型系统,因此不限制某些类型作为其他类型的组成部分出现。除了静态类型化规则,它对作为其他表达式组成部分出现的表达式类型没有限制。所以,像行、对象、XML 还有活动目录、文件、注册表项等都是查询源和查询结果中最好的类型。
大部分 .NET Framework 标准查询基础结构的基本功能源自扩展方法。事实上,编译器会将所有查询综合直接转换为由作用域内命名空间定义的标准查询操作符扩展方法。扩展方法是通过自定义属性进行标记的共享方法,这些自定义属性允许方法通过实例-方法语法进行调用。它有效地扩展了现有类型,并通过其他方法构造类型。
扩展方法主要是为那些库设计人员服务的,所以 Visual Basic 不提供针对声明语言语法的直接支持。相反,编程人员直接将所需的自定义属性附加到模块和成员上,从而将两者标记为扩展方法。以下示例定义一个任意集合上的扩展方法Count:
_ Module MyExtensions _ 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 作为输入名是很方便的,但是由于该标识符是一个关键字,必须使用括号进行转义,因此实际上在共享方法中是不允许使用它的。
扩展方法只不过是常规的共享方法,只要显式提供将要操作的实例集合,就可以调用 Count 函数,这同调用 Visual Basic 中任何其他共享函数一样:
Dim TotalSmallCountries = _ MyExtensions.Count(Select Country _ From Country In Countries _ Where Country.Population < 1000000)
扩展方法通过普通的 Imports 语句进入作用域。然后,和其他方法一样,它将以第一个参数赋予的类型出现。
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
许多标准查询操作符都定义为扩展方法,以参数形式代理 Func(Of S,T) 类型,例如 Where、Select、SelectMany 等。为了便于编译器将综合转换为底层查询操作符,或便于 Visual Basic 编程人员直接调用查询操作符,需要简单地创建代理。具体来说,我们需要能够创建所谓的闭包 — 捕捉其环境上下文的代理。Visual Basic 创建闭包的机制通过嵌套的本地函数和子例程声明来完成。
为了说明嵌套函数的使用,我们将调入在 System.Query 命名空间中定义的原始底层查询操作符。扩展方法之一是 TakeWhile 函数,在测试为真时它发送一个序列中的元素,然后忽略该序列中的其余元素。
_ Shared Function TakeWhile(Of T) _ (source As IEnumerable(Of T), Predicate As Func(Of T, Boolean)) _ As IEnumerable(Of T)
OrderByDescending 操作符根据经过证实的排序关键字按降序顺序排列其参数集合:
_ 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 挑选出所有居民总数不足一百万的国家。
Function Population(Country As Country) As Integer Return Country.Population End Function Function LessThanAMillion(Country As Country) As Boolean Return Country.Population < 1000000 End Function Dim SmallCountries = _ Countries.OrderBy(AddresOf Population) _ .TakeWhile(AddresOf LessThanAMillion)
尽管查询综合不是必要的,但是 Visual Basic 也许支持直接使用匿名函数和子例程的语法(即所谓的“lambda 表达式”),编译器会将它们转换为本地函数声明。
关系数据库存在空值语义,但是空值语义通常与普通的编程语言不一致,也不为编程人员所熟知。在数据密集型应用程序中,程序准确无误且清晰明了地处理语义很关键。正是由于认识到了这种必要性,因此在“Whidbey”中,CLR 通过一般类型 Nullable(Of T As Struct) 添加了针对为空性的运行时支持。使用这种类型,我们能够声明可为空的值类型版本,例如 Integer、Boolean、Date 等等。Visual Basic 语法中的空类型是 T?,其原因将变得显而易见。
例如,鉴于并非所有国家都独立,我们将一个新成员添加到类 Country 中,该成员代表各个国家的独立日期(如果可用):
Partial Class Country Public Property Independence As Date? End Class
与数组类型一样,我们也可以在属性名中添加表示可为空值的修饰符,如以下声明所示:
Partial Class Country Public Property Independence? As Date End Class
Palau 的独立日期是 #10/1/1994#,但是英国 Virgin Islands 是大不列颠联合王国的附属领土,因此其独立日期是 Nothing。
Dim Palau = _ New Country { _ .Name = "Palau", _ .Area = 458, _ .Population = 16952, _ .Independence = #10/1/1994# } Dim VirginIslands = _ New Country { _ .Name = "Virgin Islands", _ .Area = 150, _ .Population= 13195, _ .Independence = Nothing }
Visual Basic 9.0 将支持有关空值的三值逻辑和空传播算术,这意味着如果运算中的一个操作数为 Nothing,那么结果将为 Nothing,这样的运算包括算术、比较、逻辑、按位、移位、字符串或类型操作。如果两个操作数都是适当的值,那么将按操作数的基本值执行操作,并将结果转换为可为空。
Palau.Independence 和 VirginIslands.Independence 的类型都是 Date?,所以编译器将使用空传播算术计算下列减法,这样,本地声明 PLength 和 VILength 的推断类型都是 TimeSpan?。
Dim PLength = #8/24/2005# - Palau.Independence REM 3980.00:00:00
因为操作数都不为 Nothing,所以 PLength 的值为 3980.00:00:00。另一方面,虽然 VILength 的结果仍然是 TimeSpan? 类型,但是由于 VirginIslands.Independence 的值为 Nothing,根据空传播法则,VILength 的值将为 Nothing。
Dim VILength = #8/24/2005# - VirginIslands.Independence REM Nothing
与 SQL 中的运算符一样,比较运算符将使用空传播,逻辑运算符将使用三值逻辑。在 If 和 While 语句中,Nothing 被解释为 False,因此,在以下代码片断中,将执行 Else 分支:
If VILength < TimeSpan.FromDays(10000) ... Else ... End If
请注意,根据三值逻辑,等式将检查 X = Nothing,而 Nothing = X 总是计算为 Nothing;为了检查 X 是否为 Nothing,我们应该使用双值逻辑比较 X Is Nothing 和 Nothing Is X。
运行时处理可为空的值,特别是在 Object 中双向进行装箱和取消装箱操作时。当对一个表示 Nothing 的可为空的值(即 HasValue 属性为 False)进行装箱时,该值将装箱到空引用中。当对一个适当的值(即 HasValue 属性为 True)进行装箱时,首先将取消基础值包装,然后再进行装箱。因此,堆上的对象都不是是动态类型 Nullable(Of T);所有可见类型都是 T。而且,我们能够将 Object 中的值取消装箱,设置为 T 或 Nullable(Of T) 类型。但是,这样做将导致晚期绑定不能动态地决定应该使用双值逻辑还是三值逻辑。例如,当我们执行一个两个数的早期绑定加法时,其中一个数为 Nothing,使用空传播后,结果为 Nothing:
Dim A As Integer? = Nothing Dim B As Integer? = 4711 Dim C As Integer? = A+B REM C = Nothing
但是,在这两个值上使用晚期绑定加法时,结果却是 4711,这是由于晚期绑定使用的是双值逻辑,该逻辑依据的事实是:A 和 B 的动态类型都是 Integer,而不是 Integer?。因此 Nothing 被解释为 0:
Dim X As Object = A Dim Y As Object = B Dim Z As Object = X+Y REM Z = 4711
为了确保语义正确,我们需要将编译器定向为使用空传播重载。
Operator +(x As Object?, y As Object?) As Object?
方法是通过“?”操作符将任意一个操作数转换为可为空值的类型:
Dim X As Object = A Dim Y As Object = B Dim Z As Object? = X?+Y REM Z = Nothing
请注意,这意味着我们必须能够将任何类型 T 创建为 T?。底层的 CLR Nullable(Of T As Struct) 类型只强制参数类型为非空结构。当 T 是非空值类型时,Visual Basic 编译器将 T? 清除为 Nullable(Of T);当 T 不是非空值类型时,编译器将其清除为 T。在 Visual Basic 程序内,这两种情况下的静态类型都是 T?,编译器会保留足够的内部元数据来记住这一点。
在 Visual Basic 8.0 中创建一个使用 AddressOf 或 Handles 的委托时,为了绑定到委托标识符,一种方法是必须精确匹配委托类型的签名。在下列示例中,OnClick 子例程的签名必须精确匹配事件处理程序委托 Delegate Sub EventHandler(sender As Object, e As EventArgs) 的签名,该签名在 Button 类型的后台声明:
Dim WithEvents B As New Button() Sub OnClick(sender As Object, e As EventArgs) Handles B.Click MessageBox.Show("Hello World from" + B.Text) End Sub
然而,在调用非委托 函数和子例程时,Visual Basic 并不要求实参精确匹配正在尝试调用的这种方法。我们真正调用的 OnClick 子例程使用的实参类型是 Button 和类型 MouseEventArgs,两者分别是形参 Object 和 EventArgs 的子类型,如以下代码片断所示:
Dim M As New MouseEventArgs(MouseButtons.Left, 2, 47, 11,0) OnClick(B, M)
相反,假设我们定义一个带有两个 Object 参数的子例程 RelaxedOnClick,然后,允许通过类型为 Object 和 EventArgs 的实参调用这个子例程:
Sub RelaxedOnClick(sender As Object, e As Object) Handles B.Click MessageBox.Show("Hello World from" + B.Text)) End Sub Dim E As EventArgs = M Dim S As Object = B RelaxedOnClick(B,E)
在 Visual Basic 9.0 中,绑定到委托放宽为与方法调用一致即可。也就是说,如果可以调用 这样的函数或子例程(实参与形参精确匹配,且该函数或子例程返回委托类型),就可以将其绑定到委托。换言之,委托定义和绑定与方法调用遵循同一个负载解析逻辑。
这意味着,在 Visual Basic 9.0 中,我们现在能够 将带有两个 Object 参数的子例程 RelaxedOnClick 绑定到 Button 的 Click 事件:
Sub RelaxedOnClick(sender As Object, e As Object) Handles B.Click MessageBox.Show(("Hello World from" + B.Text) End Sub
事件处理程序的 sender 和 EventArgs 这两个参数几乎无关紧要。而处理程序访问控件的状态,事件在控件上直接注册,并忽略它的两个参数。为了支持这个常见情况,委托可以放宽为不带参数,只要没有二义性的结果。换言之,我们可以只编写以下这段代码:
Sub RelaxedOnClick Handles B.Click MessageBox.Show("Hello World from" + B.Text) End Sub
在使用 AddressOf 或代理创建表达式,甚至在方法组是一个晚期绑定调用时,也可以应用委托放宽,这一点不难理解:
Dim F As EventHandler = AddressOf RelaxedOnClick Dim G As New EventHandler(AddressOf B.Click)
在纯静态类型化语言中(例如 C#、Java 或带有 Option Strict On 的 Visual Basic),我们只能调用在编译时存在的目标表达式的成员。例如,下面第二个赋值语句理应导致一个编译时错误,因为类 Country 没有 Inflation 成员:
Dim Palau As Country = Countries(0) Dim Inflation = Country.Inflation
但是,在大多数情况下,我们有必要访问这样的成员,即使目标类型的静态类型是未知的。通过 Option Strict Off,Visual Basic 允许晚期绑定成员访问类型为 Object 的目标。尽管晚期绑定功能强大而且非常灵活,但是也付出了一定的代价。特别是,我们失去了智能感知和类型引用,因此需要转换或显式类型化以返回到早期绑定环境。
即使在进行晚期绑定调用时,我们通常也假定值粘附到一个特定的“接口”。只要对象可以满足接口,我们就会很高兴。动态语言社区称之为“鸭子类型”:如果它走起路来像鸭子,说出话来像鸭子,那么它就是一只鸭子。为了说明鸭子类型的概念,以下示例将随机返回一个 Country 或代表一个人的新匿名类型,两者都有一个类型为 String 的 Name 属性:
Function NamedThing() As Object Dim Static RandomNumbers = New Random() Dim I = RandomNumbers.Next(9) If I < 5 NamedThing = Countries(I) Else NamedThing = New{ .Name = "John Doe", .Age = 42-I } End If End Function Dim Name = CStr(NamedThing().Name)
在进行晚期调用 CStr(NamedThing()) 时,我们假定 NamedThing() 返回的静态值有一个类型为 String 的 Name 成员。我们能够通过新功能动态接口 使这个假定显而易见。静态类型为动态接口的目标通常使用晚期绑定进行访问,但是成员访问却是静态类型化的。这意味着我们可以获得完全的智能感知和类型引用,而不必进行任何转换或显式类型化:
_ Interface IHasName Property Name As String End Interface Dim Thing As IHasName = NamedThing() Dim Name = Thing.Name REM Inferred As String.
动态接口借用的现实是:程序员认为自己知道预期在晚期绑定调用中出现的成员的静态名称和签名。然而,在某些真正的动态情况下,我们甚至可能不知道想要静态调用的成员名称。动态标识符考虑到了极端的晚期绑定调用,调用表达式的标识符在该处是动态计算的。
下一个示例以三种不同的语言(英语、荷兰语和法语)声明三个类,每个类都有一个以各自的语言编写的“名字”字段:
Class English Name As String End Class Class Nederlands Naam As String End Class Class Français Nom As String End Class
为了访问 Person 中的这个“名字”字段,我们通过映射值来获得类型名,并查找表中的成员名。然后,我们就能够使用动态标识符访问来调用 Person 中的适当成员:
Dim Dictionary = New Dictionary(Of String, String) { _ .Add("English", "Name"), _ .Add("Nederlands", "Naam"), _ .Add("Français", "Nom") } Dim Person As Object = New Français { .Nom = "Eiffel" } Dim Name As String = Person.(Dictionary(Person.GetType().Name))
Visual Basic 9.0 引入各种各样的新特性。本文,我们以一系列关联的示例阐释了这些新特性,但下面的主题同样值得关注:
• | 关系数据、对象数据和 XML 数据。Visual Basic 9.0 统一了数据访问,使其独立于关系数据库、XML 文档或任意对象图形中的数据源,然而仍然能够在内存中持久保留或存储。这种统一包括类型、技术、工具和编程模式。特别灵活的 Visual Basic 语法使得将扩展(例如 XML 文字和类似 SQL 的查询综合)深入添加到语言内部很容易。这大大地减少了新 .NET 语言集成查询 API 的“表面区域”,通过智能感知和智能标记提高了数据访问功能的可发觉性,通过将外来语法从字符串数据提升到宿主语言中极大地改进了调试。将来,我们打算提高 XML 数据的一致性,甚至进一步借用 XSD 结构。 |
• | 通过所有静态类型化的优点提高动态性。众所周知,静态类型化的优点是:在编译时而不是运行时标识 bug;通过早期绑定访问获得高性能;通过源代码中的显式声明获得清晰的语义等等。但是,有时动态类型化可以使代码更简短、更清晰且更灵活。如果一种语言不直接支持动态类型化,但是程序员需要使用它,那么他们必须通过映射、词典、调度表以及其他技术来实现动态结构的一砖一瓦。这就增加了 bug 出现的可能性,而且提高了维护成本。通过支持可能的静态类型化和需要的动态类型化,Visual Basic 为程序员提供了这两方面的优点。 |
• | 减少程序员的认知负担。像类型推断、对象初始值设定项和宽松的委托等特性极大地减少了代码冗余和异常规则数,这些异常规则往往是程序员需要学习、记忆或查找的,但是对性能却没有影响。诸如动态接口等特性甚至在晚期绑定的情况下也支持智能感知,这就极大地改进了高级功能的可发觉性。 |
尽管 Visual Basic 9.0 新特性列表很长,但我们希望以上主题能够让您确信:使 Visual Basic 成为世界上最杰出的编程系统是一项连续的、及时的、专注的事业。我们也希望它能够激发您的想象力,使您和我们一样认识到这只是一个开端,更伟大的事业即将到来。