65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 LINQ 查询

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (126投票s)

2012年2月15日

CPOL

23分钟阅读

viewsIcon

663236

最重要的 LINQ 查询解释。

目录

  1. 引言
  2. 背景
    1. 查询
    2. 函数
    3. 动态 LINQ
    4. Lambda 表达式
  3. 本文中使用的类模型
  4. LINQ 查询
    1. 基本查询
    2. 字段的投影/选择
    3. 实体排序
    4. 实体过滤/限制
    5. 局部变量
  5. 集合方法
    1. 集合函数
    2. 元素函数
    3. 转换函数
    4. 量词函数
    5. 聚合函数
  6. 高级查询
    1. 表连接
    2. Join 运算符
    3. 分组运算符
    4. 嵌套查询
  7. 结论
  8. 历史

引言

Language INtegrated Queries(语言集成查询)是类似 SQL 的 C# 查询,可用于操作对象集合。在本文中,我将展示一些用例,说明如何使用 LINQ 查询对象集合。

本文的目标是成为 LINQ 的初学者指南,并为其他人提供参考/提醒。

背景

当人们听到 LINQ 时,大多数情况下,他们会想到类似 Entity Framework 的东西,即在 C# 代码中直接编写查询,这些查询将被直接转换为 SQL 语句并针对数据库执行。重要的是要知道这不是 LINQ。LINQ 是一组 C# 函数、类和运算符,使开发人员能够针对对象集合执行查询。真正的 LINQ 查询是针对集合执行的。

有许多 LINQ 扩展可以将查询转换为 SQL、XML/XPath、REST 等。在本文中,我将讨论基本的 LINQ 到集合查询。

LINQ 操作有两种形式——查询和函数。您可以在以下部分中查看有关它们的更多详细信息。

查询

在 LINQ 包中添加了一组预定义运算符(查询),使开发人员能够对对象集合创建类似 SQL 的查询。这些查询根据查询条件返回新的数据集合。查询使用以下形式

from <<element>> in <<collection>>
   where <<expression>> 
   select <<expression>>

作为查询的结果,将返回一个泛型集合(IEnumerable<T>)。泛型集合中的类型 <T> 是使用查询的 select <<expression>> 部分中的表达式类型确定的。以下清单显示了一个 LINQ 查询示例,该查询返回价格低于 500 的书籍的书名

from book in myBooks
    where book.Price < 500
    select book.Title

此查询遍历书籍集合 myBooks,从中选取价格属性小于 500 的书籍实体,并返回每个书名。查询的结果是一个 IEnumerable<String> 对象,因为 Stringselect 查询中返回表达式的类型(假设书籍的 Title 属性是 string)。

函数

LINQ 添加了一组可应用于集合的有用函数。函数作为集合对象的新方法添加,可以使用以下形式

  • <集合对象>.方法名()
  • <集合对象>.方法名(<集合对象>)
  • <集合对象>.方法名(<<表达式>>)

所有 LINQ 查询都在 IEnumerable<T>IQueryable<T> 类型的集合上执行,结果返回为新的集合 (IEnumerable<T>)、对象或简单类型。返回集合的 LINQ 查询或函数的示例有

IEnumerable<T> myBooks = allBooks.Except(otherBooks); 
IEnumerable<string> titles = myBooks.Where(book=>book.Price<500)
                                    .Select(book=>book.Title);
int count = titles.Count();

这三个函数中的第一个函数创建一个新集合,其中所有书籍(除了 otherBooks 集合中的书籍)都被放置。第二个函数选取所有价格低于 500 的书籍,并返回它们的书名列表。第三个函数在 titles 集合中查找书名的数量。此代码展示了上面所示的三个 LINQ 函数的用法。

请注意,LINQ 查询和函数有两种形式。对于大多数查询,您可以编写等效的函数形式。示例如下所示

from book in myBooks
            where book.Price < 500
            select book.Title
myBooks
       .Where(book=>book.Price<500)
       .Select(book=>book.Title);

在以下章节中,您可以找到更多关于 LINQ 到集合的示例。

