LINQ(隐式表达式、lambda 表达式)

时间:2021-01-25 16:47:12

.NET 中一项突破性的创新是 LINQ(Language Integrated Query,语言集成查询),这组语言扩展让你能够不必离开舒适的 C# 语言执行查询。

LINQ 定义了用于构建查询表达式的关键字。这些查询表达式能够对数据进行选择、过滤、排序、分组和转换。借助各种 LINQ 扩展,你可以对不同的数据源使用相同的查询表达式。

       虽然你可以在任意地方使用 LINQ ,但是只有 ASP.NET 应用程序中最可能把 LINQ 用作数据库组件的一部分。你可以和 ADO.NET 数据访问代码一起使用 LINQ ,或者借助 LINQ to Entities 取代 ADO.NET 数据访问代码。

LINQ 基础

接近 LINQ 最简单的方法时了解它是如何针对内存集合工作的,这就是 LINQ to Objects,最简单形式的 LINQ。

就本质而言,LINQ to Objects 能够使用声明性的 LINQ 表达式代替逻辑(如 foreach 块):

EmployeeDB db = new EmployeeDB();
protected void btnForeach_Click(object sender, EventArgs e)
{
    List<EmployeeDetails> employees = db.GetEmployees();
    List<EmployeeDetails> matches = new List<EmployeeDetails>();
    foreach (EmployeeDetails employee in employees)
    {
        if (employee.LastName.StartsWith("D"))
        {
            matches.Add(employee);
        }
    }
    gridEmployees.DataSource = matches;
    gridEmployees.DataBind();
}
 
protected void btnLINQ_Click(object sender, EventArgs e)
{
    List<EmployeeDetails> employees = db.GetEmployees();
    IEnumerable<EmployeeDetails> matches;
    matches = from employee in employees
              where employee.LastName.StartsWith("D")
              select employee;
    gridEmployees.DataSource = matches;
    gridEmployees.DataBind();
}

LINQ(隐式表达式、lambda 表达式)

延迟执行

使用 foreach 块的代码和使用 LINQ 表达式的代码的一个明显的区别在于它们处理匹配集合类型方式。对于 foreach ,匹配的集合被创建为一个特定类型的集合(强类型 List<T>),在 LINQ 的示例中,匹配的集合仅通过它所实现的 IEnumerable<T> 接口暴露。

产生这种差别的原因在于 LINQ 使用了延迟执行。可能和你预期的不同,匹配的对象并不是一个包含了匹配的 EmployeeDetails 对象的直观集合,而是一个特殊的 LINQ 对象,能够在你需要的时候抓取数据。

根据查询表达式的不同,LINQ 表达式可以返回不同的对象,例如:WhereListIterator<T>、UnionIterator<T>、SelectIterator<T> 等,因为通过 IEnumerable<T> 接口和结果交互,因此不必要知道代码所使用的具体迭代类。

LINQ 是如何工作的

  • 要使用 LINQ ,需要创建一个 LINQ 表达式
  • LINQ 表达式的返回值是一个实现了 IEnumerable<T> 的迭代器对象
  • 对迭代器对象进行枚举时,LINQ 执行它的工作

问:LINQ 是如何执行表达式的?为了产生过滤结果,它究竟做了什么?

答:依据你所查询的数据类型的不同而不同。LINQ to Entities 把 LINQ 表达式转换为数据库命令,所以 LINQ to Entities 需要打开一个数据库连接并执行一次数据库查询以获得你所请求的数据。如果是前一个示例中使用的是 LINQ to Objects,LINQ 执行的过程就简单多了,实际上此时 LINQ 只是使用了一个 foreach 循环从头到尾遍历集合。

LINQ 表达式

虽然重新调整了子句的顺序,但 LINQ 表达式和 SQL 查询表面上还是很相似。

所有 LINQ 表达式都必须有一个指定数据源的 from 子句并有一个表示要获取的数据的 select 子句(或者一个定义了数据要放入组的 group 子句)。

       from 子句要放在最前面,from 子句确定了两部分信息。紧随 in 之后的单词表明了数据源,紧随 from 之后的单词为数据源中的每个个体提供一个假名:

matches = from employee in employees

下面是一个简单的 LINQ 查询,从 employees 集合中获取所有数据:

matches = from employee in employees
          select employee;

提示:

