您的位置:首页 > 编程语言 > C#

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

2017-05-26 10:44 906 查看

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 key
sortedAlphabetically = 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(); // 10
int lastNumber = numbers.Last(); // 6
int secondNumber = numbers.ElementAt(1); // 9
int 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); // true
bool hasMoreThanZeroElements = numbers.Any(); // true
bool 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 names
where n.Contains ("a") // Filter elements
orderby n.Length // Sort elements
select n.ToUpper(); // Translate each element (project)
foreach (string name in query) Console.WriteLine (name);
}
}
JAY
MARY
HARRY


查询表达式一般以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里的n
orderby n.Length // n = 经where字句筛选过的n
select 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

我们会在后面的内容中介绍这几个字句的使用方式。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: