LINQ学习之旅——第一站"LTO"基础
通过前几节对LINQ中所涉及到的C#语言新特性的学习,我们已经做好了LINQ学习之旅的准备。接着我们踏入LINQ学习之旅的第一站"LTO",即Linq To Object,它是用于操作内存对象的LINQ编程接口,针对内存中的集合进行操作。所以从今天开始以及之后的几节内容都将围绕Linq To Object这个主题展开讲解。那么今天所讲的主要内容是有关于Linq To Object中的一些基本概念:IEnumerable<T>泛型接口、序列以及标准查询操作符。其中标准查询操作符在这里只做概念上的统一讲解,而对于单个具体的标准查询操作符将在后续几节里进行详细地讲解。
泛型接口IEnumerable<T>与IEnumerable接口类似,允许对接口内部的元素进行列举操作,集合只要是实现了接口IEnumerable<T>或IEnumerable就可以对其进行查询。而序列则是表示一个实现了接口IEnumerable<T>的集合对象。同时Linq To Object中的标准查询操作符都是针对序列的,标准查询操作符实质上就是一些扩展方法,这些扩展方法在静态类System.Linq.Enumerable中,其原型的第一个参数(即带this修饰符的参数)是IEnumerable<T>类型,所以这些扩展方法可以直接在IEnumerable<T>的实例对象上直接调用。但并不是所有的标准查询操作符都是扩展方法,也有一些是普通的静态方法,而这些静态方法一般是用来实现更为复杂的功能。
另外在早期的C#语言里的一些集合类型,它们实现的是IEnumerable接口,而并不支持IEnumerable<T>接口,这就使得不能在这些类型集合对象上使用标准查询操作符,为了解决这个问题,C#中提供个Cast和OfType标准查询操作符把这些不支持IEnumerable<T>接口的集合类型转化为支持IEnumerable<T>接口的集合类型。这样标准查询操作符就可以适用于C#语言的所有集合类型。下面用标准查询操作符Select来说明标准查询操作符的一些重要特性:
1 staticvoid Main(string[] args)
2 {
3 char[] ABC =newchar[] { 'A', 'B', 'C', 'D', 'E' };
4
5 var ABCs = ABC.Select(i => i);//调用标准查询操作符Select
6
7 foreach (char c in ABCs)
8 {
9 Console.WriteLine(c);
10 }
11
12 Console.Read();
13 }
结果:
示例中标准查询操作符Select返回了一个IEnumerable<T>集合对象(也称序列),但需要注意的是它所返回的序列对象内部包含的元素并不是在标准查询操作符被调用时立刻创建的:
var ABCs = ABC.Select(i => i);//返回的是空序列
而是在执行列举该序列对象元素的代码时:
foreach (char c in ABCs)
{
Console.WriteLine(c);
}
系统动态利用yield关键字来创建的。我们把这个特性称为延时查询。因此在标准查询操作符里有分延时标准查询操作符和非延时标准查询操作符。Select方法就属于延时标准查询操作符。接下来我用一个示例来具体说明延时标准查询操作符和非延时标准查询操作符在查询数据时的区别:
1 staticvoid Main(string[] args)
2 {
3 //延时查询
4 Console.WriteLine("延时查询:");
5 Console.WriteLine("-----------------------------");
6
7 char[] ABC =newchar[] { 'A', 'B', 'C', 'D', 'E' };
8 var ABCs = ABC.Select(i => i);//调用标准查询操作符Select
9
10 //第一次列举
11 Console.WriteLine("修改前:");
12 foreach (char c in ABCs)
13 {
14 Console.WriteLine(c);
15 }
16
17 ABC[0] ='H';
18 ABC[1] ='E';
19 ABC[2] ='L';
20 ABC[3] ='L';
21 ABC[4] ='O';
22
23 //第二次列举
24 Console.WriteLine("修改后:");
25 foreach (char c in ABCs)
26 {
27 Console.WriteLine(c);
28 }
29
30
31
32 //非延时查询
33 Console.WriteLine("非延时查询:");
34 Console.WriteLine("-----------------------------");
35
36 char[] ABC1 =newchar[] { 'A', 'B', 'C', 'D', 'E' };
37 var ABCs1 = ABC1.Select(i => i).ToArray();//调用标准查询操作符Select及ToArray
38
39 //第一次列举
40 Console.WriteLine("修改前:");
41 foreach (char c in ABCs1)
42 {
43 Console.WriteLine(c);
44 }
45
46 ABC1[0] ='H';
47 ABC1[1] ='E';
48 ABC1[2] ='L';
49 ABC1[3] ='L';
50 ABC1[4] ='O';
51
52 //第二次列举
53 Console.WriteLine("修改后:");
54 foreach (char c in ABCs1)
55 {
56 Console.WriteLine(c);
57 }
58
59 Console.Read();
60 }
结果:
其中方法ToArray()属于非延时标准查询操作符。从运行结果中可以清楚地看到,延时查询在数据源修改前后发生变化,而非延时查询则相同。那么为什么会出现这样的情况呢?原来当调用Select操作符返回时,返回的是一个空的IEnumerable<T>集合对象,在执行foreach语句时相应的元素才一个一个地生成,并填充到这个空的IEnumerable<T>集合对象里去,所以在第一次列举完成后,修改原始数据,在执行第二次列举时,修改的数据便马上呈现出来。而ToArray()操作符其实返回的是T[]集合,而非IEnumerable<T>集合对象,来缓存数据集合,所以尽管对数据源进行了修改,但列举的集合对象是同一个,结果也就没有变化了。
最后讲解一下LINQ中这些标准查询操作符的方法原型里的一个委托类型的参数,这些委托类型定义在System.Linq中。以下是System.Linq空间里定义的委托:
public detegate T Func<T>();
public detegate T Func<A0,T>(A0 arg);
public detegate T Func<A0,A1,T>(A0 arg0,A1 arg1);
public detegate T Func<A0,A1,A2,T>(A0 arg0,A1 arg1,A2 arg2);
public detegate T Func<A0,A1,A2,A3,T>(A0 arg0,A1 arg1,A2 arg2,A3 arg3);
这些定义的委托在今后所要讲到的大部分标准查询操作符里都会涉及到,比如示例中所提到Select操作符,它的方法原型定义如下:
public static IEnumerable<T> Select<T,S>(
this IEnumerable<T> source,
Func<T,S> selector);
原型中的委托类型Func<T,S>就与上述委托类型的第二种相对应,其中的T就与public detegate T Func<A0,T>(A0 arg)中的A0相对应,而S则与public detegate T Func<A0,T>(A0 arg)中的T相对应,从中可以看出操作符Select能够接受一个匿名方法(或者Lambda表达式),且该匿名方法返回一个类型为S的值,正如上述代码Select操作符接受的一个Lambda表达式i=>i,就是代入一个Char类型的值i,并返回同样类型的一个值i。从而可知,这些在System.Linq空间里定义的委托的重要性。