标准查询运算符概述

标准查询运算符是构成 LINQ 模式的关键字和方法。 C# 语言定义用于最常见查询表达式的 LINQ 查询关键字 。 编译器使用这些关键字将表达式转换为等效的方法调用。 这两种形式是同义词。 属于 System.Linq 命名空间的其他方法没有等效的查询关键字。 在这些情况下,必须使用方法语法。 本部分介绍所有查询运算符关键字。 运行时和其他 NuGet 包添加了更多旨在处理每个版本的 LINQ 查询的方法。 本部分介绍了最常见的方法,包括具有查询关键字等效项的方法。 有关 .NET 运行时支持的查询方法的完整列表,请参阅 System.Linq.Enumerable API 文档。 除了此处介绍的方法之外,此类还包含用于连接数据源、计算数据源中的单个值的方法,例如总和、平均值或其他值。

重要

这些示例使用 System.Collections.Generic.IEnumerable<T> 数据源。 基于System.Linq.IQueryProvider的数据源使用System.Linq.IQueryable<T> 数据源和表达式树。 表达式树对允许的 C# 语法有 限制 。 此外,每个 IQueryProvider 数据源(如 EF Core )可能会施加更多的限制。 查看数据源的文档。

其中大多数方法对序列进行作,其中序列是一个对象,其类型实现 IEnumerable<T> 接口或 IQueryable<T> 接口。 标准查询运算符提供查询功能,包括筛选、投影、聚合、排序等。 构成每个集的方法分别是 EnumerableQueryable 类的静态成员。 它们被定义为它们所操作的类型的扩展方法

序列 IEnumerable<T>IQueryable<T> 之间的区别决定了查询在运行时的执行方式。

对于 IEnumerable<T>,返回的可枚举对象捕获传递给该方法的参数。 枚举该对象时,会使用查询运算符的逻辑并返回查询结果。

对于IQueryable<T>,查询被转换为表达式树。 当数据源可以优化查询时,表达式树可以转换为本机查询。 实体框架等库将 LINQ 查询转换为在数据库中执行的本机 SQL 查询。

下面的代码示例演示如何使用标准查询运算符获取有关序列的信息。

string sentence = "the quick brown fox jumps over the lazy dog";
// Split the string into individual words to create a collection.
string[] words = sentence.Split(' ');

// Using query expression syntax.
var query = from word in words
            group word.ToUpper() by word.Length into gr
            orderby gr.Key
            select new { Length = gr.Key, Words = gr };

// Using method-based query syntax.
var query2 = words.
    GroupBy(w => w.Length, w => w.ToUpper()).
    Select(g => new { Length = g.Key, Words = g }).
    OrderBy(o => o.Length);

foreach (var obj in query)
{
    Console.WriteLine($"Words of length {obj.Length}:");
    foreach (string word in obj.Words)
        Console.WriteLine(word);
}

// This code example produces the following output:
//
// Words of length 3:
// THE
// FOX
// THE
// DOG
// Words of length 4:
// OVER
// LAZY
// Words of length 5:
// QUICK
// BROWN
// JUMPS

如果可能,本节中的查询将使用单词或数字序列作为输入源。 对于使用对象间关系更复杂的查询,使用以下为学校建模的源:

public enum GradeLevel
{
    FirstYear = 1,
    SecondYear,
    ThirdYear,
    FourthYear
};

public class Student
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required int ID { get; init; }

    public required GradeLevel Year { get; init; }
    public required List<int> Scores { get; init; }

    public required int DepartmentID { get; init; }
}

public class Teacher
{
    public required string First { get; init; }
    public required string Last { get; init; }
    public required int ID { get; init; }
    public required string City { get; init; }
}

public class Department
{
    public required string Name { get; init; }
    public int ID { get; init; }

    public required int TeacherID { get; init; }
}

每个 Student 都有一个年级、一个主要部门和一系列分数。 A Teacher 还有一个 City 属性,用于标识教师授课的校园。 Department 有一个名称,以及对担任院系主任的 Teacher 的引用。

可以在 源存储库中找到数据集。

查询运算符的类型

