查询内存对象
了解Object LINQ
本章包括:
■ LinqBooks 运行示例
■ 集合查询
■在ASP.NET和Windows Forms中使用LINQ
■ 主要的标准查询操作符
在本书的剩余部分的代码示例是一个书籍分类系统。在本站的开始,我们会对此进行描述。
本章讲述的大部分LINQ特性只是Object LINQ的特性。我们会关注如何编写语言集成的查询以及如何使用标准查询操作符。本章的目标是使你熟悉主要标准查询操作符,让你轻松完成对内存对象的查询。
4.1 运行示例介绍
当我们介绍语言新特性和LINQ概念的时候,我们使用了简单的代码示例。我们不能应对更复杂的现实情况。所以,我们的新示例基于一个不断更新的系统:LINQBooks,一个个人书籍分类系统。
4.1.1 Goals
一个可运行的示例让我们举例的代码可信。我们选择开发一个充分使用LINQ的程序作为示例。
下面是本示例的需求:
n 对象模型必须足够丰富以支持LINQ查询
n 必须能够单独或者一起处理内存对象,XML文档和关系数据
n 应该包括ASP.NET web站点以及WinForm程序
n 应该包含本地数据查询和外部数据查询,如公共web服务。
下面将会介绍我们要开发的特性。
4.1.2 Features
LINQBooks的主要特性包括:
n 跟踪现在有书籍
n 存储书籍信息
n 获取书籍信息
n 发布书籍列表和评论信息
本书介绍的技术特性包括:
n 在本地数据库中查询、插入、更新信息
n 提供在本地目录或者第三方目录中的查询能力
n 从web站点导入书籍数据
n 导出并将一些数据存储在Xml文档中
n 对你喜欢的书籍创建RSS反馈
为了实现这些特性,我们使用一个业务实体的集合。
4.1.3 业务实体
我们使用的对象模型包含以下类:Book, Author, Publisher, Subject, Review和User.
图4.1是对象关系类图
我们首先使用LINQ操作内存对象,稍后会把这些数据存储到数据库中,现在让我们看看使用的数据库模型。
图 4.1 运行示例的对象模型
4.1.4 数据库模式
在本书的第三部分,我们将会阐述LINQ如何操作关系数据库,图4.2显示我们使用的数据库模式。
图4.2 运行示例的数据库模式
我们将使用此数据库保存应用程序使用的数据。这个数据库模式包含很多不同种类的关系和数据类型,这对我们阐述SQL LINQ会有很大帮助。
4.1.5 示例数据
在本部分,我们将使用一些内存对象集合数据来阐述对象LINQ。
列表4.1包含的SampleData类包含了我们需要的数据。
列表4.1 SampleData 类提供了示例数据
using System;
using System.Collections.Generic;
using System.Text;
namespace LinqInAction.LinqBooks.Common
{
static public class SampleData
{
static public Publisher[] Publishers ={
new Publisher {Name="FunBooks"},
new Publisher {Name="Joe Publishing"},
new Publisher {Name="I Publisher"}};
static public Author[] Authors = {
new Author {FirstName="Johnny", LastName="Good"},
new Author {FirstName="Graziella", LastName="Simplegame"},
new Author {FirstName="Octavio", LastName="Prince"},
new Author {FirstName="Jeremy", LastName="Legrand"}};
static public Book[] Books = {
new Book {Title="Funny Stories",
Publisher=Publishers[0],
Authors=new[]{Authors[0],Authors[1]},
PageCount=101,
Price=25.55M,
PublicationDate=new DateTime(2004, 11, 10),
Isbn="0-000-77777-2"},
new Book {Title="LINQ rules",
Publisher=Publishers[1],
Authors=new[]{Authors[2]},
PageCount=300,
Price=12M,
PublicationDate=new DateTime(2007, 9, 2),
Isbn="0-111-77777-2"},
new Book {Title="C# on Rails",
Publisher=Publishers[1],
Authors=new[]{Authors[2]},
PageCount=256,
Price=35.5M,
PublicationDate=new DateTime(2007, 4, 1),
Isbn="0-222-77777-2"},
new Book {Title="All your base are belong to us",
Publisher=Publishers[1],
Authors=new[]{Authors[3]},
PageCount=1205,
Price=35.5M,
PublicationDate=new DateTime(2005, 5, 5),
Isbn="0-333-77777-2"},
new Book {Title="Bonjour mon Amour",
Publisher=Publishers[0],
Authors=new[]{Authors[1], Authors[0]},
PageCount=50,
Price=29M,
PublicationDate=new DateTime(1973, 2, 18),
Isbn="2-444-77777-2"}
};
}
为了更好的初始化集合,我们使用集合初始化器。
4.2 使用LINQ 查询内存集合
4.2.1 我们可以查询什么?
Object LINQ并不是可以查询任何对象。他需要集合的支持。对象LINQ可以查询的集合必须实现了IEnumerable<T>接口,在LINQ词汇表中,实现此接口的集合,我们称之为序列。好消息是.NET中几乎任何泛型集合都实现了IEnumerable<T>接口。
让我回顾一下对象LINQ可以查询的对象集合。
数组
任何类型的数组都支持LINQ,可以是对象数组,如列表4.2中所示:
列表 4.2 使用Object LINQ查询弱类型数组
using System;
using System.Linq;
static class TestArray
{
static void Main()
{
Object[] array = {"String", 12, true, 'a'};
var types = array
.Select(item => item.GetType().Name)
.OrderBy(type => type);
ObjectDumper.Write(types);
}
}
这段代码显示按数组元素类型名称排序的数组元素的类型。这是这段代码的输出:
Boolean
Char
Int32
String
当然,自定义对象也可以使用LINQ查询。列表4.3中,我们对Book对象数组进行了查询。
列表 4.3 使用Object LINQ查询强类型数组
using System;
using System.Collections.Generic;
using System.Linq;
using LinqInAction.LinqBooks.Common;
static class TestArray
{
static void Main()
{
Book[] books = {
new Book { Title="LINQ in Action" },
new Book { Title="LINQ for Fun" },
new Book { Title="Extreme LINQ" } };
var titles =books
.Where(book => book.Title.Contains("Action"))
.Select(book => book.Title);
ObjectDumper.Write(titles);
}
}
实际上,对象LINQ查询可以使用任何类型的数组。
其他重要的集合,如泛型列表和词典,也可以进行对象LINQ查询。
泛型列表
.NET2.0中最常用的集合是泛型的List<T>。对象LINQ可以像操作其它泛型列表一样操作List<T>。
下面是主要的泛型列表类型的列表:
n System.Collections.Generic.List<T>
n System.Collections.Generic.LinkedList<T>
n System.Collections.Generic.Queue<T>
n System.Collections.Generic.Stack<T>
n System.Collections.Generic.HashSet<T>
n System.Collections.ObjectModel.Collection<T>
n System.ComponentModel.BindingList<T>
列表4.4显示了与泛型列表的LINQ操作
列表 4.4 使用Object LINQ查询泛型列表
using System;
using System.Collections.Generic;
using System.Linq;
using LinqInAction.LinqBooks.Common;
static class TestList
{
static void Main()
{
List<Book> books = new List<Book>() {
new Book { Title="LINQ in Action" },
new Book { Title="LINQ for Fun" },
new Book { Title="Extreme LINQ" } };
var titles =books
.Where(book => book.Title.Contains("Action"))
.Select(book => book.Title);
ObjectDumper.Write(titles);
}
}
可以看到,查询没有变化,因为数组和列表都实现了查询用到的相同的接口:IEnumerable<Book>
我主要针对数组和列表编写查询,但是有时我们也会针对泛型词典编写查询。
泛型词典
像泛型列表一样,对象LINQ也可以操作所有的泛型词典:
n System.Collections.Generic.Dictionary<TKey,TValue>
n System.Collections.Generic.SortedDictionary<TKey, TValue>
n System.Collections.Generic.SortedList<TKey, TValue>
泛型词典实现了IEnumerable<KeyValuePair<TKey, TValue>>接口,KeyValuePair结构保存了类型化的键和值。
列表4.5显示了我们如何查询一个整数索引的字符串词典。
列表 4.5 使用Object LINQ查询泛型词典
using System;
using System.Collections.Generic;
using System.Linq;
static class TestDictionary
{
static void Main()
{
Dictionary<int, string> frenchNumbers;
frenchNumbers = new Dictionary<int, string>();
frenchNumbers.Add(0, "zero");
frenchNumbers.Add(1, "un");
frenchNumbers.Add(2, "deux");
frenchNumbers.Add(3, "trois");
frenchNumbers.Add(4, "quatre");
var evenFrenchNumbers =
from entry in frenchNumbers where (entry.Key % 2) == 0
select entry.Value;
ObjectDumper.Write(evenFrenchNumbers);
}
}
示例的执行结果如下:
zero
deux
quatre
我们列出了可以查询的最重要的集合,你还可以查询其他集合,如下:
String
虽然System.String看上去不是一个集合,但是它实现了IEnumerable<Char>。这表示字符串对象可以使用Object LINQ查询。
举例如下。4.6中的LINQ查询使用了字符串的字符数组特性。
列表 4.5 使用Object LINQ查询字符串
var count = "Non-letter characters in this string: 8".Where(c => !Char.IsLetter(c)).Count();
无需多说,结果是8
其他集合
我们只列出了.NET Framework提供的集合,当然,我们可以使用对象LINQ处理任何实现IEnumerable<T>接口的集合。这表示对象LINQ同样可以处理其他框架的集合类型。
问题是并不是所有的.NET集合都实现了IEnumerable<T>接口,实际上,只有强类型集合实现了这个接口,数组,泛型列表和泛型词典是强类型的。
非泛型集合没有实现IEnumerable<T>接口,但是实现了IEnumerable接口,这是不是意味着是不是我们不能使用LINQ查询ArrayList和DataSet对象呢?
幸运的是,有一个办法,在5.1.1节中。我们将阐述我们如何查询非泛型集合。使用Cast和OfType查询操作可以做到这点。让我们学习一下LINQ是如何对所有这些集合进行操作的。
4.2.2 支持的操作
在我们列出的这些类型中,都支持标准查询操作符。LINQ的提供的操作符可以进行序列操作和组合查询。
下面是标准查询操作符的总览:
Restric- tion,
Projection,
Partitioning,
Join,
Ordering,
Grouping,
Set,
Conversion,
Equal- ity,
Element,
Generation,
Quantifiers,
Aggregation
如你所见,LINQ支持大量的操作符。我们不会一一详述,而是详述最重要的操作符。
标准查询操作符在System.Linq.Enumerable类中,作为IEnumerable<T>的扩展方法提供。这些操作符被称作标准查询操作符,因为我们可以提供自定义的操作符。因为查询操作符仅仅是IEnumerable<T>类型的扩展方法,所以我们可以方便的创建我们自己的查询操作符,丰富我们的查询。
很快,我们将阐述查询操作符支持的操作。为了创建我们的示例应用程序,我们将使用LINQ创建我们的ASP.NET站点和WinForm应用程序。
4.3 在ASP.NET 和Windows Forms中使用LINQ
在前面的章节中,我们在控制台程序中使用了LINQ代码。这对于简单的示例足够了, 但是大多数真正的项目以ASP.NET或Windows程序实现的,而不是控制台程序。
4.3.1 web程序的数据绑定
ASP.NET控件支持绑定到任何实现IEnumerable的集合上。这使得在使用在GridView, DataList, 和Repeater 上使用LINQ时变得简单。
让我一步一步的完成我们的web站点。
步骤0:创建ASP.NET web 站点
为了创建新的ASP.NET web站点,在Visual Studio中选择File > New > Web Site,并选择ASP.NET Web站点模板。如图4.3所示:
图 4.3创建新的ASP.NET站点
图 4.4 web站点的默认内容
这就创建了如图4.4所示的web站点
我们将创建一个新页面并显示一些数据。
步骤1:使用LINQ创建ASP.NET 页面
创建一个名为Step1.aspx 的页面,并添加一个GridView 控件如列表4.7所示:
列表 4.7 第一个ASP.NET 页面的标记
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Step1.aspx.cs" Inherits=" Step1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Step 1</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="GridView1" runat="server">
</asp:GridView>
</div>
</form>
</body>
</html>
列表4.8包含你需要写在代码后置文件中的查询代码。
列表 4.8 第一个ASP.NET页面的代码后置文件
using System;
using System.Linq;
using LinqInAction.LinqBooks.Common;
public partial class Step1 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
String[] books = { "Funny Stories",
"All your base are belong to us",
"LINQ rules",
"C# on Rails",
"Bonjour mon Amour" };
GridView1.DataSource =
from book in books
where book.Length > 10
orderby book
select book.ToUpper();
GridView1.DataBind();
}
}
确保你引用了System.Linq命名空间。
这里我们使用查询表达式语法。查询选择了书名大于十个字符的书籍,然后将书籍按照字符排序,并将书名转换为大写字符。
LINQ查询返回IEnumerable<T>类型的结果,T取决于select字句的对象类型。在本书中,书籍是一个字符串,所以查询结果是IEnumerable<String>类型
因为ASP.NET支持绑定到任何IEnumerable集合之上,我们可以轻松对GridView控件赋值LINQ查询。然后调用DataBind方法。
程序运行结果如图4.5所示:
图 4.5 ASP.NET 步骤1结果
就是如此,我们使用LINQ创建了我们第一个ASP.NET web站点。没有遇到任何困难,下面让我们改进一下我们的示例。
步骤2: 使用丰富的集合
为了让我们的示例程序更加有用,我们将提供针对集合的查询能力。好消息是LINQ使这项工作变得简单。
我们使用示例数据来运行程序。例如,我们可以查询书籍并按价格排序。我们希望做到图4.6所示的结果。
图 4.6 在ASP.NET中使用复杂集合的结果
这次我们显示了价格信息,Title和Price是Book对象的两个属性。Book对象拥有更多的属性,如图4.7所示。
图 4.7 Book类
我们可以使用两个方法来只显示我们想显示的属性:在GridView层级上指定列,或者只选择Title和Price属性。
让我们实现前者的功能。
为了使用Book类和示例数据,首先需要添加对LinqBooks.Common项目的引用,然后创建一个名为Step2a.aspx的页面,并创建一个只定义两列的GridView控件。如列表4.9所示:
列表 4.9 Step2a.aspx的标记内容
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Step2a.aspx.cs" Inherits="Step2a" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Step 2 – Grid columns</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="GridView1" runat="server"
AutoGenerateColumns="false">
<Columns>
<asp:BoundField HeaderText="Book" DataField="Title" />
<asp:BoundField HeaderText="Price" DataField="Price" />
</Columns>
</asp:GridView>
</div>
</form>
</body>
</html>
列表4.10显示对示例数据进行查询的LINQ查询。
列表 4.10 代码后置文件Step2a.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
GridView1.DataSource =
from book in SampleData.Books
where book.Title.Length > 10
orderby book.Price
select book;
GridView1.DataBind();
}
请确保使用了System.Linq命名空间
GridView显示了指定的属性信息,而忽略其他属性信息。
前面说到,我们有另一种完成此目标的方法,如列表4.11所示:
列表 4.11 使用匿名类型的代码后置文件Step2b.aspx.cs
using System;
using System.Linq;
using LinqInAction.LinqBooks.Common;
public partial class Step2b : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
GridView1.DataSource =
from book in SampleData.Books
where book.Title.Length > 10
orderby book.Price
select new { book.Title, book.Price };
GridView1.DataBind();
}
}
如你所见,我们使用了匿名类型。匿名类型可以让你简单的创建内联结构。我们创建了具有两个属性的匿名对象,属性值将会被自动初始化。
这次,由于匿名类型,我们不用再显式在GridView中指定列了。如列表4.12所示:
列表 4.12 列表 4.11中的标记(Step2b.aspx)
...
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="true">
</asp:GridView>
</div>
</form>
</body>
</html>
两种限制显示列信息的方法都是有用的。第一种方法允许我们为列指定其他选项,如列头信息。第二种方法允许我们只选择需要的数据,而不是所有数据。在SQL LINQ中,这非常有用,它避免了我们从数据库中获取多余的数据。
使用匿名类型更重要的的好处是避免了创建只为展示对象而创建的新类型。在一些简单的情况下,可以使用匿名类型映射域模型到展示模型上。下面的查询中,创建匿名类型允许我们的域模型保持在一个平面视图上:
from book in SampleData.Books
where book.Title.Length > 10
orderby book.Price
select new {
book.Title,
book.Price
Publisher=book.Publisher.Name,
Authors=book.Authors.Count() };
这里我们使用匿名对象保持对象数据和对象与其它对象之间关系的数据。
4.4 重要的标准查询操作符
为回顾,表4.1显示了所有的标准查询操作符
表 4.1 The standard query operators grouped in families
家 | 查询操作符 |
Filtering | OfType, Where |
Projection | Select, SelectMany |
Partitioning | Skip, SkipWhile, Take, TakeWhile |
Join | GroupJoin, Join |
Concatenation | Concat |
Ordering | OrderBy, OrderByDescending, Reverse, ThenBy, ThenByDescending |
Grouping | GroupBy, ToLookup |
Set | Distinct, Except, Intersect, Union |
Conversion | AsEnumerable, AsQueryable, Cast, ToArray, ToDictionary, ToList |
Equality | SequenceEqual |
Element | ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault |
Generation | DefaultIfEmpty, Empty, Range, Repeat |
Quantifiers | All, Any, Contains |
Aggregation | Aggregate, Average, Count, LongCount, Max, Min, Sum |
4.4.1 Where, 约束操作符
像筛子一样,Where操作符基于一定标准过滤一个序列,Where只返回那些匹配提供的谓词条件的值。
下面是Where操作符的声明:
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source, Func<T, bool> predicate);
第一个参数表示要测试的序列。Predicate参数返回的布尔值表示给定值是否满足测试条件。下面的示例创建了一个价格大于等于15的Book序列。
IEnumerable<Book> books =
SampleData.Books.Where(book => book.Price >= 15);
在查询表达式中,一个where子句转换为一个Where操作符的调用。前面的示例与下面的查询表达式等价:
var books =
from book in SampleData.Books
where book.Price >= 15
select book;
一个重载的Where操作符使用了带有元素索引值参数的谓词:
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source, Func<T, int, bool> predicate);
谓词的第二个参数,表示序列中元素以零为开始的索引值。
下面的代码段产生了一个价格大于等于15且书籍索引是奇数的书籍序列:
IEnumerable<Book> books = SampleData.Books
.Where((book, index) => (book.Price >= 15) && ((index & 1) == 1));
Where是一个限制操作符,很简单但是使用频繁。另一个经常使用的操作符是Select。
4.4.2 使用投影操作符
让我们回顾一下下面两个投影操作符:Select和SelectMany
Select
Select操作符用来对一个序列进行投影,Select方法声明如下:
public static IEnumerable<S> Select<T, S>(
this IEnumerable<T> source, Func<T, S> selector);
Select操作符根据源序列和选择器函数产生新的序列,下面的示例创建了所有书籍的书籍名称序列:
IEnumerable<String> titles = SampleData.Books.Select(book => book.Title);
在查询表达式中,select子句转换为Select调用。下面的查询表达式与上例相同:
var titles = from book in SampleData.Books select book.Title;
该查询只保留了字符串值 。我们可以选择一个对象,下面我们选择书籍相关的Publisher对象:
var publishers =
from book in SampleData.Books select book.Publisher;
下面的示例中,我们使用了匿名类型来映射书籍的信息到一个对象中:
var books =
from book in SampleData.Books
select new { book.Title, book.Publisher.Name, book.Authors };
这种代码创建了数据的投影,所以才有了投影操作符的命名。下面让我们看一看第二个投影操作符。
SelectMany
投影家族里面的第二个操作符是SelectMany。它与Select的声明相似,除了它的投影函数返回了一个序列:
public static IEnumerable<S> SelectMany<T, S>(
this IEnumerable<T> source,
Func<T, IEnumerable<S>> selector);
SelectMany操作符将投影函数返回的每个序列合并为一个序列并返回。为了理解SelectMany操作符,我们在如下的代码中把它与Select操作做比较。
下面是使用Select操作符的代码:
IEnumerable<IEnumerable<Author>> tmp = SampleData.Books
.Select(book => book.Authors);
foreach (var authors in tmp)
{
foreach (Author author in authors)
{
Console.WriteLine(author.LastName);
}
}
下面是使用SelectMany操作的等价代码。如你所见,它更短:
IEnumerable<Author> authors = SampleData.Books
.SelectMany(book => book.Authors);
foreach (Author author in authors)
{
Console.WriteLine(author.LastName);
}
这里我们要枚举所有书籍的作者。书籍的作者属性是一个Author对象的列表。因此Select操作符返回了一个数组的枚举,而SelectMany将Author对象数组转换一个Author对象序列。
下面的表达式实现了SelectMany相同的功能:
from book in SampleData.Books
from author in book.Authors
select author.LastName
注意到我们将两个from子句串联在一起。在查询表达式中,SelectMany投影在每次from子句串联的时候调用。
Select和SelectMany操作符同样提供了带有下标的重载。让我们看看如何使用它们。
Selecting indices
Select和SelectMany操作符可以在使用中获取序列中每个元素的下标。我们可以在程序中进行操作:
index=3 Title=All your base are belong to us
index=4 Title=Bonjour mon Amour
index=2 Title=C# on Rails
index=0 Title=Funny Stories
index=1 Title=LINQ rules
列表4.14显示如何使用Select操作下标:
列表 4.14 使用索引的Select查询操作符
var books = SampleData.Books
.Select((book, index) => new { index, book.Title })
.OrderBy(book => book.Title);
ObjectDumper.Write(books);
这次我们不能使用查询表达式语法了,因为Select操作符提供索引的功能并没有对应的查询表达式语法。注意到我们应该OrderBy之前调用,因为我们需要获取排序之前的索引。
让我们复习另一个查询操作符:Distinct
4.4.3 使用Distinct
有时,查询结果中的信息是重复的。例如,列表4.15显示了出版过书的作者名称。
列表 4.15 不使用Distinct获取作者列表
var authors = SampleData.Books
.SelectMany(book => book.Authors)
.Select(author => author.FirstName+" "+author.LastName);
ObjectDumper.Write(authors);
可以看到一些作者的名字在结果中显示了多次:
Johnny Good
Graziella Simplegame
Octavio Prince
Octavio Prince
Jeremy Legrand
Graziella Simplegame
Johnny Good
这是因为一个作者可以编写很多书籍。为了移除这种重复,我们可以使用Distinct操作符,Distinct为一个序列消除元素重复。为了对元素进行比较Distinct操作符使用了元素对IEquatable<T>接口的实现。如果没有提供实现,它将使用Object.Equals方法。
列表4.16产生了唯一的作者列表
列表 4.16 使用Distinct查询操作符获取作者列表
var authors = SampleData.Books
.SelectMany(book => book.Authors)
.Distinct()
.Select(author => author.FirstName+" "+author.LastName);
ObjectDumper.Write(authors);
结果是:
Johnny Good
Graziella Simplegame
Octavio Prince
Jeremy Legrand
在C#中,没有与Distinct对应的查询表达式语法。因此只能使用方法调用。
同样,下面的操作符也没有对应的查询表达式语法对应。
4.4.4 使用转换操作符
LINQ使用操作符可以很方便的将序列转换为集合和数组。使用ToArray和ToList操作符,可以将序列转换为类型化的数组和列表。这些操作符在将集成查询与现有代码库结合在一起时非常有用。
默认情况下,查询返回序列,集合实现了IEnumerable<T>:
IEnumerable<String> titles = SampleData.Books.Select(book => book.Title);
下面是显示了将结果转换为数组或者列表的代码:
String[] array = titles.ToArray();
List<String> list = titles.ToList();
当你需要立即执行查询或者缓存查询结果的时候,ToArray和ToList同意非常有用。当调用这些操作符时,整个查询将被执行并返回整个序列。
由于查询可以返回不同的结果,你可以使用ToArray和ToList方法来得到一个序列的即时快照。因为这些操作符将结果拷贝到一个新的数组或者序列中。但是应避免对大型的序列使用这些操作符。
还有一个值得一提的事情就是,如果我们使用一个在using块中创建的一个可废弃对象,并且从该块中yield。那么在我们使用对象之前可废弃对象将会被释放。一个可行的方法是使用ToList方法,然后yield整个结果。
下面是实现代码:
IEnumerable<Book> results;
using (var db = new LinqBooksDataContext())
{
results = db.Books.Where(...).ToList();
}
foreach (var book in results)
{
DoSomething(book);
yield return book;
}
另一个有趣的转换是ToDictionary操作符。与创建一个数组或者列表相似,该操作符创建了一个词典,并使用键值组织数据。
如示例所示:
Dictionary<String, Book> isbnRef = SampleData.Books.ToDictionary(book => book.Isbn);这里我们创建一个书籍词典,通过书籍的ISBN号可以对书籍进行索引操作:
Book linqRules = isbnRef["0-111-77777-2"];
下面让我们看看最后一个操作符家族:聚合操作符
4.4.5 使用聚合操作符
一些标准的操作符可以对数据进行数学操作:这就是聚合操作符,如下所示:
n Count,获取序列中元素的个数
n Sum, 计算元素之和
n Min 和 Max, 获取序列中元素的最大值很最小值
下面的示例阐述了这些操作符的用法:
var minPrice = SampleData.Books.Min(book => book.Price);
var maxPrice = SampleData.Books.Select(book => book.Price).Max();
var totalPrice = SampleData.Books.Sum(book => book.Price);
var nbCheapBooks =SampleData.Books.Where(book => book.Price < 30).Count();
在代码示例中,Min和Max不是按照同一个方式调用的。Min操作符是在书籍集合上直接调用,而Max操作符需要在Select操作符之后调用。效果是相同的。前者的Min方法将会调用Select方法,然后调用无参数的Min方法。
我们介绍了一些重要的查询操作符。如Where, Select, SelectMany, Distinct, ToArray, ToList, Count, Sum, Min和 Max。你应该对这些操作符更加熟悉了。这是一个很好的开始,接下来你会看到,还有更多有用的操作符。
4.5 为对象创建视图
接下来我们要叙述的是,如何使用查询操作符执行如排序分组等通用操作。
让我们以排序作为开始
4.5.1 排序
通常情况下,我们希望对数据进行特定的排序,查询表达式允许我们使用orderby做到这一点。
如果我们需要看到根据出版社和价格降序排序然后按照书名升序排序的书籍列表。结果如4.16所示:
完成该查询的代码如列表4.18所示:
图 4.16 排序结果
列表 4.18 使用orderby 子句排序结果
from book in SampleData.Books
orderby book.Publisher.Name, book.Price descending, book.Title
select new { Publisher=book.Publisher.Name,book.Price,book.Title };
Orderby关键字可以用于指定很多次排序,默认情况下,对象按照升序排序。使用descending关键字可是指定降序排序。
查询表达式orderby子句转换为Orderby,ThenBy,OrderByDescending和ThenByDescending操作符的组合调用。下面的代码与以上查询表达式是等价的。
SampleData.Books
.OrderBy(book => book.Publisher.Name)
.ThenByDescending(book => book.Price)
.ThenBy(book => book.Title)
.Select(book => new { Publisher=book.Publisher.Name, book.Price,book.Title });
我们使用GridView控件获取显示在图4.16的中的结果,如4.19所示:
列表 4.19 显示排序结果的示例的标记内容
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false">
<Columns>
<asp:BoundField HeaderText="Publisher" DataField="Publisher" />
<asp:BoundField HeaderText="Price" DataField="Price" />
<asp:BoundField HeaderText="Book" DataField="Title" />
</Columns>
</asp:GridView>
这就是所有排序的用法,下面我们要讲述查询中的另一个操作。
4.5.2 嵌套查询
在前一个例子中,我们使用投影来收集数据,所有数据在同一个层次之上。而且出版社存在重复信息。我们将使用嵌套查询改进我们的示例:
假如我们要完成图4.17所示功能,我们可以首先使用如下查询:
from publisher in SampleData.Publishers
select publisher
图 4.17 使用嵌套查询分组publisher
我们需要出版社的名字和书籍信息,所以不需返回Publisher对象,我们将这些信息分组到一个匿名对象中,该匿名对象包含两个属性:Publisher 和 Books:
from publisher in SampleData.Publishers
select new { Publisher = publisher.Name, Books = ... }
现在你应该知道了,有趣的事情是,我们如何获取出版社的书籍?这不是一个简单的问题!
在我们的示例数据中,可以通过Book类的Publisher属性获取它们之间的关联信息。注意到,这里没有反向引用。幸运的是,LINQ可以帮助我们。我们可以使用简单的查询表达式,并把它嵌入到前一个表达式中:
from publisher in SampleData.Publishers
select new {
Publisher = publisher.Name,
Books =
from book in SampleData.Books
where book.Publisher.Name == publisher.Name
select book }
列表4.20包含了web页上的完整的源代码。
列表 4.20 阐述嵌套查询的代码后置文件
using System;
using System.Linq;
using LinqInAction.LinqBooks.Common;
public partial class Nested : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
GridView1.DataSource =
from publisher in SampleData.Publishers
orderby publisher.Name
select new {
Publisher = publisher.Name,
Books = from book in SampleData.Books
where book.Publisher == publisher select book};
GridView1.DataBind();
}
}
为了显示Books属性,我们使用ASP.NET一个有趣的数据控件,将它显示为一个符号列表。
列表 4.21 嵌套查询的标记
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Nested.aspx.cs" Inherits="Nested" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Nested queries</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false">
<Columns>
<asp:BoundField HeaderText="Publisher" DataField="Publisher" />
<asp:TemplateField HeaderText="Books">
<ItemTemplate>
<asp:BulletedList ID="BulletedList1" runat="server" DataSource='<%# Eval("Books") %>' DataValueField="Title" />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
</div>
</form>
</body>
</html>
在这个标记中,我们对Books属性使用了模板列。在该列中,Books属性绑定到了一个符号列表控件上。而DataValueField设定为Book的Title属性。
在本例中,我们创建了一个有层次的数据视图。这只是LINQ一个简单操作,我们将向你展示操作对象的更多方法。
4.5.3 分组
使用分组,我们可以获取与前面的示例同样的结果。如图4.18所示。
我们使用相同的页面控件代码,只有查询不同。如列表4.22所示。
图 4.18 使用分组分组publisher
列表 4.22 使用group子句分组publisher
protected void Page_Load(object sender, EventArgs e)
{
GridView1.DataSource =
from book in SampleData.Books
group book by book.Publisher into publisherBooks
select new {
Publisher=publisherBooks.Key.Name,
Books=publisherBooks };
GridView1.DataBind();
}
我们要做的是根据出版社对书籍分组。所有属于同一个出版社的书籍将被分组到同一个组中。在本例中该组的名称是publisherBooks。publisherBooks组是一个实现IGrouping<TKey, T>接口的实例。下面是该接口的定义:
public interface IGrouping<TKey, T> : IEnumerable<T>
{
TKey Key { get; }
}
可以看到,实现IGrouping泛型接口的对象有一个强类型的Key和一个强类型的序列。在我们的示例中,key是Publisher对象,序列类型为IEnumerable<Book>
我们的查询返回了出版社名称和其对应书籍的序列。这正是上一个示例中使用嵌套查询要完成的工作。这也正是我们可以重用同样的GridView标记的原因。
使用分组操作符而不是用嵌套查询,有两个优点。第一:查询更简洁。第二是我们可以命名分组,这使得查询更容易理解,并且我们可以在查询内重用该分组。例如,我们可以添加对每个出版社书籍个数的显示:
from book in SampleData.Books
group book by book.Publisher into publisherBooks
select new {
Publisher=publisherBooks.Key.Name,
Books=publisherBooks, publisherBooks.Count() };
分组在SQL使用非常普遍。使用Count等聚合运算符与SQL中的使用方式也非常相似。分组是LINQ提供的处理对象之间关系的一种方式,另一种方式是连接操作符。
4.5.4 使用链接操作符
Join操作符允许我们利用与SQL相似的语法操作对象。
Group join
为了介绍join操作符。让我们考虑如下查询表达式,如列表4.23所示。
列表 4.23 使用join..into 子句根据publisher分组book
from publisher in SampleData.Publishers
join book in SampleData.Books
on publisher equals book.Publisher
into publisherBooks
select new { Publisher=publisher.Name, Books=publisherBooks };
这就是group join。它把每个出版社的书籍包装为一个名为publisherBooks的序列。这与使用嵌套查询相同。
使用分组的查询表达式如下:
from book in SampleData.Books
group book by book.Publisher into publisherBooks
select new {
Publisher=publisherBooks.Key.Name,
Books=publisherBooks };
如图4.19所示,可以看到结果有所不同。使用嵌套查询时,没有书籍的出版社出现在结果中,与使用group join相同,而仅使用分组的查询结果中只出现有书籍的出版社。
图 4.19 Group join 的结果
Inner join
inner join查找两个序列之间的交集,并合成为一个序列。使用inner join,两个序列中匹配条件的元素被合并为一个序列。
Join操作符执行两个序列间元素的匹配。它可以显示如4.20所示的平面视图。
查询代码如列表4.24所示:
列表 4.24 使用join 子句根据publisher分组book
from publisher in SampleData.Publishers
join book in SampleData.Books on publisher equals book.Publisher
select new { Publisher=publisher.Name, Book=book.Title };
这段查询与group join示例代码相似,不同的是,这里我们没有使用into关键字来分组元素。Books对象被投影到出版社上,如图4.20所示。结果序列元素包含了每一本书。在我们的示例数据中,有一个出版社没有书籍与之对应,它在结果中也没有出现。这就是为什么我们称之为inner join的原因。。只有满足匹配条件的元素被保留下来。很快,我们将把它与left outer join相比较。
图 4.20 Inner join 的结果
列表4.25是使用查询操作符编写的同一个查询。
列表 4.25 使用Join 操作符根据publisher分组book
SampleData.Publishers
.Join(
SampleData.Books, //Inner sequence
publisher => publisher, //Outer key selector
book => book.Publisher, //Inner key selector
(publisher, book) => new { Publisher=publisher.Name,Book=book.Title }
// Result selector
);
可见,查询表达式类似SQL的语法帮助我们提高了查询的可读性
Left outer join
我们看到,使用inner join,只有匹配条件的元素被保留下来。当不管第二个序列是否有匹配,你都想保留第一个序列的元素的时候,你就需要使用left outer join了。
left outer join与inner join相似,不同的是,所有左边序列的元素至少被保留一次,即使它们不关联任何右边序列的元素。
如果我们需要在结果中包含没有书籍的出版社。如图4.21所示。
图 4.21 Left outer join 的结果
我们使用group join来表达outer join的功能。列表4.26显示了使用Group join表达同样结果的查询代码:
列表 4.26 执行left outer join的查询
from publisher in SampleData.Publishers
join book in SampleData.Books
on publisher equals book.Publisher into publisherBooks
from book in publisherBooks.DefaultIfEmpty()
select new {
Publisher = publisher.Name,
Book = book == default(Book) ? "(no books)" : book.Title
};
DefaultIfEmpty操作符为序列中的空元素提供了一个默认元素,DefaultIfEmpty使用了泛型中的default关键字。对于引用类型,它返回null,对于值类型,它返回0.对于结构,它将该结构的的字段初始化为0或者null(取决于该类型是值类型还是引用类型)。
在我们的示例中,默认值为null。但是可以使用default(Book)来决定为Book对象显示什么。
我们已经看到了group join,inner join和left outer join。还有一种连接叫做cross join。
Cross join
交叉连接计算两个序列所有元素的笛卡尔积,结果是一个序列的每个元素都对应另一个序列的每个元素,作为结果,结果序列的元素个数是两个序列元素个数的乘积。
在LINQ中,交叉连接并不是用Join操作符完成的,交叉连接是一个投影操作。使用SelectMany操作符或者使用嵌套查询都可以完成交叉连接。
图 4.22 Cross join 的结果
作为示例,如果我们要显示出版社和书籍的所有可能组合,而不管它们之间是不是存在连接。就需要使用交叉连接,我们还可以添加一列来表明它们之间是不是存在连接。如图4.22所示。
列表4.27所示的表达式显示了交叉连接的用法。
列表 4.27 执行cross join的查询
from publisher in SampleData.Publishers from book in SampleData.Books
select new {
Correct = (publisher == book.Publisher),
Publisher = publisher.Name,
Book = book.Title };
下面是与查询表达式等价的使用查询操作符的用法,使用SelectMany和Select操作符:
SampleData.Publishers.SelectMany(
publisher => SampleData.Books.Select(
book => new {
Correct = (publisher == book.Publisher),
Publisher = publisher.Name,
Book = book.Title }));
介绍了连接之后,我们将会探讨关于在内存中创建视图的问题。我们使用分割序列得到指定部分的元素。
4.5.5 分割(Partitioning)
到目前为止,我们都是使用一页显示所有数据。如果数据量不大,这不是问题。否则,我们就需要使用分页机制了。
添加分页
如果我们需要在一个页面上显示大量的书籍信息,我们可以使用GridView控件的分页特性。如图4.23所示。
图 4.23 带分页的表格
表格的底部,有我们可以访问其他页面的链接。分页可以在标记中配置,如下:
<asp:GridView ID="GridView1" runat="server" AllowPaging="true" PageSize="3" OnPageIndexChanging="GridView1_PageIndexChanging">
</asp:GridView>
列表4.28显示了代码后置文件如何处理分页。
列表 4.28 控制GridView 控件分页的代码
using System;
using System.Linq;
using System.Web.UI.WebControls;
using LinqInAction.LinqBooks.Common;
public partial class Paging : System.Web.UI.Page
{
private void BindData()
{
GridView1.DataSource = SampleData.Books
.Select(book => book.Title).ToList(); GridView1.DataBind();
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
BindData();
}
protected void GridView1_PageIndexChanging(
object sender, GridViewPageEventArgs e)
{
GridView1.PageIndex = e.NewPageIndex;
BindData();
}
}
注意 为了启用分页,我们使用了ToList 因为序列并不提供分页功能
使用GridView控件进行分页非常有用而且非常简单。但是却跟LINQ没有多大关系,控件自己处理所有需要的操作。
在LINQ中,我们可以使用Skip和Take操作符编程实现分页功能。
Skip 和 Take
当你只想保留一个序列指定范围的数据时,你可以使用两个分割查询操作符:Skip和Take
Skip操作符跳过一个序列中指定数目的元素,只产生剩下的元素。Take操作符产生指定数目的元素,而忽略剩下的元素。在给定页面大小pageSize和页面索引n时,获取当前页面的元素的经典的算法是 sequence.Skip(n * pageSize).Take(pageSize)
使用两个列表框,我们可以选择开始和结束下标。图4.24显书籍的完整列表,和选择后的列表。
列表4.29显示了产生该结果的代码。
图 4.24 分割结果
列表 4.29 阐述分割的后置代码
using System;
using System.Linq;
using System.Web.UI.WebControls;
using LinqInAction.LinqBooks.Common;
public partial class Partitioning : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
GridViewComplete.DataSource = SampleData.Books
.Select((book, index) => new { Index=index, Book=book.Title});
GridViewComplete.DataBind();
int count = SampleData.Books.Count();
for (int i = 0; i < count; i++)
{
ddlStart.Items.Add(i.ToString());
ddlEnd.Items.Add(i.ToString());
}
ddlStart.SelectedIndex = 2;
ddlEnd.SelectedIndex = 3;
DisplayPartialData();
}
}
protected void ddlStart_SelectedIndexChanged(object sender, EventArgs e)
{
DisplayPartialData();
}
private void DisplayPartialData()
{
int startIndex = int.Parse(ddlStart.SelectedValue);
int endIndex = int.Parse(ddlEnd.SelectedValue);
GridViewPartial.DataSource = SampleData.Books
.Select((book, index) => new { Index=index, Book=book.Title })
.Skip(startIndex).
Take(endIndex-startIndex+1);
GridViewPartial.DataBind();
}
}
下面是相关的标记:
...
<body>
<form id="form1" runat="server">
<div>
<h1>Complete results</h1>
<asp:GridView ID="GridViewComplete" runat="server" />
<h1>Partial results</h1> Start:
<asp:DropDownList ID="ddlStart" runat="server"
AutoPostBack="True" CausesValidation="True"
OnSelectedIndexChanged="ddlStart_SelectedIndexChanged" />
End:
<asp:DropDownList ID="ddlEnd" runat="server"
AutoPostBack="True" CausesValidation="True"
OnSelectedIndexChanged="ddlStart_SelectedIndexChanged" />
<asp:CompareValidator ID="CompareValidator1" runat="server"
ControlToValidate="ddlStart" ControlToCompare="ddlEnd"
ErrorMessage= "The second index must be higher than the first one" Operator="LessThanEqual" Type="Integer" /><br />
<asp:GridView ID="GridViewPartial" runat="server" />
</div>
</form>
</body>
</html>
操作符的介绍到此为止,下一章,你会看到更多的操作符。
4.6 总结
本将介绍了Object LINQ,阐述了如何使用LINQ对内存中的对象集合进行各种操作。
本章学到的知识在操作对象集合是非常有用,但是同样的,操作对象的知识同样可以应用到LINQ的其他实现上,如XML LINQ和SQL LINQ。
在学习了大量的LINQ语言特性后,就可以在一些常用场景下编写LINQ代码了。这就是下章要介绍的的主题。