# 七、使用 LINQ 查询对象

语言集成查询(LINQ)允许您使用类似 SQL 的语法来查询数据。LINQ 可以用于许多不同类型的数据，微软和第三方都建立了 LINQ 提供商来访问广泛的数据源。本章通过向您展示如何使用 LINQ 对象来缩小列表范围。一旦你知道了 LINQ to Objects，理解其他 LINQ 提供者就很容易了，因为有相似的语法。

## 开始

在编写任何 LINQ 代码之前，记得在文件顶部的`System.Linq`名称空间中添加一个`using`声明。本章中的每个示例都将使用以下类，其中包含要使用的集合:

```cs
    using System.Collections.Generic;

    public class Customer
    {
        public int ID { get; set; }
        public string Name { get; set; }
    }
    public class Order
    {
        public int CustomerID { get; set; }
        public string Description { get; set; }
    }

    public static class Company
    {
        static Company()
        {
            Customers = new List<Customer>
            {
                new Customer { ID = 0, Name = "May" },
                new Customer { ID = 1, Name = "Gary" },
                new Customer { ID = 2, Name = "Jennifer" }
            };
            Orders = new List<Order>
            {
                new Order { CustomerID = 0, Description = "Shoes" },
                new Order { CustomerID = 0, Description = "Purse" },
                new Order { CustomerID = 2, Description = "Headphones" }
            };
        }

        public static List<Customer> Customers { get; set; }
        public static List<Order> Orders { get; set; }
    }

```

代码清单 95

这些是内存中对象的集合。为了使集合更容易查询，`Company`是一个`static`类，带有一个初始化`static`属性的`static`构造函数。如果抽象这个概念，数据可能是从文件、数据库或 REST 服务中读取的。无论数据源或 LINQ 提供商如何，基本的 LINQ 语法都保持不变。

## 查询集合

查询数据只需要`from`和`select`两个关键词。记得为`System.Linq`命名空间添加一个`using`子句。语法看起来像 SQL，如下面的例子所示。

```cs
    using System;
    using System.Linq;
    using System.Collections.Generic;

    public class Program
    {
        public void Main()
        {
            IEnumerable<Customer> customers =
                from cust in Company.Customers
                select cust;

            foreach (Customer cust in customers)
                Console.WriteLine(cust.Name);
        }
    }

```

代码清单 96

LINQ 对对象的查询导致类型`IEnumerable<T>`的集合。在这种情况下，它是`Customer`对象的集合。`from`关键字指定了一个范围变量`cust`，它保存集合中的每个对象。您可以在`in`关键字后指定集合。

`select`定义要查询的内容。在这个例子中，你只是返回了整个对象。事实上，你得到的收藏和`Company.Customers`里的是一样的。这在对象的 LINQ 中并不特别有用，但是如果数据是从外部数据源读取的，就非常有用，比如一个数据库，您只想将一组对象放入内存中进行进一步操作。`select`允许你将你得到的数据重新组合成各种预测。以下是获取客户名称的查询。

```cs
            IEnumerable<string> customers2 =
                from cust2 in Company.Customers
                select cust2.Name;

```

代码清单 97

`select`使用`cust2`变量访问`Name`，得到一个`string`(T4)属性类型的集合。有时，您需要一个完全不同的对象，该对象可以定义为:

```cs
    public class CustomerViewModel
    {
        public string Name { get; set; }
    }

```

代码清单 98

一个新的投影可以写成:

```cs
            IEnumerable<CustomerViewModel> customerVMs =
                from custVM in Company.Customers
                select new CustomerViewModel
                {
                    Name = custVM.Name
                };

```

代码清单 99

这里，`select`实例化了一个新的`CustomerViewModel`。然后，它使用对象初始化语法填充值，将`custVM.Name`分配给新对象的`Name`属性。这就产生了一个集合类型`CustomerViewModel`。

前面的示例假设您需要使用特定类型的集合。然而，如果你不关心集合是什么类型，如果你不想创建一个新的类仅仅是为了在一个算法中进行操作呢？在这种情况下，您可以使用匿名类型，如下面的清单所示。

```cs
            var customers3 =
                from cust3 in Company.Customers
                select new
                {
                    Name = cust3.Name
                };

            foreach (var cust3 in customers3)
                Console.WriteLine(cust3.Name);

```

代码清单 100