动态 LINQ

如上所述,LINQ 查询和函数返回类或类集合。在大多数情况下,您将在查询的返回类型中使用现有领域类(BookAuthorPublisher 等)。但是,在某些情况下,您可能希望返回类模型中不存在的自定义类。例如,您可能只想返回书籍的 ISBN 和标题,因此您不想返回整个 Book 对象及其所有您根本不需要的属性。如果您想返回不同类中的字段(例如,如果您想返回书籍的标题和出版商的名称),则会出现类似的问题。

在这种情况下,您不需要定义只包含您想用作返回值的字段的新类。LINQ 允许您返回所谓的“匿名类”——动态创建的类,无需在您的类模型中显式定义。这种查询的一个示例如下所示

var items = from b in books
select new { Title: b.Title,
             ISBN: b.ISBN
           };

变量 items 是动态创建的类集合,其中每个对象都具有 TitleISBN 属性。此未在您的类模型中明确定义,它是为此查询动态创建的。如果您尝试查找变量 items 的类型,您可能会看到类似 IEnumerable<a'> 的内容——.NET 框架为该类提供了一些动态名称(例如,a')。这样,您就可以只将临时类用于查询结果,而无需在代码中定义它们。

许多人认为这是一种不好的做法,因为我们在这里使用了没有类型的对象。这不是真的——items 变量确实有类型,但类型未在某个文件中定义。但是,您拥有类型化对象所需的一切

  • 编译时语法检查——如果您在键入时犯了一些错误(例如,将 ISBN 拼写成 ISBN),编译器将显示警告并中止编译。
  • 智能感知支持——Visual Studio 将像对待常规对象一样,向您显示对象的属性/方法列表。

但是,有一种方法可以在 .NET 中使用无类型对象。如果您将关键字 var 替换为关键字 dynamic,则变量 items 将真正无类型。示例如下所示

dynamic items = from b in books
        select new { Title: b.Title,
                     ISBN: b.ISBN
                   };

在这种情况下,您拥有一个真正的无类型对象——不会进行编译时检查(属性仅在运行时验证),并且您不会获得对动态对象的任何智能感知支持。

尽管 vardynamic 好(尽可能使用 var),但在某些情况下,您将被迫使用 dynamic 而不是 var。例如,如果您想将某些 LINQ 查询的结果作为方法的返回值返回,则不能将方法的返回类型声明为 var,因为匿名类的作用域在方法体中结束。在这种情况下,您将需要定义一个显式类或将返回值声明为 dynamic

在本文中,我将使用显式类或匿名类。

Lambda 表达式

在处理 LINQ 时,您会发现一些形式为 x => y 的“奇怪语法”。如果您不熟悉这种语法,我将简要解释一下。

在每个 LINQ 函数中,您都需要定义一些条件,这些条件将用于过滤对象。最自然的方法是传递一些将被评估的函数,如果对象满足函数中定义的条件,它将被包含在 LINQ 函数的结果集中。这种条件函数需要接受一个对象并返回一个 true/false 值,该值将告诉 LINQ 是否应将此对象包含在结果集中。以下清单显示了检查书籍是否便宜的此类函数示例

public bool IsCheapBook(Book b)
{
    return (b.Price < 10);
}

如果书价低于 10,则它很便宜。现在有了这个条件,您可以在 LINQ 子句中使用它

var condition = new Func<Book, bool>(IsBookCheap);
var cheapBooks = books.Where(condition);

在这段代码中,我们以 Func<Book, bool> 的形式定义了一个指向函数 IsBookCheap 的“函数指针”,并将此函数传递给 LINQ 查询。LINQ 将对书籍集合中的每个书籍对象评估此函数,如果它满足函数中定义的条件,则在结果枚举中返回该书籍。

这并不是一种常见的做法,因为条件更具动态性,您不太可能在代码中的某个地方创建一组预编译函数,并且它们将被所有 LINQ 查询使用。通常,每个 LINQ 查询我们只需要一个表达式,因此最好动态生成并将条件传递给 LINQ。幸运的是,C# 允许我们使用 delegate 来实现这一点

