11.2 LINQ to Object
凡是使用LINQ查询表达式查询的数据,都属于LINQ to Object。LINQ to Object是指直接对任意IEnumerable或IEnumerable<(Of<(T>)>)集合使用LINQ查询,无须使用中间LINQ提供程序或API,如LINQ to SQL或LINQ to XML。
LINQ to Object可以使用查询任何可枚举的集合,如List<(Of<(T>)>)、Array或Dictionary<(Of<(TKey,TValue>)>)等。该集合可以是用户定义的集合,也可以是.NET Framework API返回的集合。从根本上说,LINQ to Objects表示一种新的处理集合的方法。采用旧方法,必须编写指定如何从集合检索数据的复杂的foreach循环。而如果采用LINQ方法,程序员只需编写描述要检索的内容的声明性代码就可以得到要检索的数据。
11.2.1 了解LINQ子句
查询表达式是由查询关键字和对应的操作数组成的表达式整体。其中,查询关键字包括与SQL对应的子句,这些子句的介绍如表11-1所示。
表11-1 查询关键字和子句
LINQ查询表达式必须以from子句开头,并且必须以select或group子句结尾。在第一个from子句和最后一个select或group子句之间,查询表达式可以包含一个或多个下列可选子句:where、orderby、join、let甚至附加的from子句。还可以使用into关键字使join或group子句的结果能够充当同一查询表达式中附加查询子句的源。
11.2.2 FROM子句
在一个查询中数据源是必不可少的元素。LINQ的数据源是实现泛型接口IEnumerable<T>或者IQueryable<T>的类对象,可以使用foreach遍历它的所有元素,从而完成查询操作。
通过为泛型数据源指定不同的元素类型,可以表示任何数据集合。C#中的列表类、集合类以及数组等都实现了IEnumerable<T>接口,所以可以直接将这些数据对象作为数据源在LINQ查询中使用。
LINQ使用from子句来指定数据源,它的语法格式如下。
from 变量 in 数据源
一般情况下,不需要为from子句的变量指定数据类型,因为编译器会根据数据源类型自动分配,通常元素类型为IEnumerable<T>中的类型T。例如,当数据源为IEnumerable<string>时,编译器为变量分配string类型。
【范例2】
创建一个string类型的数组,然后将它作为数据源使用LINQ查询所有元素。示例代码如下。
string[] Colors = { "Black", "Red", "Yello", "Green" }; //定义string类型数组Colors var result = from color in Colors select color; //LINQ查询所有元素
在上述语句中由于Colors数组是string类型,所以在LINQ查询时的color变量也是string类型。
【范例3】
如果不希望from子句中的变量使用默认类型,也可以指定数据类型。例如,下面的代码将from子句中的变量转换为object类型。
string[] Colors = { "Black", "Red", "Yello", "Green" }; //定义string类型数组Colors var result = from object color in Colors select color; //LINQ查询所有元素
【范例4】
在使用from子句时要注意,由于编译器不会检查遍历变量的类型。所以当指定的数据类型无法与数据源的类型兼容时,虽然不会有语法上的错误,但是当使用foreach语句遍历结果集时将会产生类型转换异常。
示例代码如下。
string[] enOfNum = { "one", "two", "three", "four" }; //定义string类型数组enOfNum var result = from int num in enOfNum select num; //使用int类型变量查询enOfNum数据源 foreach (var str in result) //遍历查询结果的集合 { Response.Write(str); //输出集合中的元素 }
上述代码创建一个string类型的数据源,然后使用int类型的变量来进行查询,由于int类型和string类型不兼容,也不能转换,所以在运行时将会产生异常信息,如图11-2所示。
图11-2 异常信息
注意
建议读者不要在from子句中指定数据类型,应该让编译器自动根据数据源分配数据类型,避免发生异常信息。
11.2.3 SELECT子句
select子句用于指定执行查询时产生结果的结果集,它也是LINQ查询的必备子句。语法如下。
select 结果元素
其中,select是关键字,结果元素则指定了查询结果集合中元素的类型及初始化方式。如果不指定结果的数据类型,编译器会将查询中结果元素的类型自动设置为select子句中元素的类型。
【范例5】
例如,本范例从int类型的数组中执行查询,而且在select子句中没有指定数据类型。此时num结果元素将会自动编译为int类型,因为result的实际类型为IEnumerable<int>。
int[] numbers = { 1, 2, 3, 4}; //创建数据源 var result = from num in numbers select num; //查询数据 Console.SetOut(Response.Output); foreach (var num in result) //遍历结果集合result { Response.Write(num + "|"); //每个集合元素为int类型 }
执行后的查询结果如下。
1|2|3|4|
【范例6】
在范例5中使用select子句获取数据源的所有元素。该子句还可以获取目标数据源中子元素的操作结果,例如属性、方法或者运算等。
例如,本范例从一个包含商品名称、价格和数量的数据源获取商品名称信息。
var Foods = new[] //定义数据源 { new{name="面包", price=5, amount=25}, new{name="饼干", price=6, amount=21}, new{name="饮料", price=3, amount=22}, new{name="瓜子", price=4, amount=24} }; //通过匿名类创建商品列表对象 var foodNames = from foods in Foods select foods.name; //从数据源中查询name属性 Console.SetOut(Response.Output); foreach (var str in foodNames) //遍历结果 { Console.WriteLine("名称:" + str); }
上述代码首先创建一个名为Foods的数据源,在该数据源中通过new运算符创建4个匿名的商品信息对象。每个对象包含三个属性:name(商品名称)、price(价格)和amount(数量)。
LINQ语句从数据源中查询所有对象,每个对象为一个匿名商品信息,select子句从商品信息中提取name属性,即名称信息。运行之后的输出结果如下。
名称:面包 名称:饼干 名称:饮料 名称:瓜子
【范例7】
如果希望获取数据源中的多个属性,可以在select子句中使用匿名类型来解决。以范例6中的数据源为例,现在要查询出商品名称和价格信息,实现代码如下。
var stus = from stu in students select new { stu.name, stu.age }; foreach (var stu in stus) { Response.Write("名称:{0} 价格:{1}<br/>", stu.name, stu.age); }
上述语句在select子句中使用new创建一个匿名类型来描述包含学生姓名和年龄的对象。由于在foreach遍历时无法表示匿名类型,所以只能通过var(可变类型)关键字让编译器自动判断查询中元素的类型。运行之后的输出结果如下。
名称:面包 价格:5 名称:饼干 价格:6 名称:饮料 价格:3 名称:瓜子 价格:4
技巧
通常情况下,不需要为select子句的元素指定具体数据类型。另外,如果查询结果中的元素只在本语句内临时使用,应该尽量使用匿名类型,这样可以减少很多不必要类的定义。
11.2.4 WHERE子句
在实际查询时并不总是需要查询出所有数据,而是希望对查询结果的元素进行筛选。只有符合条件的元素才能最终显示。在LINQ中使用where子句来指定查询的条件,语法格式如下。
where 条件表达式
这里的条件表达式可以是任何符合C#规则的逻辑表达式,最终返回true或者false。当被查询的元素与条件表达式的运算结果为true时,该元素将出现在结果中。
【范例8】
假设要从一个int类型数据源中查询出数字大于4的元素。实现代码如下。
int[] numbers ={ 1, 8, 2, 5,6,3,3,7,10 }; //创建数据源 var result = from num in numbers where num>4 select num; //使用where子句查询大于4的元素 foreach (var num in result) //遍历查询结果 { Response.Write (num + "|"); }
上述代码使用where子句对num进行筛选,只有满足num>4条件的元素会出现在结果集合中。运行结果如下。
8|5|6|7|10|
下面的语句从数据源中查询出大于4的偶数。
var result = from num in numbers where (num%2==0)&&(num>4) select num;
这里使用了与运算符&&连接两个表达式。
【范例9】
从范例6创建的商品信息数据源中执行如下条件查询。
(1)查询价格为5的商品名称和数量。
var result = from food in Foods where f.price==5 select new { food.name, food.amount };
(2)查询价格大于3的商品名称。
var result = from food in Foods where food.price>3 select food.name;
(3)查询价格大于4,并且数量大于10的商品名称。
var result = from food in Foods where (food.price>4) && (food.sex>10) select food.name;
(4)查询价格等于4,或者数量等于10的商品名称、价格和数量。
var result = from food in Foods where (food.price>4) || (food.sex>10) select new {food.name, food.price , food.amount};
11.2.5 ORDERBY子句
LINQ查询使用orderby子句来对结果中的元素进行排序,语法格式如下。
orderby 排序元素 [ascending | descending]
其中,排序元素必须在数据源中,中括号内是排序方式,默认是ascending关键字表示升序排列,descending关键字表示降序排列。
【范例10】
本范例演示使用orderby子句对整型数据源的升序和降序操作,并输出结果。实现代码如下。
int[] numbers = { 1, 8, 2, 5, 6, 3, 3, 7, 10 }; //定义数据源 var result1 = from num in numbers orderby num select num; //使用默认升序排列 Response.Write("默认排序结果是:<br>"); foreach (var num in result1){ Response.Write(num + " , "); } var result2 = from num in numbers orderby num descending select num; //指定降序排列 Response.Write("<br>降序排列结果是:"); foreach (var num in result2){ Response.Write(num + " , "); }
输出结果如下。
默认排序结果是: 1 ,2 ,3 ,3 ,5 ,6 ,7 ,8 ,10 , 降序排列结果是: 10 ,8 ,7 ,6 ,5 ,3 ,3 ,2 ,1 ,
【范例11】
对商品信息按价格降序排列,输出商品名称和价格。代码如下。
var foods = from food in Foods orderby food.price descending select new { food.name, food.price }; foreach (var f in foods) { Response.Write(string.Format("名称:{0}价格:{1}<br>", f.name, f.price)); }
运行后的输出结果如下。
名称:饼干 价格:6 名称:面包 价格:5 名称:瓜子 价格:4 名称:饮料 价格:3
11.2.6 GROUP子句
group子句用于对LINQ查询结果中的元素进行分组,这一点与SQL中的group by子句作用相同。group子句的语法格式如下。
group 结果元素 by 要分组的元素
其中,要分组的元素必须在结果中,而且group子句返回的是一个分组后的集合,集合的键名为分组的名称,集合中的子元素为该分组下的数据。因此,必须使用嵌套foreach循环来遍历分组后的数据。
【范例12】
对商品信息列表中的单位进行分组,输出每组下的商品名称和价格。实现代码如下。
var Foods = new[] //定义数据源 { new{name="面包", price=5, amount=25, quality="袋"}, new{name="饼干", price=6, amount=21, quality="袋"}, new{name="饮料", price=3, amount=22, quality="瓶"}, new{name="瓜子", price=4, amount=24, quality="袋"}, new{name="啤酒", price=5, amount=22, quality="瓶"} }; var foods = from food in Foods group food by food.quality; //对Foods数据源按照quality元素进行分组 foreach (var group in foods) //遍历分组结果 { Response.Write(String.Format("单位为{0}的共{1}个<br>", group.Key, group.Count())); foreach (var f in group) { Response.Write(string.Format("名称:{0} 价格:{1}<br>", f.name, f.price)); } }
上述代码将分组结果保存在foods变量中,此时该变量是一个二维数组。在遍历二维数组时通过调用Key属性获取分组的名称,调用Count()方法获取该组子元素的数量,最后再使用一个foreach循环遍历子元素的内容。
运行后输出结果如下。
单位为袋的共3个 名称:面包 价格:5 名称:饼干 价格:6 名称:瓜子 价格:4 单位为瓶的共2个 名称:饮料 价格:3 名称:啤酒 价格:5
11.2.7 JOIN子句
与SQL一样,LINQ也允许根据一个或者多个关联键来联接多个数据源。join子句实现了将不同的数据源在查询中进行关联的功能。
join子句使用特殊的equals关键字比较指定的键是否相等,并且join子句执行的所有联接都是同等联接。join子句的输出形式取决于所执行联接的类型,联接类型包括:内联接、分组联接和左外联接。
本节用到两个数据源,第一个是图书分类信息如表11-2所示,第二个是图书详细信息如表11-3所示。
表11-2 图书分类信息表
表11-3 图书详细信息表
1.内联接
内联接是LINQ查询中最简单的一种联接方式。它与SQL语句中的INNER JOIN子句比较相似,要求元素的联接关系必须同时满足被联接的两个数据源,即两个数据源都必须存在满足联接关系的元素。
【范例13】
使用内联接将分类编号作为关联查询图书名称、价格和分类名称。实现步骤如下。
(1)创建保存图书分类信息的数据源,代码如下。
var Types = new[] { new {tid=1, tname="软件开发", pid=0}, new {tid=2, tname="数据库", pid=0}, new {tid=3, tname="办公软件", pid=0}, new {tid=4, tname="C#程序设计", pid=1}, new {tid=5, tname="Java程序设计", pid=1}, new {tid=6, tname="SQL Server数据库", pid=2} };
(2)创建保存图书信息的数据源,代码如下。
var Books = new[] { new{bkid=1, bkname="C#入门与提高", price=105, tid=4}, new{bkid=2, bkname="C#程序设计标准教程", price=98, tid=4}, new{bkid=3, bkname="轻松学Java编程", price=58, tid=5}, new{bkid=4, bkname="轻松学C#编程", price=59, tid=4}, new{bkid=5, bkname="轻松学Word软件", price=62, tid=3}, new{bkid=6, bkname="SQL入门与提高", price=55, tid=6} };
(3)创建LINQ查询,使用join子句按tid元素关联分类信息和图书信息,并选择图书编号、价格和分类名称作为结果。实现语句如下。
var result = from bk in Books join t in Types on bk.tid equals t.tid select new { bookName = bk.bkname, bookPrice = bk.price, typeName = t.tname };
上述语句指定以Books数据源为依据,根据tid键在Types数据源中查找匹配的元素。
提示
与SQL不同,LINQ内联接的表达式顺序非常重要。equals关键字左侧必须是外部数据源的关联键(from子句的数据源),右侧必须是内部数据源的关联键(join子句的数据源)。
(4)使用foreach语句遍历结果集,输出每一项的内容。代码如下。
foreach (var item in result) { body.Text+=string.Format("<tr><td>{0}</td><td>{1}</td> <td>{2}</td> </tr>", item.bookName, item.bookPrice, item.typeName); }
(5)运行上述代码,输出结果如图11-3所示。
图11-3 内联接运行效果
2.分组联接
分组联接是指包含into子句的join子句的联接。分组联接将产生分层结构的数据,它将第一个数据源中的每个元素与第二个数据源中的一组相关元素进行匹配。第一个数据源中的元素都会出现在查询结果中。如果第一个数据源中的元素在第二个数据中找到相关元素,则使用被找到的元素,否则使用空。
【范例14】
使用分组联接完成如下查询。
(1)从Types类中查询图书分类信息。
(2)使用join子句联接Types和Books,联接关系为相等(equal),并设置分组的标识为g。
(3)使用select子句获取结果集,要求包含分类名称及其分类下的图书名称。
实现上述查询要求的LINQ语句如下。
var result = from t in Types join bk in Books on t.tid equals bk.tid into g select new { typeName=t.tname, Books=g.ToList() };
上述语句在join子句中通过into关键字将某一分类下对应的图书信息放到g变量中。select子句使用g.ToList()方法将图书信息作为集合放在结果的Books属性中。
使用嵌套的foreach遍历分组及分组中的内容,语句如下。
foreach (var item in result) { body.Text += string.Format("分类名称[ <b>{0}</b> ]下的图书有:<ul>", item.typeName); foreach (var book in item.Books) { body.Text +=string.Format("<li>{0}</li>", book.bkname); } body.Text +="</ul>"; }
执行后的输出结果如图11-4所示。
图11-4 按分类查看图书信息
3.左外联接
左外联接与SQL语句中的LEFT JOIN子句比较相似,它将返回第一个集合中的每一个元素,而无论该元素在第二个集合中是否具有相关元素。
LINQ为左外联接提供了DefaultIfEmpty()方法。如果第一个集合中的元素没有找到相关元素时,DefaultIfEmpty()方法可以指定该元素的相关元素的默认元素。
【范例15】
使用左外联接分类信息和图书信息,并显示分类编号、分类名称和图书信息。查询语句如下。
var result = from t in Types join bk in Books on t.tid equals bk.tid into g from left in g.DefaultIfEmpty( new { bkid = 0, bkname = "该分类下暂无图书", price = 0, tid = t.tid } ) select new { id = t.tid, name = t.tname, bookName = left.bkname };
上述使用left指定使用左外联接,在联接时如果找不到右侧数据源中的匹配元素则使用DefaultIfEmpty()方法创建一个匿名类型作为值。编写foreach语句遍历查询结果,语句如下。
foreach (var item in result) { body.Text+=string.Format("<tr><td>{0}</td><td>{1}</td><td> {2}</td></tr>", item.id, item.name, item.bookName); }
执行后的结果如图11-5所示。
图11-5 左外联接运行效果
注意
左外联接和分组联接虽然相似但是并非一样。分组联接返回的查询结果是一种分层数据结构,需要使用两层foreach才能遍历它的结果。而左外联接是在分组联接的查询结果上再进行一次查询,所以它在join之后还需要一个from子句进行查询。