标准查询运算符在执行时机上有所不同,具体取决于它们是返回单个值还是值序列。 返回单例值的方法(如 AverageSum)会立即执行。 返回序列的方法延迟查询执行并返回可枚举对象。 可以将一个查询的输出序列用作另一个查询的输入序列。 对查询方法的调用可以在一个查询中链接在一起,从而使查询变得任意复杂。

查询运算符

在 LINQ 查询中,第一步是指定数据源。 在 LINQ 查询中,子 from 句首先引入数据源(students)和 范围变量student)。

//queryAllStudents is an IEnumerable<Student>
var queryAllStudents = from student in students
                        select student;

范围变量类似于循环中的 foreach 迭代变量,只是查询表达式中没有发生实际迭代。 执行查询时,范围变量充当对每个连续元素的 students引用。 由于编译器可以推断其 student类型,因此无需显式指定。 您可以在 let 子句中新增更多范围变量。 有关详细信息,请参阅 let 子句

注释

对于非泛型数据源,例如 ArrayList,必须显式键入范围变量。 有关详细信息,请参阅如何使用 LINQ (C#)From 子句查询 ArrayList。

获取数据源后,可以对该数据源执行任意数量的作:

  • 使用关键字where
  • 使用和可选orderby关键字descending
  • 使用和可选group关键字into
  • 使用关键字联接join
  • 使用 关键字的select

查询表达式语法表

下表列出了具有等效查询表达式子句的标准查询运算符。

方法 C# 查询表达式语法
Cast 使用显式类型化的区间变量:

from int i in numbers

(有关详细信息,请参阅 from 子句。)
GroupBy group … by

-或-

group … by … into …

(有关详细信息,请参阅 分组子句。)
GroupJoin<TOuter,TInner,TKey,TResult>(IEnumerable<TOuter>, IEnumerable<TInner>, Func<TOuter,TKey>, Func<TInner,TKey>, Func<TOuter,IEnumerable<TInner>, TResult>) join … in … on … equals … into …

(有关详细信息,请参阅 join 子句。)
Join<TOuter,TInner,TKey,TResult>(IEnumerable<TOuter>, IEnumerable<TInner>, Func<TOuter,TKey>, Func<TInner,TKey>, Func<TOuter,TInner,TResult>) join … in … on … equals …

(有关详细信息,请参阅 join 子句。)
OrderBy<TSource,TKey>(IEnumerable<TSource>, Func<TSource,TKey>) orderby

(有关详细信息,请参阅 orderby 子句。)
OrderByDescending<TSource,TKey>(IEnumerable<TSource>, Func<TSource,TKey>) orderby … descending

(有关详细信息,请参阅 orderby 子句。)
Select select

(有关详细信息,请参阅 let 子句。)
SelectMany 多个 from 子句。

(有关详细信息,请参阅 from 子句。)
ThenBy<TSource,TKey>(IOrderedEnumerable<TSource>, Func<TSource,TKey>) orderby …, …

(有关详细信息,请参阅 orderby 子句。)
ThenByDescending<TSource,TKey>(IOrderedEnumerable<TSource>, Func<TSource,TKey>) orderby …, … descending

(有关详细信息,请参阅 orderby 子句。)
Where where

(有关详细信息,请参阅 where 子句

使用 LINQ 进行数据转换

Language-Integrated 查询(LINQ)不仅与检索数据有关。 它也是用于转换数据的强大工具。 通过使用 LINQ 查询,可以使用源序列作为输入,并通过多种方式对其进行修改以创建新的输出序列。 可以通过排序和分组来修改序列本身,而无需修改元素本身。 但也许 LINQ 查询最强大的功能是能够创建新类型。 select 子句从输入元素创建输出元素。 使用它将输入元素转换为输出元素:

  • 将多个输入序列合并到具有新类型的单个输出序列中。
  • 创建其元素仅包含源序列中每个元素的一个或多个属性的输出序列。
  • 创建输出序列,其元素包含对源数据执行的作的结果。
  • 创建采用不同格式的输出序列。 例如,可以将 SQL 行或文本文件中的数据转换为 XML。

可以在同一查询中以各种方式组合这些转换。 此外,一个查询的输出序列可用作新查询的输入序列。 以下示例将内存中数据结构中的对象转换为 XML 元素。


// Create the query.
var studentsToXML = new XElement("Root",
    from student in students
    let scores = string.Join(",", student.Scores)
    select new XElement("student",
                new XElement("First", student.FirstName),
                new XElement("Last", student.LastName),
                new XElement("Scores", scores)
            ) // end "student"
        ); // end "Root"

// Execute the query.
Console.WriteLine(studentsToXML);

该代码生成以下 XML 输出:

<Root>
  <student>
    <First>Svetlana</First>
    <Last>Omelchenko</Last>
    <Scores>97,90,73,54</Scores>
  </student>
  <student>
    <First>Claire</First>
    <Last>O'Donnell</Last>
    <Scores>56,78,95,95</Scores>
  </student>
  ...
  <student>
    <First>Max</First>
    <Last>Lindgren</Last>
    <Scores>86,88,96,63</Scores>
  </student>
  <student>
    <First>Arina</First>
    <Last>Ivanova</Last>
    <Scores>93,63,70,80</Scores>
  </student>
</Root>

有关详细信息,请参阅在 C# 中创建 XML 树(LINQ to XML)。

可以将一个查询的结果用作后续查询的数据源。 此示例演示如何对联接作的结果进行排序。 此查询创建组联接,然后根据仍在范围内的类别元素对组进行排序。 在匿名类型初始化器中,子查询对产品序列中的所有匹配元素进行排序。

var orderedQuery = from department in departments
                   join student in students on department.ID equals student.DepartmentID into studentGroup
                   orderby department.Name
                   select new
                   {
                       DepartmentName = department.Name,
                       Students = from student in studentGroup
                                  orderby student.LastName
                                    select student
                   };

foreach (var departmentList in orderedQuery)
{
    Console.WriteLine(departmentList.DepartmentName);
    foreach (var student in departmentList.Students)
    {
        Console.WriteLine($"  {student.LastName,-10} {student.FirstName,-10}");
    }
}
/* Output:
Chemistry
  Balzan     Josephine
  Fakhouri   Fadi
  Popov      Innocenty
  Seleznyova Sofiya
  Vella      Carmen
Economics
  Adams      Terry
  Adaobi     Izuchukwu
  Berggren   Jeanette
  Garcia     Cesar
  Ifeoma     Nwanneka
  Jamuike    Ifeanacho
  Larsson    Naima
  Svensson   Noel
  Ugomma     Ifunanya
Engineering
  Axelsson   Erik
  Berg       Veronika
  Engström   Nancy
  Hicks      Cassie
  Keever     Bruce
  Micallef   Nicholas
  Mortensen  Sven
  Nilsson    Erna
  Tucker     Michael
  Yermolayeva Anna
English
  Andersson  Sarah
  Feng       Hanying
  Ivanova    Arina
  Jakobsson  Jesper
  Jensen     Christiane
  Johansson  Mark
  Kolpakova  Nadezhda
  Omelchenko Svetlana
  Urquhart   Donald
Mathematics
  Frost      Gaby
  Garcia     Hugo
  Hedlund    Anna
  Kovaleva   Katerina
  Lindgren   Max
  Maslova    Evgeniya
  Olsson     Ruth
  Sammut     Maria
  Sazonova   Anastasiya
Physics
  Åkesson    Sami
  Edwards    Amy E.
  Falzon     John
  Garcia     Debra
  Hansson    Sanna
  Mattsson   Martina
  Richardson Don
  Zabokritski Eugene
*/

以下代码显示了使用方法语法的等效查询:

var orderedQuery = departments
    .GroupJoin(students, department => department.ID, student => student.DepartmentID,
    (department, studentGroup) => new
    {
        DepartmentName = department.Name,
        Students = studentGroup.OrderBy(student => student.LastName)
    })
    .OrderBy(department => department.DepartmentName);


foreach (var departmentList in orderedQuery)
{
    Console.WriteLine(departmentList.DepartmentName);
    foreach (var student in departmentList.Students)
    {
        Console.WriteLine($"  {student.LastName,-10} {student.FirstName,-10}");
    }
}

尽管可以在联接前对一个或多个源序列使用 orderby 子句,但通常我们不推荐这样做。 某些 LINQ 提供程序可能不会在联接后保留该排序。 有关详细信息,请参阅 join 子句

另请参阅