匿名类型没有可以使用的名称，尽管 C# 可能会创建一个内部名称供自己使用。要解决这个问题，请使用`var`关键字作为类型。注意投影如何使用没有类型名的`new`:匿名类型。您可以为匿名类型定义任何您想要的属性；把它们写进去。还要注意，您可以在`foreach`循环中使用`var`。

如果需要从方法返回集合，请创建一个新的(命名的)类型并将其投影到该类型中。匿名类型是为限制其使用范围的情况而设计的。您将在代码的其他地方看到`var`关键字，但是它被添加到语言中的原因是为了支持这种场景。下面的列表显示了使用`var`的一种常见方式，不同于前面的场景。

```cs
    var customer = new Customer();

```

代码清单 101

前面的语句比指定变量的对象类型要短，这种情况下是多余的，明显是`Customer`。然而，下面的例子不太明显。

```cs
    var response = DoSomethingAndReturnResults();

```

代码清单 102

前面陈述的问题是，仅仅阅读代码并不能告诉你`var`是什么类型。你不知道它是一个单一的对象还是一个集合。在这种情况下，通过指定类型，代码可能更容易维护。

| ![](img/note.png) | 注意:一个常见的误解是 var 很危险，因为它的行为类似于 object，允许您将变量设置为任何类型。这不是真的。当您使用 var 时，代码仍然是强类型的。一旦为 var 类型的变量赋值，就不能为该变量赋值任何其他类型。在前面的示例中，customers 是 Customer 类型的一个实例。您不能在以后编写代码来将另一个实例类型(例如，订单类型)的对象分配给该变量。 |

## 过滤数据

您可以使用`where`子句过滤集合，如下例所示。

```cs
            var customers4 =
                from cust4 in Company.Customers
                where cust4.Name.Length > 3 && !cust4.Name.StartsWith("G")
                select cust4;

            foreach (var cust4 in customers4)
                Console.WriteLine(cust4.Name);

```

代码清单 103

在前面的列表中，客户的姓名必须长于`3`，这将列表过滤到`Gary`和`Jennifer`。`&&`运算符右侧的子句过滤列表，甚至进一步过滤第一个字符不是`"G"`的名称。

在对象的 LINQ 中，您可以在`where`子句中使用逻辑运算符、用于分组的括号以及任何其他逻辑来创建复杂的条件来过滤结果。您甚至可以调用另一个方法来评估正在评估的当前对象。`where`条款的结果必须评估为`bool`。其他 LINQ 提供商可能会限制`where`子句中的表达式类型，因此您必须查看该特定提供商的文档才能了解更多信息。

## 订购收藏品

在 LINQ，`orderby`子句允许您对收集结果进行排序。下面的清单演示了这一点。

```cs
            var customers5 =
                from cust5 in Company.Customers
                orderby cust5.Name descending
                select cust5;

            foreach (var cust5 in customers5)
                Console.WriteLine(cust5.Name);

```

代码清单 104

在本例中，`orderby`子句按照`descending`顺序按客户名称对列表进行排序。默认顺序是升序，您可以省略`descending`或指定`ascending`来代替。输出结果是:

`May`

`Jennifer`

`Gary`

## 连接对象

有时，数据库中会有两个不同的对象或相关表的集合，您需要将它们连接在一起。为此，请使用`join`子句。

```cs
            var customerOrders =
                from cust in Company.Customers
                join ord in Company.Orders
                    on cust.ID equals ord.CustomerID
                select new
                {
                    ID = cust.ID,
                    Customer = cust.Name,
                    Item = ord.Description
                };

            foreach (var custOrd in customerOrders)
                Console.WriteLine(
                    $"Customer: {custOrd.Customer}, Item: {custOrd.Item}");

```

代码清单 105

在`from`子句之后，您可以使用一个或多个`join`子句来访问您需要的类型。`on`关键字允许您指定要在表之间匹配的键。本示例在匿名类型上创建一个投影，以创建基于连接信息的报表。这是一个普通的连接，省略了任何没有匹配的`Order`的`Customers`。下面的示例允许您执行相当于左连接的操作。

```cs
            var customerOrders2 =
                from cust in Company.Customers
                join ord in Company.Orders.DefaultIfEmpty()
                    on cust.ID equals ord.CustomerID
                select new
                {

                    ID = cust.ID,
                    Customer = cust.Name,
                    Item = ord.Description
                };

            foreach (var custOrd2 in customerOrders)
                Console.WriteLine(
                    $"Customer: {custOrd2.Customer}, Item: {custOrd2.Item}");

```

代码清单 106

