Getting Started With LINQ in Visual Basic (翻译 + 评论)
使LINQ可行的VB9.0语言特性
类型推断(自动类型设定)
类型推断延续了VB的传统,为静态变量在可能的情况下、为动态变量在需要的时候推测类型。在VB9里面,编译器不需要为所有的数据显式的声明类型,而是定义变量的同时,通过其初始化的表达式来推测变量的类型。例如下面的代码:
Dim population = 131000
Dim name = "
Dim area = 1.9
Dim someNumbers = {4, 18, 11, 9, 8, 0, 5}
For Dim i = 0 To 10
Console.WriteLine(i)
Next
将鼠标移动到变量上面,你会看到这些变量的类型已经被推测成你所想要的类型了。
Dim population As Integer = 131000
Dim name As String = "
Dim area As Double = 1.9
Dim someNumbers() As Integer = {4, 18, 11, 9, 8, 0, 5}
For i As Integer = 0 To 10
Console.WriteLine(i)
Next
注意,类型推侧实在编译时完成的,而不是使用晚期绑定。
你可以看到在此文的后续部分,通过类型推侧你可以不用声明类型而直接定义查询返回值。
(译注)
可以使用一个常量进行类型推断,比如
Dim aInt = 5 ‘Integer
Dim aDbl = 1.2 ‘Double
Dim aStr = "farrio" ‘String
Dim aChr = "A"c ‘Char
也可以声明数组
Dim aArr1() = {1, 2, 3, 4, 5}
Dim aArr2() = {"1", " 2", "3", " 4", " 5"}
数组如果不是一个类型的话,将会尽可能的无损转化为可能的类型。
Dim aArr3() = {1, 2, 3, 5, 5, 6.2} ‘Double()
但是如果数组的初始化部分不能自动转化成一个类型的时候,将会自动推测为他们的基类。
Dim aArr3() = {1, "2", 3, "5", 5, 6.2} ‘Object()
动态推测只支持局部变量,而不支持成员变量。
Private a = 123 ‘Runtime: Int32 Compile-Time: Object
匿名类型
VB9可以自动推测已定义的类型变量(如前例所示)。并且,他还能够创建一个并没有被声明过的类型的实例。因为这个新的类型从来都没有被声明过,所以是“匿名类型”。
匿名类型建立在另一个VB9的新特性的基础之上——对象初始化器。使用对象初始化器,你可以使用一些表达式来初始化一个复杂的对象实例。例如,如果你看了Visual Basic 9 Survey工程里面Object Initializers这部分的内容,你能够看到一个名叫Customer的类定义,包括Name、Address和City属性。你可以用很多办法来初始化这个类的实例。比如说,你可以完整地指定这个对象的类型以及所有成员的值。
Dim cust1 As Customer = New Customer {Name := "
Address := "
类似的,你也可以只初始化一部分成员而其他成员使用其默认值。
也可以在等号的右面省略掉类型来让编译器自动推断这个类型。
Dim cust1 As Customer = {Name := "
或者你也可以省略掉等号左边的声明类型。
Dim cust1 = New Customer {Name := "
对象初始化器和匿名类型结合在一起,在后面的部分将会看到应用到查询工程的例子。再举一个简单的例子,如果你想创建一个Product类包含一个Name和一个Price属性,你可以定义一个包含Name和Price成员的Product类,然后像前面Customer的那个例子那样实例化,或者你也可以这样:
Dim someProduct = New {Name := "paperclips", _
Price := 1.29}
要去验证一下编译器确实是按照你的要求建立了一个包含两个成员的实例,输入
Console.WriteLine(someProduct.
使用智能感知技术显示所包含的两个属性(你可以看到Name和Price)。如果你将鼠标悬停在someProduct变量上面,你可以看到他被定义为了一个匿名类型的实例。而且Name和Price分别被定为String和Double类型。变量someProduct是一个强类型对象,在编译时就已经确定了类型和成员,只不过编译器是通过他的初始化部分来建立的。
在3.7和3.8节里面,描述基于LINQ的查询表达式结构的时候还可以看到这部分的内容。默认类型允许你从原始类型集合中创建一个包含一部分成员(而不是全部成员)的新类型集合,或者将多个集合合并在一起。使用投影,你可以选择输入数据中所关心的部分,并且让VB将他们打包在一个类里面。
(译注)
匿名类型优先于对象初始化器。比如一个Person类,包含Name和Age属性。如果我们定义
Dim p = New {Name := "xuzy", Age := 27}
那么得到的不是自动推测的Person对象,而是一个匿名对象。
只有我们在等号的一方加入Person声明(左右任何一方均可)就可以自动推测出Person实例了。
另外,相同成员的匿名类型是相同的,比如
Dim p1 = New {Name := "xuzy", Age := 27}
Dim p2 = New {Name := "yinxb", Age := 25}
他们是同一个类型的。p2 = p1操作是可以的。
匿名类型也仅支持局部变量,如果是一个成员变量,比如
Private b = New {Name := "xuzy", Age := 27}
在运行时是一个匿名类型,但是编译时是Object对象。虽然可以通过
Console.WriteLine(b.Name)
来得到成员,但是那时晚期绑定了。
LINQ查询表达式
查询表达式的语法
首先看一个查询整形数组的例子,来看看VB9的查询。
这是数组
Dim someNumbers = {4, 18, 11, 9, 8, 0, 13, 21, 5}
这是查询语句
Dim oneDigitNumbers = From n In someNumbers _
Where n < 10 _
Select n
这是结果
Console.WriteLine("One-digit numbers from the list:")
For Each Dim m In oneDigitNumbers
Console.Write(m & " ")
Next
注意在声明someNumber以及oneDigitNumbers的时候使用的类型推测。
正如你所看到的,一个基本的查询表达式由From…Where…Select组成。如果你将鼠标悬停在变量上面,你可以看到在查询语句中,n被定义为Integer,oneDigitNumbers是IEnumerable(Of Integer)类型——基本上是一个数组集合。
正如上面的例子所示,For…Each…Next方法可以很方便的遍历(迭代)查询表达式的结果集合中(或者所有实现了IEnumerable接口的集合对象)的每一个元素。
(译注)
LINQ的查询语句和SQL有所不用。
首先是From语句,他定义了查询的对象集合并且推断出每一个查询元素的类型。
From n In someNumbers
这里someNumbers是查询的对象集合,由于是Integer数组,所以n被推测为Integer类型。
其次是Where子句,由于From子句里面已经声明了n,所以这里就可以使用了。
最后是Select子句,表示要返回的类型。我们可以这样
Select New {BaseValue := n, DoubleValue := n * 2}
所有的查询表达式返回值都是IEnumerable接口实例,只不过根据Select子句的不同,他的范型类型不同而起。使用动态推测就可以省掉用户判断返回值类型的麻烦而直接查询结果。所以查询表达式返回值对于使用者来说是透明的,我们可以不用去关心它。
用Order By排序
想要为结果排序,在Select子句后面使用Order By。
'From ... Select ... Order By ...
Dim inOrder = From n In someNumbers _
Select n _
Order By n
'From ... Where ... Select ... Order By ...
Dim oneDigitNumbersInOrder = From n In someNumbers _
Where n < 10 _
Select n _
Order By n
Order By默认使用降序排序。你也可以使用Ascending和Descending关键字来特别指明升序还是降序。
Order By n Ascending
Order By n Descending
(译注)
Order By字句里面执行排序的字段只能是出现在Select里面的字段,或者是Select里面字段的某些成员(属性、方法等)。这一点和C# LINQ不太一样。例如
Dim oneDigitNumbers = From n In someNumbers _
Where n < 10 _
Select n _
Order By n
但是如果我们这样写
Dim oneDigitNumbers = From n In someNumbers _
Where n < 10 _
Select n.ToString _
Order By n
就会出错,编译器提示“n没有定义”。不知道这是不是VB LINQ的一个BUG。
但是如果
Dim oneDigitNumbers = From n In someNumbers _
Where n < 10 _
Select n _
Order By n.ToString
是没有问题的。
合计操作符
针对数值的合计操作符引入LINQ中,下面的例子中使用Count方法来做个例子。合计操作符作用于一个集合,将每一个元素合并到一个值中,比如为集合的数据进行合计、计数操作。
Dim oneDigitCount = Count(From n In someNumbers _
Where n < 10 _
Select n)
或者
Dim oneDigCt = (From n In someNumbers _
Where n < 10 _
Select n).Count()
Console.WriteLine("Number of one-digit numbers: " & _
oneDigitCount)
其他的合计操作符有Min、Max、Sum和Acerage,使用方法和Count一样。请参考追加实例Sample Queries project,101 LINQ Query Samples和Aggregate Operators。
(译注)
实际上合计操作符是定义在System.Query.Sequence类里面的一系列静态方法。
通过“扩展方法”的办法将其扩展到所有IEnumerable接口的实例上。我们可以直接使用这个静态方法,也可以使用IEnumerable的扩展方法。
分割操作符
分割操作符允许你从查询返回集合中只抽取一部分,或者除了某些部分之外的子集。
使用Take(i)可以返回查询结果中的前i个记录。(类似于SQL里面的TOP关键字)
Dim takeFive = (From n In someNumbers _
Select n).Take(5)
使用Skip(i)可以返回查询结果集合中前i项以外的数据。
Dim skipFive = (From n In someNumbers _
Select n _
Order By n).Skip(5)
指定元素操作符
使用指定元素操作符,可以从查询结果中选择一个特定的元素。
First操作符返回结果中的第一个元素。
Dim firstElement = (From n In someNumbers _
Where n > 20 _
Select n).First()
ElementAt(i)方法返回结果中的第i个元素。由于结果集合的下标从0开始,所以下面的例子里面返回的是第四个元素。
Dim fourthElement = (From n In someNumbers _
Select n _
Order By n).ElementAt(3)
(译注)
要注意,指定元素操作符将会返回所迭代的类型而不是IEnumerable对象。
而且,如果返回的集合为空集(没有元素),那么使用First方法将会引发一个名为ArgumentInvalidateException的异常。
如果ElementAt的参数超过了集合的最大下标,将会返回OutOfRangeException。
使用结构化数据的例子
下面的例子是用了一个小的Customer实例的集合,这个实例的定义可以在Visual Basic 9 Survey project里面找到。
每一个Customer对象包含一个Name,一个Address(街道地址)和一个City属性。还有一个能够返回当前顾客列表的方法GetCustomers,一个子程序ObjectInitializers用来建立和现实这个列表。
这个例子的操作和3.1-3,5节类似,但是操作的对象集合不是整形了。
首先,一个From…Where…Select的例子。查询所有地址里面包含“123”的用户。
' Here is the customer list
Dim customers = GetCustomers()
' Here is the query
Dim custs = From cust In customers _
Where cust.Address.Contains("123") _
Select cust
' Here is the result -- display their names
Console.WriteLine("Customers with 123 in their addresses:")
For Each Dim c In custs
Console.WriteLine(" Name: " & c.Name)
Next
再次注意,编译器正确的推断了customers、custs和c的类型。你可以使用智能感知去验证他们的类型。
刚才的例子是用了一个直接的投影操作,选择符合要求的元素并且返回。同样的,也可以写一个单值投影来返回一个缩小的对象,从原来的数据中只得到唯一的一个成员。下面的例子只返回了符合要求的元素的Name属性。
Dim custNames = From cust In customers _
Where cust.Address.Contains("123") _
Select cust.Name
Console.WriteLine("Customers with 123 in their addresses:")
For Each Dim cname In custNames
Console.WriteLine(" Name: " & cname)
Next
如果你将鼠标悬停在cname变量上面,你可以看到他被推测为String类型。你也可以看看custs和custNames的类型。
参看3.7节多值投影的例子。
使用Order By可以非常简单进行排序。下面的查询语句用Name的值进行排序。
Dim custsInOrder = From cust In customers _
Where cust.Address.Contains("123") _
Select cust _
Order By cust.Name
Console.WriteLine("Customers with 123 addresses, in order:")
For Each Dim c In custsInOrder
Console.WriteLine("Name: " & c.Name)
Next
你也可以使用连续的值进行排序。试试这个,先用City排序再用Name排序。
(注意,不能对Select以外的对象作为排序的字段。也即是说,我们目前还无法投影cust.Name,并且用cust.City排序。)
Dim custsInOrder = From cust In customers _
Select cust _
Order By cust.City,cust.Name
下面例子里面的合计操作符和简单数据的使用方法、执行结果一样。当然你首先要决定哪些对象要被比较、合计或者平均。
Dim firstCity = (From cust In customers _
Select cust.City).Min()
' or alternatively
'Dim firstCity = Min (From cust In customers _
' Select cust.City)
Console.WriteLine ("The first city, alphabetically, is " & _
firstCity)
下面的这个例子得到了City属性集合并且取其中的第一个值。
Dim startCity = (From cust In customers _
Select cust.City).First()
这个例子取得除了第一个值以外的其它值。
Dim allButFirst = (From cust In customers _
Select cust.City).Skip(1)
最后,这个例子用了ElementAt取返回集合中的第二个元素。
Dim secondCity = (From cust In customers _
Select cust.City).ElementAt(1)
使用匿名类型进行多值投影
匿名类型可以用来从现有类型进行多值投影操作(参照3.6节的单值投影,以及2.2节的匿名类型介绍)。例如,在Visual Basic 9 Survey例子工程里面,你可能希望从Customer类里面得到一个包含街道地址和城市的实例列表,但是不包含Name信息。使用匿名类型你可以有很多的途径来实现。
建立一个附带名字的匿名类型。(为Customer里面的Address和City在匿名类型里面建立两个新的属性名字)
Dim customerLocs = From cust In customers _
Select New {theStreet := cust.Address, theCity := cust.City}
将设定值所属的属性作为匿名类的的属性。(参见3.8节)
Dim customerLocs = From cust In customers _
Select New {cust.Address, cust.City}
最后,用一个简短的查询表达式实现。
Dim customerLocs = From cust In customers _
Select cust.Address, cust.City
下面的例子里面,想要显示在customerLocs列表里面cLoc.Name属性将会引发错误,因为这个成员已经不属于返回的元素的。但是cLoc.Address和cLoc.City仍然有效。
Console.WriteLine("City names in customerLocs: ")
For Each Dim cLoc In customerLocs
'Console.WriteLine(cLoc.Name)
Console.WriteLine(" " & cLoc.City)
Next
下面的例子更能显示这一特性,一个查询系统当前进程的程序。System.Diagnostics.Process类里面诸多的公共属性,也许你只关心ProcessName和ID这两个信息。如果是这样,你就可以建立一个只包含着两个成员的新集合。
Dim tasks = From proc In _
System.Diagnostics.Process.GetProcesses() _
Where proc.Threads.Count > 10 _
Select proc.ProcessName, proc.ID
Console.WriteLine("Processes:")
For Each Dim task In tasks
Console.WriteLine(" Name : " & task.ProcessName & _
" ID: " & task.ID)
Next
使用只能感知去检测Task.ProcessName,他是一个String类型属性,ID则是Integer类型,并且Task是一个匿名类型。这个例子还突出了一个新的信息:你可以查询所有的集合类型。GetProcesses方法返回一个进程集合对象,你可以想查询任何其它集合对象一样查询他。
(译注)
如前所述,Order By子句只能查询Select子句里面的项目,如果Select子句是一个匿名类型,那么我们将无法进行Order By操作。但是在C# 3.0里面是可以的。
使用it关键字(参见3.9节)可以解决这个问题。It关键字表示这次查询操作的迭代器对象,也就是我们最终要返回的结果集合的成员类型。例如
Dim ps = From p In persions _
Select p.Name, p.Age _
Order By it.Age Descending, it.Name Ascending
这里的it就是Select子句返回的匿名类型。
从多个数据源结合数据
你可以使用LINQ查询表达式方便的将多个数据源的数据结合在一起,运用起来就和从单一数据源取得数据一样的简单。下面的例子将所有customers数据里面,地址信息包含在一个oneDigitNumbers数组(在3.1节里面创建的)里面的成员取出来。
Dim custsWMatchingDigit = From cust In customers, n In oneDigitNumbers _
Where cust.Address.Contains(n.ToString()) _
Select cust
Console.WriteLine("Customers with digit match in their addresses:")
For Each Dim c In custsWMatchingDigit
Console.WriteLine(" " & c.Name)
Next
你也可以方便的再返回信息里面加入另一个数据源的内容。
Dim custsWMatchingDigit = From cust In customers, n In oneDigitNumbers _
Where cust.Address.Contains(n.ToString()) _
Select cust,n
Console.WriteLine("Customers with digit match in their addresses:")
For Each Dim custn In custsWMatchingDigit
Console.WriteLine(" " & custn.cust.Name & ", " & custn.n)
Next
注意,Name属性在两个显示操作里面的代码是不一样的,因为custsWMatchingDigit成员在两个例子里面是两个不同的结构。第一个例子返回的是customer的集合,包含Address和City属性,但是第二个例子里面,返回的却是一个包含了cust和n属性的匿名类型。
再上一个例子里面,oneDigitNumbers和customers都实现了IEnumerable接口,但这并不是必需的。如果你用someNumbers,一个整数数组来替换oneDigitNumbers,程序仍旧能够正常运行。
在Form子句里面有一个自动的迭代顺序。上一个例子是通过一个数值列表内的值来对customer的地址信息进行检索。下面的例子,通过所有的customer的地址和整数列表进行检索。虽然得到结果列表的成员是一样的,但是顺序却是不一样的。
Dim digitsWMatchingCusts = From n In oneDigitNumbers, cust In customers _
Where cust.Address.Contains(n.ToString()) _
Select cust
Console.WriteLine("With the iterator for digits first:")
For Each Dim c In digitsWMatchingCusts
Console.WriteLine(" " & c.Name)
Next
(译注)
使用上面的检索,将会产生笛卡尔乘积。相同的项目将会重复出现。比如在number数组里面有1,2,3这几个成员,在customer的address中有一个address同时包含了1,2,3,那么将会在结果集合中返回三个这个customer对象。
但是使用下面的方法可以避免这个现象。
首先考虑这样两个类,Person和Department。
Public Class Department
Private m_dCode As String
Private m_dName As String
Public Property Code() As String
Get
Return Me.m_dCode
End Get
Set(ByVal value As String)
Me.m_dCode = value
End Set
End Property
Public Property Name() As String
Get
Return Me.m_dName
End Get
Set(ByVal value As String)
Me.m_dName = value
End Set
End Property
End Class
Public Class Person
Private m_Name As String
Private m_Age As Integer
Private m_Department As String
Public Property Department() As String
Get
Return Me.m_Department
End Get
Set(ByVal value As String)
Me.m_Department = value
End Set
End Property
Public Property Name() As String
Get
Return Me.m_Name
End Get
Set(ByVal value As String)
Me.m_Name = value
End Set
End Property
Public Property Age() As Integer
Get
Return Me.m_Age
End Get
Set(ByVal value As Integer)
Me.m_Age = value
End Set
End Property
Public Overrides Function ToString() As String
Return String.Format("Name: {0}{1}Age: {2}{1}Department: {3}", Me.m_Name, vbTab, Me.m_Age, Me.m_Department)
End Function
End Class
以及他们得到数组的方法
Function GetPersonList() As Person()
Return New Person() { _
New Person {Name := "xuzy", Age := 27, Department := "1"}, _
New Person {Name := "zhangkun", Age := 24, Department := "QA"}, _
New Person {Name := "yinxb", Age := 25, Department := "1"}, _
New Person {Name := "Diablo", Age := 30, Department := "1"}, _
New Person {Name := "hehui", Age := 26, Department := "1"}, _
New Person {Name := "Happy2007", Age := 29, Department := "3"}, _
New Person {Name := "loulou", Age := 29, Department := "1"}, _
New Person {Name := "pangzi", Age := 29, Department := "1"}}
End Function
Function GetDepartmentList() As Department()
Return New Department() { _
New Department {Code := "1", Name := "1部"}, _
New Department {Code := "2", Name := "2部"}, _
New Department {Code := "3", Name := "3部"}, _
New Department {Code := "QA", Name := "QA部"}}
End Function
实际的操作如下
Dim persions = GetPersonList()
Dim dps = GetDepartmentList()
Dim ps = From d In dps, p In persions _
Where p.Department = d.Code _
Select New {Person := p.Name, Age := p.Age, Code := p.Department, Dp := d.Name}
For Each p In ps
Console.WriteLine(p.ToString)
Next
实际上是一个左右连接的例子,通过Where p.Department = d.Code连接两个数据源,如果不匹配则不输出。这样就消除了笛卡儿积。
分组操作
Gourp By允许你使用某一个标准对输入集合的元素进行分组,并且允许你访问每一个子组。现在的Group By使用一个表示迭代器变量的上下文关键字it。由Groupt By子句,it关键字表示查询结果集合中的迭代元素。例如,下面的查询将someNumbers数组分组为奇数和偶数,并且为每一个组记数。
Dim oddEven = From n In someNumbers _
Group By n Mod 2 _
Select it.Key, Count(it)
Console.WriteLine("Count of even and odd integers in someNumbers:")
For Each Dim g In oddEven
Console.WriteLine("Key: " & g.Key & " Count: " & g.Count)
Next
Console.WriteLine()
显示的结果如下:
Key: 0 Count: 4
Key: 1 Count: 5
Key项目也可以不是数字。下面的例子将customers按照city进行分组。Key是City名字。(字符串)
Dim cityCount = From cust In customers _
Group By cust.City _
Select it.Key, Count(it)
Console.WriteLine("Number of times each city occurs in customers:")
For Each Dim ct in cityCount
Console.WriteLine(ct.Key & ", " & ct.Count)
Next
Console.WriteLine()
注意,输出分组信息的cities的顺序和输入顺序保持一致。如果你想排序,可以在查询表达式的后面使用Order By子句。
Dim cityCount = From cust In customers _
Group By cust.City _
Select it.Key, Count(it) _
Order By it.Key
分组Key也可以是一个匿名类型。下面的例子扩展了3.8节的例子,从customer里面选择address属性中包含oneDigitNumbers整形数组元素,使用一个包含了City和Name属性的匿名类型来分组,最后将每一组里面有多少成员进行投影。
Dim grp = From c In customers, n In oneDigitNumbers _
Where c.Address.Contains(n.ToString()) _
Group By New {c.City, c.Name} _
Select it.key, Count(it)
Order By it.key.City
Console.WriteLine("Results with anonymous type:")
For Each Dim g In grp
Console.WriteLine(" Key: {0}, Occurrences: {1}", g.key, g.Count)
Next
(译注)
实际上,包含了Group By子句的查询表达式的返回值是一个IGrouping接口的实例,而且它还实现了IEnumerable接口。考虑下面的代码
Dim nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
Dim odd = From n In nums _
Group By n Mod 2 _
Select it
在这里,it返回的迭代元素就是IGrouping。通过
For Each Dim g In odd
Console.WriteLine("Key: " & g.Key)
Next
就可以将所有的组的信息表示出来。(IGrouping里面只有一个Key属性,它返回当前组的分组关键字类型的对应值。)
我们还可以进一步得到每个组下面的内容,比如
For Each Dim g In odd
Console.WriteLine("Key: " & g.Key)
For Each Dim _g In g
Console.WriteLine(_g)
Next
Next
这时候,_g的类型就是Integer了。我们可以将分组操作看成是两层集合的嵌套,外层是所有得到的足的集合,内层是每个组里面包含的元数据的集合。