可以在微软的 101 LINQ 示例中(http://code.msdn.microsoft.com/101-LINQ-Samples-3fb9811b)找到各种表达式的示例。

1. 投影

可以修改 select 子句获取一组数据。

// Sample 1
IEnumerable<string> matches;
matches = from employee in employees
          select employee.FirstName;
 
// Sample 2
IEnumerable<string> matches;
matches = from employee in employees
          select employee.FirstName + employee.LastName ;

当你选中信息时,可以对数值数据或字符串数据使用标准的 C# 操作符对齐进行修改。更有意思的是,可以动态定义一个新类以封装返回的信息。C# 匿名类可以做到这一点,技巧是向 select 子句中添加一个 new 关键字并把选择的内容以对象的形式赋给属性:

var matches = from employee in employees
              select new { First = employee.FirstName, Last = employee.LastName };

这个表达式在执行的时候返回一组隐式创建的类的对象。你不会看到类的定义并且不能把实例传给方法调用,因为它由编译器生成,并且具有自动创建的无意义的名称。不过,你可以在本地使用该类,访问 First 和 Last 属性,甚至结合数据绑定使用它(此时 ASP.NET 使用反射根据属性名称获取对应的值)。

把正在查询的数据转换为各种结构的能力被称为投影。

引用独立的对象也要使用关键字 var ,比如对前面的结果进行迭代:

foreach (var employee in matches)
{
    // 可以读取 First 和 Last
}

当然了,执行投影的时候,并非只能使用匿名类。你可以正式定义类型,然后在表达式中使用它:

public class EmployeeName
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
 
    public EmployeeName(string firstName, string lastName)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }
}
 
IEnumerable<EmployeeName> matches = from employee in employees
    select new EmployeeName { FirstName = employee.FirstName, 
    LastName = employee.LastName };

上述的表达式之所以能够工作,是因为 FirstName 和 LastName 属性可以被公共访问且不是只读的。创建 EmployeeName 对象后 LINQ 设置这些属性。另外,你也可以在 EmployeeName 类名称后的括号里为参数化的构造函数提供其他的参数:

IEnumerable<EmployeeName> matches = from employee in employees
        select new EmployeeName(employee.FirstName, employee.LastName);

2. 过滤和排序

IEnumerable<EmployeeDetails> matches;
matches = from employee in employees
          where employee.LastName.StartsWith("D")
          select employee;

where 子句接受一个条件表达式,它针对每个项目进行计算。如果结果为 true ,该项目就被包含到结果中。不过,LINQ 使用相同的延迟执行模型,也就是说,直到对结果集进行迭代时才会对 where 子句进行计算。

你或许已经猜到,能够用逻辑与(&&)以及逻辑或(||)操作符组合多个条件表达式并且能够使用关系操作符(如 <、<=、>、>=):

IEnumerable<Product> matches;
matches = from product in products
          where product.UnitsInStock > 0 && product.UnitPrice > 3.00
          select product;

LINQ 表达式一个有意思的特性是让你能够随时调用自己的方法。例如,可以创建一个检查员工的函数 TestEmployee(),根据它是否在结果集中返回 true 或 false:

private bool TestEmployee(EmployeeDetails employee)
{
    return employee.LastName.StartsWith("D");
}

然后,可以这样使用:

IEnumerable<EmployeeDetails> matches;
matches = from employee in employees
          where TestEmployee(employee)
          select employee;

orderby 操作符同样很直观。它的模型基于 SQL 里的查询语句。你只要为排序提供一个或多个以逗号分隔的值列表(最后可加 decending 降序):

IEnumerable<EmployeeDetails> matches;
matches = from employee in employees
          orderby employee.LastName,employee.FirstName
          select employee;

注解:

所有实现了 IComparable 的类型都支持排序,它是 .NET 最核心的数据类型之一(如 数值、日期、字符串)。你也可以传递一个自定义的 IComparable 对象对数据进行排序。

3. 分组和聚合

分组让你把大量的信息浓缩为精简的概要。

分组是一种投影,因为结果集中的对象和数据源集合中的对象是不一样的。例如,假设你正在处理一组 Product 对象,并决定把它们放到特定价格的组中。最终的结果是分组对象的 IEnumerable<T> 集合,其中每个对象代表某个价格区间内的特定产品。每个组实现 System.Linq 命名空间的 IGrouping<T,K> 接口。

使用分组,首先需要做两个决定:

  • 创建分组的条件
  • 每个组显示什么信息