var cheapBooks = books.Where(delegate(Book b){ return b.Price < 10; } );

在此示例中,我动态创建了 Function<Book, bool>,并将其直接放入 Where() 条件中。结果与上一个示例相同,但您不需要为此定义一个单独的函数。

如果您认为对于一个简单的内联函数来说,键入的内容太多,则有一种更短的语法——lambda 表达式。首先,您可以看到我们不需要 delegate 关键字(编译器应该知道我们需要将 delegate 作为参数传递)。此外,为什么我们需要定义参数的类型(Book b)?因为我们将此函数应用于书籍集合,我们知道 bBook——因此我们也可以删除此部分。此外,为什么我们要键入 return——定义返回条件的表达式就足够了。我们唯一需要的是一个分隔符,它将放置在参数和将返回的表达式之间——在 C# 中,我们使用 => 符号。

当我们删除所有不必要的东西并放置分隔符 => 时,我们得到一个形式为 argument => expression 的 lambda 表达式语法。原始的 delegate 和等效的 lambda 表达式替换示例如下所示

Funct<Book, bool> delegateFunction = delegate(Book b){ return b.Price < 10; } ;
Funct<Book, bool> lambdaExpression = b => b.Price< 10 ;

如您所见,lambda 表达式只是内联函数的最小化语法。请注意,我们可以将 lambda 表达式用于任何类型的函数(不仅是返回 bool 值的函数)。例如,您可以定义一个 lambda 表达式,它接受一本书和一个作者,并以“书名 (作者姓名)”的格式返回书名。以下清单显示了这种 lambda 表达式的示例

Func<Book, Author, string> format = (book, author) => book.Title + "(" + author.Name + ")";

此函数将接受两个参数(BookAuthor),并返回一个 string 作为结果(Func<> 对象中的最后一个类型始终是返回类型)。在 lambda 表达式中定义了括号中的两个参数和将返回的字符串表达式。

Lambda 表达式在 LINQ 中被广泛使用,所以您应该习惯它们。

本文中使用的类模型

在示例中,我们将使用表示书籍、作者和出版商信息的结构。此数据结构的类图如下所示

LINQ-Queries-Overview/Linq2EntitiesSampleDiagram.gif

每本书可以有多个作者和一个出版商。与实体相关的字段显示在图表上。Book 包含 ISBN、价格、页数、出版日期和标题信息。此外,它还包含对出版商的引用和对一组作者的引用。Author 具有名字和姓氏,没有对书籍的反向引用,出版商只有名称,没有对已出版书籍的引用。

假设书籍、出版商和作者的集合放置在 SampleData.BooksSampleData.PublishersSampleData.Authors 字段中。

LINQ 查询

在本节中,我将展示一些可以使用的基本查询/函数的示例。如果您是初学者,这应该是一个很好的起点。

基本查询

以下示例展示了 LINQ 的基本用法。为了使用 LINQ to Entities 查询,您需要有一个将被查询的集合(例如,书籍数组)。在这个基本示例中,您需要指定将查询哪个集合(“from <<element>> in <<collection>>”部分)以及查询中将选择哪些数据(“select <<expression>>”部分)。在下面的示例中,查询针对书籍数组执行,书籍实体被选择并作为查询结果返回。查询的结果是 IEnumerable<Book>,因为“'select << expression>>”部分中表达式的类型是 Book 类。

Book[] books = SampleData.Books;
IEnumerable<Book> bookCollection = from b in books
                                   select b;

foreach (Book book in bookCollection )
         Console.WriteLine("Book - {0}", book.Title);

如您所见,此查询没有做任何有用的事情——它只是从书籍集合中选择所有书籍并将它们放入枚举中。但是,它展示了 LINQ 查询的基本用法。在以下示例中,您可以找到更多有用的查询。

字段的投影/选择

使用 LINQ,开发人员可以转换集合并创建新集合,其中元素只包含某些字段。以下代码显示了如何从书籍集合中提取书籍标题集合。

