概述
长久以来,开发人员一直希望能在程式码中使用资料查询技术,就像在 SQL 查询中使用的一样。现在,透过即将推出的 Microsoft® Visual Studio® (代号 "Orcas"),终于得以实现这项功能。下一版 Visual Studio 包含一组新的语言功能,统称为 Language Integrated Query (LINQ),直接将资料查询功能新增到 Visual Basic® 与 Visual C#®。
有了 LINQ,您就可以直接在 Visual Basic 中撰写资料存取程式码。您将可以使用编译时期语法及结构描述检查,其中还提供更好的工具支援 (例如 IntelliSense®) 让您用于撰写查询。您不再需要在字串中撰写查询,也不需要到执行阶段才能确定查询的语式是否正确。
使用 LINQ 即可从基础储存设备抽象撷取资料存取程式码。这表示您可以使用相同的程式码构造,来查询关联式资料库、XML 及记忆体中物件,也可以轻松地连结不同来源网域上的资讯。例如,您可以建立以 XML 储存的客户姓名清单与储存在资料库资料表的客户姓名清单之间的等位。此外,LINQ 也允许延迟执行,好让您将查询分割成数个逻辑部分,而且仍能保有仅需执行一次即可取得最终结果的效能优势。
最后,Visual Basic/XML 整合几乎消弭了您所撰写的程式码与您尝试表达或处理的文件之间在概念上的隔阂。我们现在就开始来练习如何撰写查询。
LINQ 语法
您可以使用 LINQ 在各种来源上执行查询,这些来源包括 Microsoft .NET Framework 物件集合、关联式资料库或 XML 资料来源。Visual Basic 中的一般 LINQ 查询格式如下所示:
From ... < Where ... Order By ... Select ... >
当然,若您是 SQL 程式设计人员,对于查询以 From 而非 Select 开头可能会感到不解。原因很简单:IntelliSense。透过先指出资料来源,IntelliSense 可以显示您的集合中的物件上的适当型别成员清单。在输入 Select 子句时,这点很重要。
在下列各节中,我们将检视一些标准 LINQ 子句,并观察这些子句如何相互建置以便建立稳定、灵活的查询。我们将依序进行下列步骤:From 子句、Select 子句、使用查询结果、Where 子句,以及 Order By 子句。
在下面的范例中,有时会提到客户 (Customer) 或订单 (Order)。架设这些范例的定义如 [代码 1] 所示。每个 Customer 都透过 Orders 属性来参考自己的订单集合。如此可建立这两个实体间的简单关联。此关联性是由每个 Order 中的 CustomerID 外部索引键所构成。除了这些型别定义之外,还有储存在 customers 变数中的 Customers 集合。
代码1:客户和订单
Public customers As List(Of Customer) Public Class Customer Public Property CustomerID As Integer Public Property FirstName As String Public Property LastName As String Public Property City As String Public Property Orders As List(Of Order) End Class Public Class Order Public Property OrderID As Integer Public Property CustomerID As Integer Public Property OrderDate As DateTime End Class
From 子句是 LINQ 的核心,因为每次查询都需要用到这个子句。每个查询都必须以 From 子句开始,而且它是唯一需要明确提供的子句。From 子句遵循下列基本模式:
From <query variable> In <datasource>
基本上,From 代表逐一检查一组资料。进行这个动作需要有一个对应到来源中各个资料项目的变数,很类似 For Each 陈述式中使用的反覆运算变数。From 陈述式后面的子句,可以透过参考查询变数来筛选、排序或处理资料。
以下是一个很简单的 From 子句范例:
Dim numbers As Integer = {1, 7, 4, 2, 91, 12, 23, 37, 42} Dim allNumbers = From number In numbers For Each num In allNumbers Console.Write(num & “ “) Next
在这个范例中,查询会传回型别为 IEnumerable(Of Integer) 的集合。有关这个查询还有一些需要注意的重点。首先,Select 是隐含的,因此在范例中看不到它。Select 陈述式是选择性项目;若未提供 Select 陈述式,便会传回范围中的一组变数。第二点是,此查询并不是非常好用。您可以移除查询,直接使用 For Each 迴圈来反覆运算数字变数。LINQ 的威力在此尚未完全展现。不过请看一下这个查询:
From <query variable> As Type In <datasource>
Dim checkboxes = From checkbox As Checkbox In groupJobType.Controls
这个 From 陈述式的变体可让您指定查询变数后面的型别陈述式。如果您想从特定来源撷取型别,然后将这些型别重新指定为更具体 (或更抽象) 的型别,这个变体就很有用。我们刚好知道 groupJobType GroupBox 的控制项全都是 Checkbox 控制项,因此可以在撷取这些控制项时放心地将它们分类为 Checkbox。
From 子句还有另一个使用两次 From 关键字的变体:
From <query variable> In <datasource> From <let variable> = <value>
From <query variable> In <datasource>, <let variable> = <value>
Dim evens = From Number In numbers From IsEven = (Number Mod 2 = 0) Dim evens = From Number In numbers, IsEven = (Number Mod 2 = 0)
这个查询语法实际上都是同一个 From 子句,后面的 From 子句部分称为 From 的 "Let" 部分。本质上我们是在定义第二个变数并为其指定特定值。在这个范例中,我们会在称为 "IsEven" 的结果集上定义第二个值 (它是布林值),也就是我们选取的数字是否为偶数的结果。当然,这表示传回的内容已改变:它不再只是 Integers 的 IEnumerable,因为传回的每个可列举项目,都同时包含一个数字和一个布林值。我们将在下文看到如何使用查询的结果。请注意,在一般的语法运算式中,可以省略第二个 From 关键字并使用逗号代替;亦即上述两个范例是相同的。
Let 变数与标准查询变数的差异在于,指派给 Let 变数的值一定会使用 =,而查询变数则是在 In 关键字前面。不过,Visual Basic 小组正积极寻求公开 Let 变数的方法,因此,从现在开始到最终发行产品的这段时间内,Let 的支援语法很有可能会变更:
From <queryVar1> In <datasource1>, <queryVar2> In <datasource2>, ...
Dim customerOrders = From cust In customers, ord In cust.Orders
此语法可让您指定多个资料来源。在上面的范例中,我们取得客户 (customers) 与订单 (orders) 的集合;基本上,这算是两个来源的隐含联结 (Join)。在大多数的情况下,这些来源不会这么轻易地相互关联 (第一个查询变数上不会有第二个查询变数),因此必须使用 Where 子句来建立关联。但在这个范例中,我们的 Orders 集合实际上是位在每个 cust 执行个体上;因此我们只会得到套用至特定客户的订单。
Select 子句
目前您已经看过如何指定资料来源。要注意的是,每个范例都省略了 Select 关键字。原因只是我们沿用预设的 Select 行为,也就是选取 (Select) 每个查询或指定的 Let 变数 (逻辑上等于 Select *)。但我们也可以指定 Select 子句,这样将大幅提高使用弹性:
Select <varA>, <varB>, ...
Dim allNumbers = From number In numbers _ Select number Dim evens = From Number In numbers From IsEven = (Number Mod 2 = 0) _ Select Number, IsEven Dim customerOrders = From cust In customers, ord In cust.Orders _ Select cust, ord
这是最简单的 Select 用法。比较一下之前的查询与此处 From 区段中的相同对应项查询。在这三个范例中,包含明确 Select 子句的各个查询的行为,与之前 Select 为隐含的查询一模一样。
不过如果您尝试不同的变数,那么使用 Select 会更加有趣:
Dim customerInfos1 = From cust In customers _ Select cust.FirstName, cust.LastName Dim customerInfos2 = From cust In customers, ord In cust.Orders _ Select cust.LastName, ord.OrderID, ord.OrderDate
首先请注意,此处并未指定新的一般语法 (范例上没有註解)。这是因为在这些范例中,您会看到之前的程式码採用相同样式:毕竟 FirstName 和 LastName 都只是变数。但是与之前范例不同的是,我们解除参考了查询变数,只传回一些属性。这个功能非常强大。
LINQ 还有一个很棒的功能,就是当您不需要重新命名栏位时,Visual Basic 会根据属性名称来推断栏位名称。不过有时候您可能会要重新命名栏位或合併资料,如下所示:
Select <aliasA> = <varA>, <aliasB> = <varB>, ...
Dim customerInfos1 = From cust In customers, ord In cust.Orders _ Select Date = ord.OrderDate, _ Name = cust.FirstName & cust.LastName
在某些情况下 (像是 Date = ord.OrderDate),您可能是出于个人喜好而这么做。但在其他情况下 (例如 Name = cust.FirstName & cust.LastName),您其实是打算建立一组新资料 -- 进行栏位的组合。使用别名还有第三种情况。假设您选取来自不同来源的两个栏位,但这两个栏位的名称刚好一样。名称推断会根据产生的型别,来指定相同的属性名称给这些变数,这将会产生冲突而导致编译失败。您可以针对单一 (或两个) 冲突的栏位名称使用别名来解决此冲突。
关于 Select 子句最后一点需要注意的是,它会重新指定变数范围,所以查询中的所有子句都只会看到 Select 公开的内容。我们将在讨论 Where 子句的区段进一步详细说明。
使用查询结果
现在我们来看看如何使用查询结果。首先我们必须了解执行查询后会产生什么:
Dim allNumbers = From number In numbers _ Select number
任何查询的结果都一定是 IEnumerable(Of T)。这是查询最可靠且有效的优点之一,因为这表示您永远都可以反覆运算资料。有时候您会知道 T 的型别,有时则不知道 (例如在匿名型别的情况下)。
匿名型别是 LINQ 的新功能,是用来代表包含多个传回栏位资料行的查询结果。匿名型别是指您无法在程式码中直接命名或参考的型别,但是编译器会依照其他命名型别的相同方式处理这些型别 (编译器会指定型别名称,但您不会知道此名称,也不需要知道)。匿名型别的栏位名称可以在程式码中直接指定,或由编译器从查询运算式自动推断。例如,使用匿名型别就可以实现您在前文看到的程式码,在此程式码中的 {Num, IsEven} 与 {cust, ord} 配对,实际上就是新的型别,各自拥有两组资料 (分别是 Num, IsEven 与 Cust, Ord 等可存取栏位名称):
Dim customerInfos1 = From cust In customers _ Select cust.FirstName, cust.LastName Dim customerInfos2 = From cust In customers, ord In cust.Orders _ Select cust.LastName, ord.OrderID, ord.OrderDate
如您所见,匿名型别可让您在 Select 子句中指定不同的栏位数,您无须事先指定代表每个特定栏位组合的确实型别。
虽然说每个查询都是 IEnumerable(Of T),您可能已经发现并没有任何范例实际指定代表查询的变数型别。您可能会忧虑变数逐渐变成 System.Object 的型别,而且在整个过程中,慢慢失去所有强型别支援。
其实不然。称为型别推断 (Type Inference) 的新编译器功能,是指编译器可以根据初始化数值来推断以不含型别规范宣告的变数型别 (前提是您必须在程式码顶端加入陈述式 Option Infer On)。例如,下列变数会根据初始化数值来静态指定型别:
Dim x = 5 ‘ x is typed as an Integer Dim y = DateTime.Now ‘ x is typed as a DateTime Dim z = GetCustomer() ‘ assuming GetCustomer returns a customer ‘ type, z is typed as a Customer x = “hello” ‘ COMPILE-TIME ERROR: x can only store Integers
同样地,代表查询的变数是强型别。这可让您实际使用直接位于变数上的成员,而无须担心他们是晚期繫结的成员。此外,您还是保有 IntelliSense 支援等其他强型别优点。当新的变数宣告不包含型别规范时,For Each 陈述式也会採取同样的型别推断:
‘ assuming allNumbers is a collection of Integers, ‘ the type of num is inferred to be an Integer For Each num In allNumbers
一般而言,使用查询时往往不容易知道会传回哪种特定物件类型。这时候您就应该信赖查询。私底下,很多不同类型的物件都可能用来储存您的查询结果。重要的是查询结果一定是 IEnumerable(Of T),因此您可以使用 For Each 或是将结果传送至可接受 IEnumerable 物件的 API,以进行反覆运算。
然而,您可以记住一条重要的经验法则,它将协助您确知传回的内容 (原因马上揭晓)。当您传回的结果是由单一命名型别构成,则查询会传回 IEnumerable(Of <您的型别>)。当您传回的结果包含多个型别,则查询会传回 IEnumerable(Of <匿名型别>)。因此在只会传回来自 Integers 阵列的一个整数的上述范例中,您将会得到 IEnumerable(Of Integer)。
当查询结果是单一型别的 IEnumerable 时,使用结果很容易。只要使用 For Each 子句即可:
For Each num In allNumbers Console.WriteLine(num) Next
既然只有单一型别的 IEnumerable,因此这段程式码应该没有什么出人意表的部分。现在我们来看看涉及匿名型别的情况:
Dim evens = From Number In numbers From IsEven = (Number Mod 2 = 0) _ Select Number, IsEven For Each numInfo In evens Console.WriteLine(“Is “ & numInfo.Number & “ even? “ & _ numInfo.IsEven) Next
由于查询传回的物件包含两组资料,因此变数 "evens" 是 IEnumerable(Of <匿名型别>)。我们取得的每个实体都具有命名为 Num 与 IsEven 的栏位;因此,当我们反覆运算 evens 集合时,必须解除参考每一组资料以便撷取这些资料 (例如 numInfo.Num)。这里有几点需要注意。首先,我们命名反覆运算变数 numInfo。反覆运算匿名型别集合时,使用一般名称来表示它具有多组资料是不错的做法。其次,我们在范例中以大写表示查询变数,如此一来,当我们解除参考并在 For Each 内部使用这些变数时,它们看起来就像标准属性一样。在之前的范例中我们不需要对 "number" 执行这个动作,因为当时并未引入匿名型别。
让我们再看一个范例,您就知道这有多简单:
Dim customerInfos = From cust In customers, ord In cust.Orders _ Select Name = cust.FirstName & cust.LastName, _ Date = ord.OrderDate For Each custInfo In customerInfos Console.WriteLine(“Customer Name: “ & custInfo.Name & _ “ Order Date: “ & custInfo.OrderDate) Next
真是再简单不过了。不过,有时您可能想要单独撷取名字和姓氏栏位。以下是上述程式码的变体,可保留 Customers 与 Orders (稍后可使用其他栏位) 中的所有资料,同时又达到这个反覆运算的同样效果。
Dim customerInfos = From cust In customers, ord In cust.Orders _ Select Customer = cust, Order = ord For Each custInfo In customerInfos Console.WriteLine(“Customer Name: “ & _ custInfo.Customer.LastName & “, “ & _ custInfo.Customer.FirstName) Console.WriteLine(“Order Date: “ & custInfo.Order.OrderDate) Next
这里一样没什么特别的。藉由在结果中包含各个完整物件,我们必须解除参考物件以便取得要使用的特定栏位。请注意,我们串连了 For Each 迴圈内部的姓氏和名字。
我们再给您一个提示。看一下查询中可用的成员,您也可以找到一些很酷的用法。例如,请看下面的程式码:
Dim customerInfos = From cust In customers, ord In cust.Orders _ Select Customer = cust, Order = ord Console.WriteLine(“There are “ & customerInfos.Count & _ “ customer orders”)
这个范例直接在查询结果上使用 Count,以查看它有多少项目。我们将不一一细述在集合中可以找到的成员,但别忘了看一下您可以如何运用结果集。另外还要注意,从查询产生的所有集合都可以使用这些成员。如果您猜想这表示这些成员都必须位于 IEnumerable(Of T) 上,您猜的没错;甚至连实作都必须位于 IEnumerable(Of T) 上。这是称为「延伸方法」的新功能。
关于执行查询的方式,您必须了解延迟执行这个概念。在延迟执行中,直到实际使用资料时才会尝试从查询撷取资料。这点很重要,因为它可让您撰写不同部分的查询,而不需要对底层资料来源多次唿叫,好处是能大幅提升效能,尤其是存取资料库的资讯时 (在这种情况中尤其要避免多次唿叫)。
也就是说,查询的宣告实际上并不会撷取资料;只有进行需要资料的动作时,才会执行资料的撷取。您最可能採取的这种动作,通常是反覆运算查询。不过其他可能的动作还包括显示资料,或尝试使用 Count 等方法来寻找查询中的实体数目。[代码2] 显示执行资料撷取的场合。
Dim seattleCustomers = From cust In customers _ Where cust.City = “Seattle” Dim recentOrders = From ord In orders _ Where ord.OrderDate.Year = 2007 Dim custInfos = From cust In seattleCustomers, ord in recentOrders _ Where cust.CustomerID = ord.OrderID _ Select Name = cust.LastName & “, “ & cust.FirstName, _ cust.CustomerID, ord.OrderDate _ Order By OrderDate Descending, Name ‘ None of the above queries are run until this point. ‘ This means that the information is only retrieved once. For Each custInfo In custInfos ‘ take action on the data here Next
使用 Where 子句进行筛选
Where 子句可让您筛选特定条件,让查询功能更加强大。Where 子句的运作方式与 If 陈述式一样。亦即 If 陈述式可以做的,Where 子句也行:
Where <condition> Dim evens = From num In numbers _ Where num Mod 2 = 0 _ Select num
请留意这个集合称为 "evens",正如之前的范例一样。不过与之前范例不同的是,它只包含偶数。当您反覆运算结果时,将得到偶数的结果:
Dim mids = From num In numbers _ Where num > 10 And num < 50 _ Select num
Where 子句之美在于其简洁性。<condition> 是指您选择的任何条件。我们来看一下这个子句在更复杂的查询中如何运作:
Dim seattleCustomers = From cust In customers, ord In cust.Orders _ Where cust.City = “Seattle” _ Select Customer = cust, Order = ord Dim seattle2003Orders = From cust In customers, ord In cust.Orders _ Where cust.City = “Seattle” _ And ord.OrderDate.Year = 2003 _ Select Customer = cust, Order = ord
有时您可能想要根据某个栏位进行筛选,并选取不同的栏位。做法非常直接:
Dim dallas2005Orders = From cust In customers, ord In cust.Orders _ Where cust.City = “Dallas” _ And ord.OrderDate.Year = 2005 _ Select cust.CustomerID, ord.OrderID
宣告 From 子句之后,其他子句的顺序就无所谓 -- 您可以依照自己喜好的顺序来使用:
Dim seattle2003Orders = From cust In customers, ord In cust.Orders _ Select Customer = cust, Order = ord _ Where Customer.City = “Seattle” _ And Order.OrderDate.Year = 2003
比较这个范例与上述相同名称的范例。您会发现我们将 Select 子句移到 Where 子句之前,而且这个动作完全没有问题。但是也请注意,Where 子句必须稍加变更,它现在参考 Customer 与 Order 而非 cust 与 ord。要特别小心的是,Select 子句是重新指定范围子句,因此 Select 子句之后的子句都只能看到 Select 公开的内容,亦即下面的程式码将会无效:
‘ This won’t compile Dim dallas2005Orders = From cust In customers, ord In cust.Orders _ Select cust.CustomerID, ord.OrderID _ Where cust.City = “Dallas” _ And ord.OrderDate.Year = 2005
我们参考 Where 子句中的 ord 与 cust,但是 Where 在 Select 后面,因此这两个变数都消失了。在 Select 之后,只剩下包含两个属性 CustomerID 与 OrderID 的匿名型别。程式码可以变更如下:
Dim dallas2005Orders = From cust In customers, ord In cust.Orders _ Select cust.CustomerID, ord.OrderID, _ cust.City, ord.OrderDate _ Where City = “Dallas” _ And OrderDate.Year = 2005
请注意,包含 Where 及 Order By 在内的许多子句,都不会重新指定可用变数的范围。
使用 Order By 进行排序
Order By 是所有查询共有的基础子句。Order By 会根据指定栏位来排序查询结果:
Order By <query variable>
这是最简单的 Order By 用法。若未指定排序方向,则 Order By 子句会假定您要採用递增顺序。以下为子句的运用情形:
Dim nums = From num In numbers _ Order By num _ Select num
Order By 的下一个变体与第一个雷同。只是它假设您要根据变数内部的栏位而非变数本身来进行排序:
Order By <field1>, <field2>, ... Dim customerInfos1 = From cust In customers, ord In cust.Orders _ Order By cust.City _ Select Customer = cust, Order = ord Dim customerInfos2 = From cust In customers, ord In cust.Orders _ Order By cust.City, ord.OrderDate _ Select Customer = cust, Order = ord
选取一或多个栏位都可以。此外,由于 Order By 并不会重新指定可用变数的范围,因此之后的 Select 子句,可以使用来自前一个范围的任何可用变数:
Order By <var1> [Ascending/Descending], _ <var2> [Ascending/Descending], ... Dim customerInfos = From cust In customers, ord In cust.Orders _ Order By cust.City Descending, ord.OrderDate _ Select Customer = cust, Order = ord
这充分发挥了 Order By 的功用。我们省略了第二个栏位的方向,但在第一个栏位中有指定,也就是说,首先会以递减的方式依照城市来排序,接着会以递增的方式依照订单日期来排序。
当然,您可以结合 Order By 与 Where,我们现在就来看几个范例当做总结:
Dim orderedEvens = From num In numbers _ Where num Mod 2 = 0 _ Order By num _ Select num Dim seattleCustomers = From cust In customers, ord In cust.Orders _ Order By cust.City Descending, ord.OrderDate _ Where City = “Dallas” _ And OrderDate.Year = 2005 _ Select Name = cust.LastName & “, “ _ cust.FirstName, ord.OrderDate
其他子句
其他可用子句的相关讨论属于另一篇文章的范畴,不过许多标准查询功能均受支援,就像本文提到的这些子句一样。例如,联结 (Join)、汇总 (Aggregation) 及群组 (Grouping) 都有支援。您已经在前文的范例中看过隐含联结,不过使用 Join 关键字也可以执行明确联结。使用 Aggregate 子句可支援汇总资讯,而结合使用 Group By 子句与 Sum 或 Average 等内建的汇总方法 (您也可以建立及使用自己的方法),就能支援群组功能。诸如插入 (Insert)、更新 (Update) 和删除 (Delete) 等标准查询功能也都支援,只是不能直接在查询语法中使用。
结论
LINQ 提供基本查询功能,包括指定来源 (From)、识别传回的资料 (Select)、筛选 (Where) 以及排序 (Order By)。另外也可使用分组 (Group)、联结 (Join) 或汇总 (Aggregate) 资料集等进阶查询功能。有了这些功能,再加上匿名型别或型别推断等其他功能的辅助,即万事俱全,您就可以开始在 Visual Basic 程式码中直接撰写类似 SQL 的查询了。
最后
以上就是粗心项链为你收集整理的Visual Basic LINQ入门的全部内容,希望文章能够帮你解决Visual Basic LINQ入门所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复