.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();
}

延迟执行

使用 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;

提示:

       对于这个示例,也可以使用 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 的一组核心功能,它们不是通过专门的 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 的内部工作细节变得清晰。

LINQ(隐式表达式、lambda 表达式)的更多相关文章

  1. Linq入门演练---(2)lambda表达式

    今天大家一同学习下lambda表达式, lambda表达式使用起来更方便, lambda表达式其实是一个匿名函数,使用的运算符为=> 语法: (参数)=>表达式 如果只有一个参数,可以不使 ...

  2. python---基础知识回顾(一)(引用计数,深浅拷贝,列表推导式,lambda表达式,命名空间,函数参数逆收集,内置函数,hasattr...)

    一:列表和元组(引用计数了解,深浅拷贝了解) 序列:序列是一种数据结构,对其中的元素按顺序进行了编号(从0开始).典型的序列包括了列表,字符串,和元组 列表是可变的(可以进行修改),而元组和字符串是不 ...

  3. LINQ教程三:Lambda表达式解剖

    C#3.0(.NET3.5)中引入了Lambda表达式和LINQ.Lambda表达式是使用一些特殊语法表示匿名方法的较短方法. 最基本的Lambda表达式语法如下: (参数列表)=>{方法体} ...

  4. Java基础进阶:内部类lambda重点摘要,详细讲解成员内部类,局部内部类,匿名内部类,Lambda表达式,Lambda表达式和匿名内部类的区别,附重难点,代码实现源码,课堂笔记,课后扩展及答案

    内部类lambda重点摘要 内部类特点: 内部类可以直接访问外部类,包括私有 外部类访问内部类必须创建对象 创建内部对象格式: 外部类.内部类 对象名=new外部类().new内部类(); 静态内部类 ...

  5. linq本质扩展方法+lambda表达式

    string[] names = { "aa","bb","cc","dd"}; /* IEnumerable<s ...

  6. C#3.0新增功能08 Lambda 表达式

    连载目录    [已更新最新开发文章,点击查看详细] Lambda 表达式是作为对象处理的代码块(表达式或语句块). 它可作为参数传递给方法,也可通过方法调用返回. Lambda 表达式广泛用于: 将 ...

  7. 009-jdk1.8版本新特性一-展方法,Lambda表达式,函数式接口、方法引用构造引用

    一.JDK1.8 名称:Spider(蜘蛛) 发布日期:2014-03-18 新特性: 1.1.扩展方法[接口的默认方法] Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 defaul ...

  8. Java8简明学习之Lambda表达式

    函数式接口 就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,函数式接口可以被隐式转换为lambda表达式. 之前已有的函数式接口: java.lang.Runnable java.uti ...

  9. JDK1.8新特性(一) ----Lambda表达式、Stream API、函数式接口、方法引用

    jdk1.8新特性知识点: Lambda表达式 Stream API 函数式接口 方法引用和构造器调用 接口中的默认方法和静态方法 新时间日期API default   Lambda表达式     L ...

  10. 夯实Java基础(二十二)——Java8新特性之Lambda表达式

    1.前言 Java 8于14年发布到现在已经有5年时间了,经过时间的磨练,毫无疑问,Java 8是继Java 5(发布于2004年)之后的又一个非常最重要的版本.因为Java 8里面出现了非常多新的特 ...

随机推荐

  1. VHD_Update_diskpart

    ###################功能说明########################该脚本用来对离线VHD文件更新,导入系统补丁############################### ...

  2. 继承虚函数浅谈 c++ 类,继承类,有虚函数的类,虚拟继承的类的内存布局,使用vs2010打印布局结果。

    本文笔者在青岛逛街的时候突然想到的...最近就有想写几篇关于继承虚函数的笔记,所以回家到之后就奋笔疾书的写出来发布了 应用sizeof函数求类巨细这个问题在很多面试,口试题中很轻易考,而涉及到类的时候 ...

  3. gcc中不同namespace中同名class冲突时

    正常情况下,编译器都会报错,提示你有两个候选类,让你明确的选择一个. 比如我的情况,我自己设计了一个类Message, 然后在某个文件里面引用了它.但是我的文件中又引入了mongodb的头文件,非常不 ...

  4. [Jest] Track project code coverage with Jest

    Jest comes pre-packaged with the ability to track code coverage for the modules you're testing, but ...

  5. int *(*a[5])(int, char*)

    int* 表示是一个int型指针;(*a[5])(int, char*)中的a[5]表示是一个有5个元素的数组,而(*)(int, char*)则表示指向一个函数的指针,该函数有两个参数,第一个参数为 ...

  6. java_类承继其他类的内部类例子

    package ming; class Outer { class In { public In(String msg) { System.out.println(msg); } } } public ...

  7. Helpers\Password

    Helpers\Password The password class uses php 5 password_ functions. To create a hash of a password, ...

  8. iOS 数据类型

    标签: 数据类型 1.Objective-C数据类型可以分为:基本数据类型.对象数据类型和id类型. 2.基本数据类型有:int.float.double和char类型. 3.对象类型就是类或协议所声 ...

  9. Mac OS X 配置 Apache+Mysql+PHP 详细教程

    网上的教程已经有很多,这里简洁的记录一下.以 Mac OS X Mavericks 10.9.X 为例. 先附上如何进入指定目录文件夹,按键盘 Command + Shift + G ,然后输入指定目 ...

  10. 虚拟机 VirtualBox 自制帮助文档

    初学 VirtualBox ,网络上教程很多,图片一张一张的费流量,讲得又比较散,于是花了一下午制作了此 CHM 帮助文档. 下载:(图片另存为--重命名为 RhinoC.rar --解压缩)