Book[] books = SampleData.Books;            
IEnumerable<string> titles = from b in books
                             select b.Title;

foreach (string t in titles)
    Console.WriteLine("Book title - {0}", t);

作为此查询的结果,创建了 IEnumerable<string>,因为 select 部分中表达式的类型是 string。以下代码显示了作为 select 函数和 lambda 表达式编写的等效示例

Book[] books = SampleData.Books;            
IEnumerable<string> titles = books.Select(b=>b.Title);

foreach (string t in titles)
    Console.WriteLine("Book title - {0}", t);

任何类型都可以用作结果集合类型。在以下示例中,返回一个匿名类枚举,其中枚举中的每个元素都包含对书籍和书籍第一位作者的引用

var bookAuthorCollection = from b in books
                   select new { Book: b,
                                Author: b.Authors[0]
                              };
    
foreach (var x in bookAuthorCollection)
    Console.WriteLine("Book title - {0}, First author {1}", 
                         x.Book.Title, x.Author.FirstName);

当您需要动态创建新类型的集合时,这种类型的查询非常有用。

在 Select 查询中返回扁平化集合

假设您想为一组书籍返回一个作者集合。使用 Select 方法,此查询将如下面示例所示

Book[] books = SampleData.Books;            
IEnumerable< List<Author> > authors = books.Select(b=>b.Authors);