第一个任务比较简单。使用 group、by 以及 into 关键字选择要分组的对象,确定如何分组并决定引用每个分组时要使用的假名:

var matches = from employee in employees
              group employee by employee.TitleOfCourtesy into g
              ...
// 在 LINQ 中使用 g 作为分组假名是一个常见的约定

具有相同数据的对象被放到了同一个组里。要把数据按数值范围进行分组,需要编写一段计算代码以便为每个组产生相同的数值。例如,要把产品按每个价格区间是 50 元 进行分组:

var matches = from product in products
              group product by (int)(product.UnitPrice / 50) into g
              ...

现在,所有几个低于 50 元的产品的分组键为 0,而价格在 50 - 100 之间的产品的分组键为 1,以此类推。

得到分组后,还要确定分组的结果返回什么样的信息。每个组以实现了 IGrouping<T,K> 接口的对象的形式暴露给代码。例如,前一个表达式创建 IGrouping<int,Product> 类型的组,也就是说,分组的键值类型是整形,而其中的元素类型是 Product 。

       IGrouping<T,K> 接口只提供一个属性 Key ,它用于返回创建组的值。例如,如果要创建显示每个 TitleOfCourtesy 组的 TitleOfCourtesy 的字符串列表,应该使用这样的表达式:

var matches = from emp in employees
              group emp by emp.TitleOfCourtesy into g
              select g.Key;

LINQ(隐式表达式、lambda 表达式)

提示:

       对于这个示例,也可以使用 IEnumerable<string> 替代 var 关键字,因为最终的结果是一系列字符串。然后,通常在分组查询中使用 var 关键字,因为通常需要使用投影和匿名类获取更多有用的汇总信息。

另外,也可以返回整个组:

var matches = from emp in employees
              group emp by emp.TitleOfCourtesy into g
              select g;

这对数据绑定没有任何用处。因为 ASP.NET 不能够显示关于每个组的任何有用信息。但是,它让你可以很方便的对每个组的数据进行*迭代:

foreach (IGrouping<string, EmployeeDetails> group in matches)
{
    foreach (EmployeeDetails emp in group)
    {
        // do something
    }
}

这段代码说明即使创建了组,还是能够灵活的访问组里的每个项目。

从更实用的角度来说,可以使用聚合函数对组里的数据进行计算。LINQ 聚合函数模仿了过去可能已用到的数据库聚合函数,允许对组中的元素进行技术和汇总,获取最小值、最大值及平均值。

下面的示例返回一个匿名类型,它包含分组的键值以及分组中对象的个数,使用一个嵌入的方法 Count():

var matches = from emp in employees
              group emp by emp.TitleOfCourtesy into g
              select new { Title = g.Key, Employees = g.Count() };

LINQ(隐式表达式、lambda 表达式)

上一个示例有一点需要注意,它使用了一个扩展方法。从本质上说,扩展方法是 LINQ 的一组核心功能,它们不是通过专门的 C# 操作符公开,而是需要直接调用这些方法。

扩展方法和普通方法的区别在于扩展方法不是定义在使用该方法的类里。LINQ 有一个 System.Linq.Enumerable 类,它定义了几十个扩展方法,这些方法可被所有实现了 IEnumerable<T> 的对象调用。

除了 Count(),LINQ 还定义了大量可在分组中应用的强大的扩展方法,比如聚合函数 Max()、Min()、Average()等。使用了这些方法的 LINQ 表达式会更加复杂,因为它们还使用了另一个被称为 lambda 表达式的 C# 特性,它允许为扩展方法提供其他参数。对于 Max()、Min()、Average(),lambda 表达式允许你指定用于计算的属性。

这个示例用于计算每个类别中项目的最高价格、最低价格以及平均价格:

var categories = from p in products
                 group p by p.Category into g
                 select new
                 {
                     Category = g.Key,
                     MaxPrice = g.Max(p => p.UnitPrice),
                     MinPrice = g.Min(p => p.UnitPrice),
                     AvgPrice = g.Average(p => p.UnitPrice)
                 };

揭秘 LINQ 表达式

虽然 LINQ 使用了新的 C# 关键字(如 from、in 和 select),但这些关键字的实现是由其他类提供的。实际上,所有的 LINQ 查询都被转换为一组方法的调用。除了借助转换,还可以直接调用这些方法:

var matches = from emp in employees
              select emp;
 
// 上面的表达式可以改写成这样:
var matches = employees.Select(employee => employee);

