C#学习笔记(八)—–LINQ查询的基础知识(中)

来源:互联网 发布:淘宝散货耳机 编辑:程序博客网 时间:2024/05/16 11:34

LINQ查询(中)

  • (接上文)Lambda表达式及Func方法签名:标准的查询运算符使用了一个泛型Func委托,Func是System.Linq命名空间中一组通用的泛型委托,它的作用是保证Func中的参数顺序和Lambda表达式中的参数顺序一致。因此,一个Fuc<TSource,bool>对应的Lambda表达式为TSource=>bool,也就是接受一个TSource,返回bool。
    类似的,Func<TSource,TResult>所对应的Lambda表达式为TSource=>TResult。
  • Lambda表达式和元素类型:标准的查询运算符使用下面这些泛型:
**泛型类型**          **名称意义**  TSource            输入集合的元素类型  TResult            输出集合的元素类型(不同于TSource)  TKey               在排序、分组或者连接操作中所用的键

这里的TSource有输入集合的元素类型决定。而TResult和TKey则由我们给出的lambda表达式指定。以Select的签名为例:

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

Func<TSource,TResult>对应的Lambda表达式是TSource=>TResult,这个表达式定义了输入元素和输出元素之间的映射关系,实际上TSource和TResult可以是不同的数据类型。更进一步说,Lambda表达式可以指定输出序列的类型。也就是说Select运算符可以根据Lambda表达式中的定义将输入类型转换成输出类型。下面这个示例中使用Select运算符将string类型的集合元素转换成int类型的数据来输出:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<int> query = names.Select (n => n.Length);foreach (int length in query)Console.Write (length + "|"); // 3|4|5|4|3|

编译器通过判断Lambda表达式中返回值的类型,推断出TRsult的类型。在这个示例中,推断TResult为int型。
Where查询运算符的内部操作比Select查询运算符要简单一些,因为他只筛选集合,不对集合中的元素进行类型转换,因此不需要进行类型推断。它的签名如下:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource,bool> predicate)

最后我们看一下Orderby运算符的方法签名:

// Slightly simplified:public static IEnumerable<TSource> OrderBy<TSource,TKey>(this IEnumerable<TSource> source, Func<TSource,TKey> keySelector)

Func<TSource,TKey>将每一个输入元素关联到一个排序键TKey,TKey的类型也是由Lambda表达式中推测出来的,但他的类型与输入类型和输出类型是无关的,三者是独立的,类型可以相同也可以不同。例如,我们可以选择对names集合按照名字的长度进行排序(TKey是int),也可以对names集合按字母排序(TKey是string):

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<string> sortedByLength, sortedAlphabetically;sortedByLength = names.OrderBy (n => n.Length); // int keysortedAlphabetically = names.OrderBy (n => n); // string key

这里写图片描述

自然排序

LINQ中集成了对集合的排序功能,这种内置的排序对整个LINQ体系来说有重要的意义。因为一些查询操作直接依赖于这种排序,例如:Take、Skip、和Reverse。
Take运算符会输出集合中前x个元素,这个x以参数的形式指定,例如:

int[] numbers = { 10, 9, 8, 7, 6 };IEnumerable<int> firstThree = numbers.Take (3); // { 10, 9, 8 }

Skip运算符会跳过集合中的前x个元素,输出其余元素:例如:

IEnumerable<int> lastTwo = numbers.Skip (3); // { 7, 6 }

Reverse运算符则会将集合中的所有元素反转,也就是按照元素当前顺序的逆序排列:

IEnumerable<int> reversed = numbers.Reverse(); // { 6, 7, 8, 9, 10 }

Where和Select这两个查询运算符在执行时,将集合中元素按照原有的顺序进行输出,事实上,在LINQ中,除非有必要,否则各个查询运算符都不会告便集合中元素的排序方式。

其他查询运算符

在LINQ中,并不是所有的查询运算符都会返回一个集合,一些针对元素的运算符可以从输入集合众返回单个元素,如First、Last、ElementAt等查询运算符,下面是几个简单示例:

int[] numbers = { 10, 9, 8, 7, 6 };int firstNumber = numbers.First(); // 10int lastNumber = numbers.Last(); // 6int secondNumber = numbers.ElementAt(1); // 9int secondLowest = numbers.OrderBy(n=>n).Skip(1).First(); // 7

而聚合运算符则返回一个表示数量的值:

int count = numbers.Count(); // 5;int min = numbers.Min(); // 6;

下面这些量词运算符返回bool型的结果:

bool hasTheNumberNine = numbers.Contains (9); // truebool hasMoreThanZeroElements = numbers.Any(); // truebool hasAnOddElement = numbers.Any (n => n % 2 != 0); // true

由于这些运算符返回的不是一个集合,所以无法在他们的后面再使用其他查询运算符,这一点很容易理解,所以这些运算符一般都不现在一个查询的结尾。
有些查询运算符同时接受两个输入集合,例如Concat运算符会把一个集合众的元素添加到另一个元素的集合中,另外还有Union运算符,他和Concat运算符的作用是相同的,唯一的区别是Union运算符会将结果集合中相同的元素去掉:

int[] seq1 = { 1, 2, 3 };int[] seq2 = { 3, 4, 5 };IEnumerable<int> concat = seq1.Concat (seq2); // { 1, 2, 3, 3, 4, 5 }IEnumerable<int> union = seq1.Union (seq2); // { 1, 2, 3, 4, 5 }

实际上,连接运算符也属于这一类,这在后面的章节中会继续介绍。

查询表达式

C#中新增了一组专门用于LINQ查询的语法结构,这种语法结构和C#原有的语法差别很显著,这种语法结构用到的查询运算符如from、select、where等和SQL中的关键字很类似,但实际上这种语法结构并不是基于SQL设计出来的,而是来源于诸如LISP和Haskell这样的函数式编程语言。C#借鉴了这些语言中的列表解析方式。
在之前的章节中,我们以查询表达式流的方式写过这样一段代码,他可以筛选出names集合中的所有含字母a的元素,并把这些元素排序后以大写形式输出。下面使用查询表达式语法来完成相同的操作:

using System;using System.Collections.Generic;using System.Linq;class LinqDemo{static void Main(){string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };IEnumerable<string> query =from n in nameswhere n.Contains ("a") // Filter elementsorderby n.Length // Sort elementsselect n.ToUpper(); // Translate each element (project)foreach (string name in query) Console.WriteLine (name);}}JAYMARYHARRY

查询表达式一般以from字句开始,最后一select或者group字句结束。from子句的作用是定义一个范围变量,(本例中是n),这个变量会分别被输入序列中的每个元素赋值,通过这个变量,可以操作序列中的所有元素,实际上和foreach循环中的临时变量的作用是相同的。下图展示了完整的语法:
这里写图片描述
提示:要理解上面查询表达式中逻辑关系,可以从表达式的最左边开始把整个表达式看作是一个队列,例如,在一个表达式中,在from子句之后,可以选择性的使用orderby、where、let、或者join子句。在所有这些子句之后,我们可以接着使用select或者group来结束整个查询。也可以不直接结束查询,而是把上次的查询结果是做一个输入,重新使用from、orderby、where、let或者join子句进行第二轮查询。

  • 编译器在执行查询表达式之前会把他编译成运算符流的形式,这样更接近它的原始状态。这个过程非常机械化,并没没有使用什么特别操作,都是最常用的基本操作。foreach语句也是一样。就是通过多次调用GetEnumerator和movenext方法来完成内部逻辑。因此查询表达式中的所有逻辑都可以用运算符流语法来书写。鞋面这个语句是上面的查询表达式经编译器编译后的结果:
IEnumerable<string> query = names.Where (n => n.Contains ("a")).OrderBy (n => n.Length).Select (n => n.ToUpper());

Where、Orderby、Select这些运算符(我觉得是说的是查询表达式中的关键字)在执行前被解析成运算符流语法中相对的运算符,能达到这个效果,是因为这些关键字绑定了Enumerbale类中对应的查询语法,如何知道绑定到哪个方法呢?在一开始导入了System.Linq命名空间,并且输入集合names也实现了Enumerable<string>接口,这时使用Where、Orderby和Select这些运算符时,编译器就会依次找到Enumerable类中绑定的方法,最终完成查询的是这些方法而不是运算符,只是以一种更抽象的方式定义查询表达式,这种做法不仅让代码更易懂,而且还可以在执行的时候判断到底需要调用哪个类中的Where和Select方法。还有其他类也实现了这些方法,例如下面要讲到的Queryable类。
如果我们从代码中删除对System.linq命名空间的引用,那么查询表达式就不能顺利编译,因为编译器在编译where、orderby和select的时候,找不到与之对应的方法去绑定,编译器需要的是可以进行绑定的方法。这时需要导入命名空间,或者为每个查询运算符实现一个相应的方法。
这里写图片描述

范围变量

在之前用到的LINQ表达式中,紧跟在from关键字之后标识符n实际上是一个范围变量。范围变量指向当前序列中药进行操作的元素。
在之前的示例中,在每一个查询子句中都有范围变量,每个范围变量都会在各个子句中被重新定义,后面的子句并没有重用前面子句中的范围变量,下面是一个简单示例:

from n in names // 这里的n是范围变量where n.Contains ("a") // n = from里的norderby n.Length // n = 经where字句筛选过的nselect n.ToUpper() // n = 使用经orderby排序过的集合中的n

经过上面的解释,就很容易理解编译器将LINQ表达式转换成运算符流形式后的代码是:

names.Where (n => n.Contains ("a")) // 私有的局域变量.OrderBy (n => n.Length) // 一个新的私有局域变量.Select (n => n.ToUpper()) // 又一个

正如我们看到的,在每个子查询的Lambda表达式中,n都会被重新定义。

  • 如果有必要,在查询中可以将结果集暂存于某个变量中作为中间结果集,然后再对这个中间结果集进行新的查询。要定义这种存储中间结果的变量,需要使用下面几个子句:
    ①let
    ②into
    ③一个新的from子句
    ④join
    我们会在后面的内容中介绍这几个字句的使用方式。