这里的不同之处在于对`DefaultIfEmpty`的调用，它包括带有`Name` `Gary`的`Customer`，尽管在连接中没有任何订单与他的`ID`匹配。

## 使用标准运算符

您已经看到了基本的 LINQ 语法，但是还有更多标准查询操作符。标准查询操作符实际上有几十个，你可以在 MSDN 上[https://msdn . Microsoft . com/en-us/library/vstudio/bb 397896(v = vs . 120)上查看全部。aspx](https://msdn.microsoft.com/en-us/library/vstudio/bb397896(v=vs.120).aspx) 。

下面的代码清单是一个示例包，演示了如何使用您可能会发现有用的标准查询运算符。

到目前为止，您一直在使用`IEnumerable<T>`，其中`T`是查询的预计类型。有一组标准的查询操作符会返回不同的集合类型，包括`ToList`、`ToArray`、`ToDictionary`等等。下面是一个将结果转化为`List`的例子。

```cs
            var custList =
                (from cust in Company.Customers
                 select cust)
                .ToList();
            custList.ForEach(cust => Console.WriteLine(cust.Name));

```

代码清单 107

前面的代码将查询括在括号中，然后调用`ToList`运算符。`List<T>`上的`ForEach`方法让你通过一个λ。

LINQ 查询使用延迟执行。这意味着直到您执行一个`foreach`循环或调用一个请求数据的标准查询操作符(如`ToList`)时，查询才会执行。

您已经看到了 C# `select`、`where`、`orderby`和`join`关键词如何帮助构建查询。这些查询中的每一个都有一个等效的标准查询运算符。这些标准查询操作符使用流畅的语法，并以不同的方式执行与其匹配语言语法相同的查询。有些人更喜欢流畅的风格，有些人更喜欢语言语法，但你选择的方法确实是个人偏好。以下是`Where`和`Select`运算符的示例，它们反映了`where`和`select`语言语法子句。

```cs
            var customers6 =
                Company.Customers
                       .Where(cust => cust.Name.StartsWith("J"));
            foreach (var cust6 in customers6)
                Console.WriteLine(cust6.Name);

            var customers7 =
                Company.Customers.Select(cust => cust.Name);
            foreach (var cust7 in customers7)
                Console.WriteLine(cust7);

```

代码清单 108

`Where`λ必须计算为一个`bool`，而`Select`λ可以让你指定投影。

您可以执行设置操作，如`Union`、`Except`和`Intersect`。以下清单是`Union`的一个例子。

```cs
            var additionalCustomers =
                new List<Customer>
                {
                    new Customer { ID = 1, Name = "Gary" }
                };
            var customerUnion =
                Company.Customers
                       .Union(additionalCustomers)
                       .ToArray();
            foreach (var cust in customerUnion)
                Console.WriteLine(cust.Name);

```

代码清单 109

只需传递一个兼容的集合，`Union`将产生所有对象的组合集合。我在这个例子中也使用了`ToArray`操作符，这导致了一个集合类型的数组，`Customer`。

选择`First`、`FirstOrDefault`、`Single`、`SingleOrDefault`、`Last`和`LastOrDefault`有一套有用的操作符。下面的例子演示了`First`。

```cs
            Console.WriteLine(Company.Customers.First().Name);

```

代码清单 110

以这种方式使用`First`的唯一一点是`InvalidOperationException`有可能带有消息“序列不包含元素。”这个序列包含元素，但这不能保证。使用带`OrDefault`后缀的运算符会更安全，如下列表所示。

```cs
            var empty =
                Company.Customers
                       .Where(cust => cust.ID == 999)
                       .SingleOrDefault();

            if (empty == null)
                Console.WriteLine("No values returned.");

```

代码清单 111

上例写`"No values returned."`因为没有客户带`ID == 999`，`SingleOrDefault`返回`null`，这是引用类型对象的默认值。

这些只是少数可用的操作符，但希望您对语言语法的丰富支持以及构成 LINQ 的标准查询操作符有所了解。

## 总结

LINQ 允许您使用类似 SQL 的语法来查询数据。本章中使用的 LINQ 提供程序是“LINQ 到对象”，它允许您查询内存中的对象，但是对于其他数据源，还有许多其他 LINQ 提供程序。使用`from`指定被查询的集合，使用`select`对结果进行整形。`where`子句允许您过滤结果，并使用`bool`表达式来评估是否应该包含给定的对象。`orderby`子句允许您对结果进行排序。`join`子句允许您组合两个集合。标准查询操作符扩展了 LINQ，使其比语言关键字更强大。