这里所用的语法不太常见。代码看起来像是在调用 employees 集合的 Select()方法。然而,employees 集合是一个普通的 List<T> 集合,它并没有包含这个方法。相反,Select()是一个扩展方法,它被自动提供给所有的 IEnumerable<T> 类。

1. 扩展方法

扩展方法让你能够在一个类里定义方法,然后就好像它也在其他类里定义了那样调用它。LINQ 扩展方法定义在了 System.Linq.Enumerable 类里,但是所有的 IEnumerable<T> 对象都可调用。

注解:

因为 LINQ 扩展方法是定义在 System.Linq.Enumerable 类里的,因此该类必须处于可用范围。

查看一下 Select()方法的定义:

public static IEnumerable<TResult> Select<TSource, TResult>(
    this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{ ... }

扩展方法需要遵守一些规则:

  • 所有的扩展方法都必须是静态的
  • 扩展方法可用返回任意的数据类型并可以接收任意个数的参数
  • 第一个参数必须是对调用扩展方法的对象的引用(并且之前跟着关键字 this)
  • 该参数的数据类型决定了扩展方法可用的类

Select()方法可以被所有实现了 IEnumerable<T> 的类的示例调用(this IEnumerable<TSource> source 参数决定)。另一个参数,用于获取正在选择的信息段的委托。返回值是一个 IEnumerable<T> 对象。

2. lambda 表达式

lambda 表达式在 C# 里是基于方法的 LINQ 表达式的新语法。lambda 表达式像这样传递给 Select()方法:

matches = employees.Select(employee => employee);

当 Select()被调用时,employees 对象作为第一个参数传递,它是查询的源。第二个参数需要一个指向某个方法的委托。这个方法执行选择任务,且被集合里的每个元素调用。

Select()方法接收一个委托。你可以提供一个普通委托(它指向定义在类里的其他地方的命名方法),但这样做的话会使你的代码变的冗长。

一个更简单的解决办法是使用匿名方法,匿名方法以 delegate 开头,然后是方法签名的声明,随后的花括号里是该方法的代码。如果使用匿名方法,前面的示例看起来应该是这个样子的:

IEnumerable<EmployeeDetails> matches = employees
    .Select(
        delegate(EmployeeDetails emp)
        {
            return emp;
        }
    );

lambda 表达式就是让这类代码看起来更加简练的一种方式。lambda 表达式由以 => 分隔的两部分组成。第一部分表示匿名方法接收的参数,对于这个示例,lambda 表达式接收集合里的每个对象并通过名为 employee 的引用暴露它们。lambda 表达式的第二部分定义要返回的值。

下面这个显式的 LINQ 表达式从每个 employee 里析取数据并封装成一个匿名类型返回:

var matches = employees
    .Select(
            delegate(EmployeeDetails employee)
            {
                return new
                {
                    First = employee.FirstName,
                    Last = employee.LastName
                };
            }
    );

现在,你可以用 lambda 表达式来简化代码:

var matches = employees.Select(employee =>
    new { First = employee.FirstName, Last = employee.LastName });

3. Multipart 表达式

当然,多数 LINQ 表达式要比我们这里讲过的示例更加复杂。一个更为现实的 LINQ 表达式可能会加入排序或过滤。

比如下面这段代码:

matches = from employee in employees
          where employee.LastName.StartsWith("D")
          select employee;

可以用显示的语法把这个表达式重写为:

matches = employees
    .Where(employee => employee.LastName.StartsWith("D"))
    .Select(employee => employee);

显式 LINQ 语法的一个好处是它使操作符顺序更加明确。对于前一个示例,可以很清楚的看到它从 employees 集合开始,然后调用 Where(),最后调用 Select()。如果要使用更多的操作符,将会面临更长的一组方法调用。

Where()提供一个 lambda 表达式验证每个元素,如果它应该包含在结果中,则返回 true。

Select()提供一个 lambda 表达式用于把每个数据项转换为你期望的形式。

大多数情况下,都使用隐式语法创建 LINQ 表达式。但是,偶尔还是要用到显式语法。例如,需要向扩展方法传递一个不被隐式 LINQ 语法支持的参数时。

理解表达式如何映射到方法的调用、扩展方法如何绑定到 IEnumerable<T> 对象、lambda 表达式怎样封装过滤、排序、投影等。这些使得 LINQ 的内部工作细节变得清晰。