foreach (List<Author> bookAuthors in authors)
    bookAuthors.ForEach(author=>Console.WriteLine("Book author {0}", author.Name);

在此示例中,从书籍集合中获取每本书的作者列表。当您使用 Select 方法时,它将在结果枚举中返回一个元素,并且每个元素都将具有 List<Author> 类型,因为这是 Select 方法中返回属性的类型。结果是,您需要对集合进行两次迭代才能显示所有作者——一次迭代枚举,然后对于枚举中的每个列表,您需要再次迭代以访问每个单独的作者。

然而,在某些情况下,这不是您想要的。您可能想要一个单一的扁平化作者列表,而不是两级列表。在这种情况下,您需要使用 SelectMany 而不是 Select 方法,如下面示例所示

Book[] books = SampleData.Books;            
IEnumerable<Author> authors = books.SelectMany(b=>b.Authors);

foreach (Author authors in authors)
    Console.WriteLine("Book author {0}", author.Name); 

SelectMany 方法将 lambda 表达式中返回的所有集合合并到单个扁平化列表中。这样,您可以轻松地操作集合的元素。

请注意,在第一个示例中,当我迭代作者列表以显示它们时,我使用了 ForEach 方法。ForEach 方法不是 LINQ 的一部分,因为它是一个添加到列表类的常规扩展方法。然而,它对于紧凑的内联循环来说是一个非常有用的替代方案(这可能是许多人认为它是 LINQ 的一部分的原因)。由于 ForEach 方法不是 LINQ 的一部分,您不能像常规 LINQ 方法一样在枚举上使用它,因为它被定义为 List<T> 而不是 Enumerable<T> 的扩展——如果您喜欢此方法,您需要将您的可枚举对象转换为列表才能使用它。

实体排序

使用 LINQ,开发人员可以对集合中的实体进行排序。以下代码显示了如何获取书籍集合,按书籍出版商名称和标题对元素进行排序,并选择排序集合中的书籍。作为查询的结果,您将获得一个按书籍出版商和标题排序的 IEnumerable<Book> 集合。

Book[] books = SampleData.Books;              
IEnumerable<Book> booksByTitle = from b in books
                                 orderby b.Publisher.Name descending, b.Title
                                 select b;

foreach (Book book in booksByTitle)                
    Console.WriteLine("Book - {0}\t-\tPublisher: {1} ",
                       book.Title, book.Publisher.Name );

使用函数的替代代码示例如下

Book[] books = SampleData.Books;              
IEnumerable<Book> booksByTitle = books.OrderByDescending(book=>book.Publisher.Name)
                                      .ThenBy(book=>book.Title);

foreach (Book book in booksByTitle)                
    Console.WriteLine("Book - {0}\t-\tPublisher: {1} ",
                       book.Title, book.Publisher.Name );

如果您有复杂的结构,需要使用相关实体的属性(在此示例中,书籍按出版商名称字段排序,该字段根本不在书籍类中)对实体进行排序,则这种类型的查询非常有用。

实体过滤/限制

使用 LINQ,开发人员可以从集合中过滤实体并创建只包含满足特定条件的新集合。以下示例创建了一个书籍集合,其中包含标题中包含单词“our”且价格小于 500 的书籍。从书籍数组中选择标题包含单词“our”、价格与 500 比较的记录,并将这些书籍选择并作为新集合的成员返回。在“''where <<expression>>''”中可以使用有效的 C# 布尔表达式,该表达式使用集合中的字段、常量和作用域中的变量(即价格)。返回集合的类型是 IEnumerable<Book>,因为在“''select <<expression>>''”部分中选择的类型是 Book

Book[] books = SampleData.Books;            
int price = 500;            
IEnumerable<Book> filteredBooks = from b in books                                         
                                  where b.Title.Contains("our") && b.Price < price
                                  select b;

foreach (Book book in filteredBooks)                
    Console.WriteLine("Book - {0},\t Price {1}", book.Title, book.Price);

作为替代,可以使用 .Where() 函数,如下面示例所示

Book[] books = SampleData.Books;            
int price = 500;            
IEnumerable<Book> filteredBooks = books.Where(b=> (b.Title.Contains("our") 
                            && b.Price < price) ); 

foreach (Book book in filteredBooks)                
    Console.WriteLine("Book - {0},\t Price {1}", book.Title, book.Price);

局部变量

您可以在 LINQ 查询中使用局部变量,以提高查询的可读性。局部变量是使用 LINQ 查询内部的 let <<localname>> = <<expression>> 语法创建的。一旦定义,局部变量可以在 LINQ 查询的任何部分(例如 whereselect 子句)中使用。以下示例演示了如何使用局部变量选择标题中包含单词“our”的书籍中的一组第一位作者。

IEnumerable<Author> firstAuthors =  from b in books
                                    let title = b.Title 
                                    let authors = b.Authors
                                    where title.Contains("our")
                                    select authors[0];             

foreach (Author author in firstAuthors)
    Console.WriteLine("Author - {0}, {1}",
                       author.LastName, author.FirstName);

在此示例中,变量 TitleAuthors 引用书籍的标题和作者列表。通过变量引用这些项可能比直接引用更容易。

集合方法

使用 LINQ,您可以修改现有集合或使用其他 LINQ 查询创建的集合。LINQ 为您提供了一组可以应用于集合的函数。这些函数可以分为以下几种类型

  • 集合函数 - 可用于集合操作的函数,例如合并、交集、反向排序等。
  • 元素函数 - 可用于从集合中获取特定元素的函数
  • 转换函数 - 用于将一种集合类型转换为另一种类型的函数
  • 聚合函数 - 类似于 SQL 的函数,可用于查找集合中某个字段的最大值、总和或平均值
  • 量词函数 - 用于快速遍历集合

这些函数在以下部分中描述。

集合函数

集合运算符允许您操作集合并使用标准集合操作,如并集、交集等。LINQ 集合运算符有

  • Distinct - 用于从集合中提取不同的元素
  • Union - 创建一个表示两个现有集合并集的集合
  • Concat - 将一个集合中的元素添加到另一个集合
  • Intersect - 创建一个包含两个集合中都存在的元素的集合
  • Except - 创建一个包含在一个集合中存在但不存在于另一个集合中的元素的集合
  • Reverse - 创建一个元素顺序颠倒的集合副本
  • EquallAll - 检查两个集合是否具有相同顺序的相同元素
  • Take - 此函数从一个集合中获取一定数量的元素,并将它们放入一个新集合
  • Skip - 此函数跳过集合中的一定数量的元素

假设在前面的示例中创建了 booksByTitlefilteredBooks 集合,以下代码查找 booksByTitle 中不存在于 filteredBooks 中的所有书籍,并反转它们的顺序。

IEnumerable<Book> otherBooks = booksByTitle.Except(filteredBooks);            

otherBooks = otherBooks.Reverse();  

foreach (Book book in otherBooks)
   Console.WriteLine("Other book - {0} ",  book.Title);

在以下示例中,booksByTitlefilteredBooks 被连接,并显示元素数量和不同元素的数量。

IEnumerable<Book> mergedBooks = booksByTitle.Concat(filteredBooks);
Console.WriteLine("Number of elements in merged collection is {0}", mergedBooks.Count());
Console.WriteLine("Number of distinct elements in merged collection is {0}", 
                   mergedBooks.Distinct().Count());

分页示例

此示例展示了使用 Skip(int)Take(int) 方法进行客户端分页的示例。假设每页有十本书,使用 Skip(30) 跳过前三页(每页十本书,共三页),并使用 Take(10) 获取第四页上应显示的所有书籍。示例代码如下

IEnumerable<Book> page4 = booksByTitle.Skip(30).Take(10);            

foreach (Book book in page4)                
    Console.WriteLine("Fourth page - {0} ", book.Title);

还有 Skip/Take 函数在 SkipWhile/TakeWhile 形式下的有趣用法

IEnumerable<Book> page1 = booksByTitle.OrderBy(book=>book.Price)            
                                      .SkipWhile(book=>book.Price<100)
                                      .TakeWhile(book=>book.Price<200);
foreach (Book book in page1)                
    Console.WriteLine("Medium price books - {0} ", book.Title);

在此示例中,书籍按价格排序,所有价格低于 100 的书籍被跳过,所有价格低于 200 的书籍被返回。这样,所有价格在 100 到 200 之间的书籍都被找到了。

元素函数

当您需要从集合中提取特定元素时,可以使用几个有用的函数

  • First - 用于查找集合中的第一个元素。可选地,您可以向此函数传递一个条件,以查找满足条件的第一个元素。
  • FirstOrDefault - 用于查找集合中的第一个元素。如果找不到此类元素,则返回该类型的默认元素(例如,0null)。
  • ElementAt - 用于查找特定位置的元素。

以下示例展示了 FirstOrDefaultElementAt 函数的用法

Book firstBook = books.FirstOrDefault(b=>b.Price>200);              
Book thirdBook = books.Where(b=>b.Price>200).ElementAt(2);

请注意,您可以将函数应用于集合,也可以应用于其他 LINQ 函数的结果。

转换函数

有一些转换函数允许您将一种集合类型转换为另一种。其中一些函数是

  • ToArray - 用于将 IEnumerable<T> 集合的元素转换为 <T> 元素数组
  • ToList - 用于将 IEnumerable<T> 集合的元素转换为 List<T> 列表
  • ToDictionary - 用于将集合的元素转换为 Dictionary。在转换过程中,必须指定键和值。
  • OfType - 用于提取实现接口/类 T2IEnumerable<T1> 集合元素,并将它们放入 IEnumerable<T2> 集合中

以下示例展示了 ToArrayToList 函数的用法

Book[] arrBooks = books.ToArray();
List<Book> lstBook = books.ToList();

ToDictionary 是一个有趣的方法,它允许您通过某个字段快速索引列表。这种查询的一个示例如下所示

Dictionary<string, Book> booksByISBN = books.ToDictionary(book => book.ISBN);
Dictionary<string, double> pricesByISBN = books.ToDictionary(    book => book.ISBN, 
                                book=>book.Price);

如果您只提供一个 lambda 表达式,ToDictionary 将使用它作为新字典的键,而元素将是对象。您还可以为键和值提供 lambda 表达式,并创建自定义字典。在上面的示例中,我们创建了一个以 ISBN 键索引的书籍字典,以及一个以 ISBN 索引的价格字典。

量词函数

在每个集合中,您都可以找到一些逻辑函数,可用于快速遍历集合并检查某些条件。例如,您可以使用的一些函数是

  • Any - 检查集合中是否有任何元素满足特定条件
  • All - 检查集合中所有元素是否满足特定条件

函数用法的示例如下所示

if(list.Any(book=>book.Price<500)) 
    Console.WriteLine("At least one book is cheaper than 500$"); 

if(list.All(book=>book.Price<500))  
    Console.WriteLine("All books are cheaper than 500$");

在上面的示例中,AllAny 函数将检查列表中书籍的价格是否小于 500 的条件是否满足。

聚合函数

聚合函数使您能够对集合元素执行聚合。LINQ 中可用的聚合函数有 CountSumMinMax 等。

以下示例显示了应用于整数数组的一些聚合函数的简单用法

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

Console.WriteLine("Count of numbers greater than 5 is {0} ", numbers.Count( x=>x>5 ));
Console.WriteLine("Sum of even numbers is {0} ", numbers.Sum( x=>(x%2==0) ));
Console.WriteLine("Minimum odd number is {0} ", numbers.Min( x=>(x%2==1) ));
Console.WriteLine("Maximum is {0} ", numbers.Max());
Console.WriteLine("Average is {0} ", numbers.Average());

如您所见,您可以使用标准聚合函数,也可以使用 lambda 条件预选一个子集。

高级查询

本节展示了如何创建高级查询。这类查询包括连接不同的集合和使用 group by 运算符。

表连接

LINQ 允许您对对象集合使用类似 SQL 的连接。集合的连接方式与 SQL 中的表相同。以下示例展示了如何连接三个集合(出版商、书籍和作者),在 where 部分设置一些限制条件,并打印查询结果

var baCollection = from pub in SampleData.Publishers
                   from book in SampleData.Books
                   from auth in SampleData.Authors
                   where book.Publisher == pub
                      && auth.FirstName.Substring(0, 3) == pub.Name.Substring(0, 3)
                      && book.Price < 500
                      && auth.LastName.StartsWith("G")
                   select new { Book = book, Author = auth};              

foreach (var ba in baCollection)
{    Console.WriteLine("Book {0}\t Author {1} {2}", 
                ba.Book.Title,
                ba.Author.FirstName,
                ba.Author.LastName);
}

此查询获取出版商、书籍和作者;通过出版商引用连接书籍和出版商,通过姓名的前三个字母连接作者和出版物。此外,结果通过价格小于 500 的书籍和姓名以字母“G”开头的作者进行过滤。如您所见,您可以使用任何条件连接集合实体。

Join 运算符

LINQ 允许您使用“''<<collection>> join <<collection>> on <<expression>>'' 运算符在 join 条件上连接两个集合。它类似于前面的示例,但您可以轻松阅读查询。以下示例展示了如何使用 Book.Publisher 引用作为 join 条件将出版商与他们的书籍连接起来。

var book_pub = from p in SampleData.Publishers
                    join b in SampleData.Books  on p equals b.Publisher 
                    into publishers_books
               where p.Name.Contains("Press")
               select new { Publisher = p, Books = publishers_books};             

foreach (var bp in book_pub){
    Console.WriteLine("Publisher - {0}", bp.Publisher.Name);
    foreach (Book book in bp.Books)
        Console.WriteLine("\t Book - {0}", book.Title);
}

一个书籍集合作为 publishers_books 属性附加到每个出版商记录。在 where 子句中,您可以通过条件过滤出版商。

请注意,如果您通过引用连接对象(在上面的示例中,您可以看到 join 条件是 p equals b.Publisher),如果引用的对象未加载,则可能会收到“Object reference not set to the instance objects”异常。请确保在开始查询之前已加载所有相关对象,确保已在查询中处理 null 值,或者在可能的情况下使用 ID 而不是引用作为 join 条件。

分组运算符

LINQ 允许您对对象集合使用分组功能。以下示例展示了如何按出版年份对书籍进行分组。作为查询的结果,返回的是一个匿名类的枚举,其中包含一个表示分组所用键的属性(Year),以及另一个表示该年份出版书籍集合的属性(Books)。

var booksByYear = from book in SampleData.Books
               group book by book.PublicationDate.Year
               into groupedByYear
               orderby groupedByYear.Key descending
          select new {
                       Value = groupedByYear.Key,
                       Books = groupedByYear
                      };

foreach (var year in booksByYear){
        Console.WriteLine("Books in year - {0}", year.Value);
        foreach (var b in year.Books)
            Console.WriteLine("Book - {0}", b.Title);
}

聚合示例

使用 LINQ 和分组,您可以模拟“select title, count(*) from table”的 SQL 查询。以下 LINQ 查询展示了如何使用 LINQ 聚合数据

var raw = new[] {    new { Title = "first", Stat = 20, Type = "view" },
                     new { Title = "first", Stat = 12, Type = "enquiry" },
                     new { Title = "first", Stat = 0, Type = "click" },
                     new { Title = "second", Stat = 31, Type = "view" },
                     new { Title = "second", Stat = 17, Type = "enquiry" },
                     new { Title = "third", Stat = 23, Type = "view" },
                     new { Title = "third", Stat = 14, Type = "click" }
        };

var groupeddata = from data in raw
                       group data by data.Title
                       into grouped
                  select new {    Title = grouped.Key,
                                  Count = grouped.Count()
                             };

foreach (var data in groupeddata){
    Console.WriteLine("Title = {0}\t Count={1}", data.Title, data.Count);
}

嵌套查询

LINQ 允许您使用嵌套查询。一旦您从集合中选择了实体,您可以将它们用作内部查询的一部分,该内部查询可以在其他集合上执行。例如,您可以看到上面显示的类图,其中 Book 类引用了 Publisher 类,但没有反向关系。使用嵌套 LINQ 查询,您可以在集合中选择所有出版商,并为每个出版商实体调用其他 LINQ 查询,这些查询查找所有引用该出版商的书籍。此类查询的示例如下所示

var publisherWithBooks = from publisher in SampleData.Publishers
                     select new { Publisher = publisher.Name,
                                  Books =  from book in SampleData.Books
                                           where book.Publisher == publisher
                                           select book
                                 };

foreach (var publisher in publisherWithBooks){
    Console.WriteLine("Publisher - {0}", publisher.Name);
    foreach (Book book in publisher.Books)
        Console.WriteLine("\t Title \t{0}", book.Title);
}

当在查询中创建新实例时,对于每个出版商实体,都会获取 LINQ 查询中设置的书籍集合,并在控制台上显示。

使用局部变量,您可以为查询提供更好的格式,如下面示例所示

var publisherWithBooks = from publisher in SampleData.Publishers
                         let publisherBooks = from book in SampleData.Books
                                              where book.Publisher == publisher
                                              select book
                         select new { Publisher = publisher.Name, 
                                      Books = publisherBooks
                                    };

foreach (var publisher in publisherWithBooks){
    Console.WriteLine("Publisher - {0}", publisher.Name);
    foreach (Book book in publisher.Books)
        Console.WriteLine("\t Title \t{0}", book.Title);
}

在此查询中,当前出版商的书籍被放置在 publisherBooks 变量中,然后返回一个包含出版商名称及其书籍的对象。

通过这种方式,您可以动态地在原始类模型中不存在的实体之间创建新的关系。

结论

在实体集合上使用 LINQ 可以显著改善您的代码。一些常见的集合操作,如过滤、排序、查找最小值或最大值,可以使用单个函数调用或查询完成。此外,许多 LINQ 功能使您能够以类似 SQL 的方式使用集合,从而允许您像在标准 SQL 中一样连接和分组集合。如果没有 LINQ,对于这种功能,您可能需要创建几个复杂的函数,但现在有了 LINQ 库,您可以用一条语句完成它。

如果您对改进本文或 LINQ 查询的一些有趣用法有任何建议,请告诉我,我将在此处添加它们。

历史

  • 2012年2月15日:初版
© . All rights reserved.