A.1 聚合
聚合操作符(见表A-1),所有的结果只有一个值而不是一个序列。 Average 和 Sum 针对数值 (任何内置数值类型)序列或使用委托从元素值转换为内置数值类型的元素序列。 Min 和 Max 具有 不同数值类型的重载,不过也只能在对元素类型使用默认比较符或使用转换委托的序列上进行操 作。 Count 和 LongCount 是等价的,不同之处仅仅在于返回类型。它们两者都具有两个重载—— 一个只统计序列长度,一个可以接受谓词,即只统计与谓词匹配的元素。
string[] words = { "zero", "one", "two", "three", "four" }; int[] numbers = { 0, 1, 2, 3, 4 }; var n1 = numbers.Sum(); var n2 = numbers.Count(); var n3 = numbers.Average(); var n4 = numbers.LongCount(x => x % 2 == 0); var n5 = numbers.Aggregate("seed", (current, item) => current + item, result => result.ToUpper()); var w1 = words.Min(word => word.Length); var w2 = words.Max(word => word.Length);
最常见的聚合操作符就是 Aggregate (表A-1最后一行)。所有的其他聚合操作符都能表示 为对 Aggregate 的调用,虽然这样做会相对繁琐。其基本思想就是,总是存在以初始元素开头的 “当前结果”。聚合委托被应用于输入序列的每个元素;委托取得当前结果和输入元素,并生成下 一个结果。作为最终可选步骤,转换被应用于聚合结果上,并将其转换为这个方法的返回值。如 果有必要,这种转换可以产生不同的数据类型。这虽然不像听起来那么复杂,不过你依旧不希望 频繁地使用它。
所有的聚合操作符都使用立即执行的模式。没有使用谓词的 Count 方法的重载为实现了 ICollection 和 ICollection<T> 的类型进行了优化。届时,将使用集合的 Count 属性,无须读 取任何数据 。LongCount 没有这种优化。我个人从来没有在LINQ to Objects中使用过这个方法。
A.2 连接
只存在一个连接操作符: Concat (见表A-2)。如你所料,它会对两个序列进行操作,并 返回一个单独的序列,该序列中包含了第1个序列和第2个序列中所有元素,且第2个序列连接 在第1个序列的后面。两个输入序列必须具有相同的类型,使用延迟执行模式,并且均为流式 数据。
int[] numbers = { 0, 1, 2, 3, 4 }; var n = numbers.Concat(new[] { 1, 2, 3 });
A.3 转换
转换操作符被广泛地使用,不过它们总是成对出现。
object[] allStrings = { "These", "are", "all", "strings" }; object[] notAllStrings = { "Number", "at", "the", "end", 5 }; string[] words = { "zero", "one", "two", "three", "four" }; int[] numbers = { 0, 1, 2, 3, 4 }; var a1 = allStrings.Cast<string>(); var a2 = allStrings.OfType<string>(); var n1 = notAllStrings.Cast<string>(); var n2 = notAllStrings.OfType<string>(); var b1 = numbers.ToArray(); var b2 = numbers.ToList(); var w1 = words.ToDictionary(w => w.Substring(0, 2)); var w2 = words.ToLookup(word => word[0]); var w3 = words.ToDictionary(word => word[0]); //已添加了具有相同键的项
ToArray 和 ToList 的含义显而易见:它们读取整个序列到内存中,并把结果作为一个数组 或一个 List<T> 返回。两者都是立即执行。
Cast 和 OfType 把一个非类型化序列转换为类型化的,或抛出异常(对于 Cast ),或忽略那 些不能隐式转换为输出序列元素类型的输入序列元素(对于 OfType )。这个运算符也能用于把某 个类型化序列转换为更加具体的类型化序列,例如把 IEnumerable<object> 转换为 IEnumerable<string> 。转换以流的方式延迟执行。
ToDictionary 和 ToLookup 都使用委托来获得任何特定元素的键。 ToDictionary 返回一个 把键映射到元素类型的字典,而 ToLookup 返回相应的类型化的 ILookup<,> 。查找类似于查字 典,只不过和健相关的值不是一个元素而是元素的序列。查找通常用于普通操作中希望有重复的 键存在的时候,而重复的键会引起 ToDictionary 抛出异常。两者更复杂的重载方法可以将自定 义的 IEqualityComparer<T> 用于比较键的操作,并在每个元素被放到字典或者开始查找之前, 把转换委托应用于其上。另外,两个方法都会使用立即执行的模式。
我没有提供 AsEnumerable 和 AsQueryable 这两个操作符的例子,因为它们不会以一种显而 易见的方式立即影响结果。但它们影响查询执行的方式。 Queryable.AsQueryable 是返回一个 IQueryable 类型(既可以是泛型,也可以是非泛型,取决于你选取的重载)的 IEnumerable 上的扩展方法。如果调用的 IEnumerable 已经是一个 IQueryable ,则它会返回同一个引用,否 则就创建一个包装类来包含原始序列。通过这个包装类,你可以使用所有普通的 Queryable 扩展 方法,在表达式树中传递,不过在查询执行的时候,表达式树被编译为普通的IL代码直接执行。 LambdaExpression.Compile 的用法已经在9.3.2节中讲到过。
Enumerable.AsEnumerable 是 IEnumerable<T> 的扩展方法,包含一些简单的实现,仅返 回被调用者的引用。这次不会用到包装类——仅返回同一个引用。
它强制 Enumerable 扩展方法 用于随后的LINQ操作符。思考一下如下查询表达式:
var query1 = from user in context.Users where user.Name.StartWith("Tim") select user; var query2 = from user in context.Users.AsEnmuerable() where user.Name.StartWith("Tim") select user;
第2个查询表达式强制编译时的源类型为 IEnumerable<User> 而非 IQueryable <User> , 因而所有的处理过程都可以在内存中而不必在数据库中完成。编译器将使用 Enumerable 的扩展 方法(它获取委托参数)而不是 Queryable 的扩展方法(它获取表达式树参数)。通常而言,你 希望尽可能在SQL中完成大多数处理,但在遇到需要“本地”代码参与转换的时候,你有时不得 不强制LINQ去使用适当的 Enumerable 扩展方法。当然,这不仅仅针对数据库,其他提供器也可 以在查询末端强制使用 Enumerable ,只要它们基于 IQueryable 及其同类即可。
A.4 元素操作符
另外一种用于选择数据的查询操作符,分组后成对显示在表A-4中。这次,每一对操作符都以 同样的方式执行。选取单个元素的简化版本,就是在特定元素存在时返回它,不存在时就抛出一个 异常,还有一个以 OrDefault 结尾的版本。 OrDefault 版本的功能完全一样,只是在找不到想要 的元素时,返回结果类型的默认值,而非抛出一个异常。所有操作符都使用立即执行的模式。
1 string[] words = { "zero", "one", "two", "three", "four" }; 2 3 var w1 = words.ElementAt(2); 4 var w2 = words.ElementAtOrDefault(10); 5 var w3 = words.First(); 6 var w4 = words.First(w => w.Length == 3); 7 var w5 = words.First(w => w.Length == 10); //异常 8 var w6 = words.FirstOrDefault(w => w.Length == 10); 9 var w7 = words.Last(); 10 var w8 = words.Single(); 11 var w9 = words.Single(w => w.Length == 10); //异常 12 var w10 = words.SingleOrDefault(); 13 var w11 = words.SingleOrDefault(w => w.Length == 10);
操作符的名称很容易理解: First 和 Last 分别返回序列的第1个和最后1个元素,如果序列为 空,抛出 InvalidOperationException 异常。 Single 返回序列中的一个元素,如果序列为空 或者返回一个以上的元素则抛出异常,而 ElementAt 通过索引返回特定的元素(例如,第5个元 素)。如果索引为负数或大于集合中元素的实际数量,将抛出 ArgumentOutOfRangeException 。 另外,除了 ElementAt 首先过滤序列以外,所有的操作符都存在相应的重载方法——例如, First 可以返回匹配给定条件的第1个元素。
以上这些方法的 OrDefault 版本都不会抛出异常,而是返回元素类型的默认值。但有一种例 外情况:如果序列为空(empty), SingleOrDefault 将返回默认值,但如果序列中的元素不止 一个,将抛出异常,就像 Single 方法一样。该方法适用于所有条件正确,序列只包含0或1个元 素的情况。如果你要处理的序列可能包含多个元素,应该使用 FirstOrDefault 方法。
所有没有使用谓词参数的重载都为 IList<T> 的实例进行了优化,因为它们不通过迭代就可 以访问正确的元素。包含谓词的版本没有优化,因为这对大多数调用都没有意义,尽管从末尾向 后移动,会使查找列表中最后一个匹配元素的过程产生很大不同。在本书写作之时,这种情况还 没有优化,未来版本也许会发生改变。
A.5 相等操作
只有一个标准的相等操作符: SequenceEqual (见表A-5)。它是按照顺序逐一比较两个序 列中的元素是否相等。例如,序列0,1,2,3,4不等于4,3,2,1,0。重载方法允许使用具体 的 IEqualityComparer<T> ,对元素进行比较。返回值就是一个Boolean值,并使用立即执行的 模式来计算。
string[] words = { "zero", "one", "two", "three", "four" }; var b1 = words.SequenceEqual(new[] { "zero", "one", "two", "three", "four" }); var b2 = words.SequenceEqual(new[] { "ZERO", "ONE", "TWO", "THREE", "FOUR" }); var b3 = words.SequenceEqual(new[] { "ZERO", "ONE", "TWO", "THREE", "FOUR" }, StringComparer.OrdinalIgnoreCase);
同样,LINQ to Objects在这里也没有进行优化:如果存在有效的方式可以得到两个序列的数 量,那么在比较它们的元素之前,就能知道它们是否相等了。而实际上,该方法的实现直接遍历 两个序列,直到末尾或找到不等的元素。
A.6 生成
在所有的生成操作符(见表A-6)中,只有一个会对现有的序列进行处理: DefaultIfEmpty 。如果序列不为空,就返回原始序列,否则返回含有单个元素的序列。其中的元素通常是序列类型 的默认值,不过重载方法允许你设定要使用的值。
int[] numbers = { 0, 1, 2, 3, 4 }; var n1 = numbers.DefaultIfEmpty(); var n2 = new int[0].DefaultIfEmpty(); var n3 = new int[0].DefaultIfEmpty(10); var n4 = Enumerable.Range(15, 2); var n5 = Enumerable.Range(25, 2); var n6 = Enumerable.Empty<int>();
其他3个生成操作符仅仅是 Enumerable 的静态方法。
Range 生成一个整数序列,通过参数可以设定第一个值和需要生成值的个数。
Repeat 根据指定的次数来重复特定的单个值,以生成任意类型的序列。
Empty 生成任意类型的空序列。
所有生成操作符都使用延迟执行,并对结果进行流式处理。也就是说,它们不会预先生成集 合并返回。不过,返回正确类型的空数组的 Empty 方法是个例外。一个空的数组是完全不可变的, 因此相同元素类型的所有这种调用,都将返回相同的空数组。
A.7 分组
有两个分组操作符,不过其中一个就是 ToLookup (我们已经在A-3中讲述转换操作符的 时候看到过了)。剩下的 GroupBy ,已经在11.6.1节中讨论查询表达式中的 group... by 子句 时见过。它使用延迟执行,不过会缓存结果。当你开始迭代分组的结果序列时,消费的是整 个输入序列。
GroupBy 的结果是相应类型化 IGrouping<,> 元素的序列。每个元素具有一个键和与之匹配 的元素序列。从许多方面讲,它只是查找的一种不同的方式——不是通过键随机访问分组数据, 而是顺序枚举它。分组数据的返回顺序是它们各自键被发现的顺序。在一个分组数据内,元素的 顺序和最初序列中的一致。
GroupBy 具有大量的重载方法,你既可以设定从元素派生出键的方式(这一项是必须指定 的),也可以有选择地设定如下内容。
1.如何比较键。
2.从原始元素到分组内元素的投影。
3.包含键和匹配元素序列的投影。这时,整个结果为投影结果类型的元素序列。
表A-7包含了第二个和第三个选项的示例,当然还有最简单的形式。自定义键比较器不是三 言两语就能概括的,不过它们的工作方式一目了然。
string[] words = { "zero", "one", "two", "three", "four" }; var w1 = words.GroupBy(word => word.Length); var w2 = words.GroupBy(word => word.Length, //键 word => word.ToUpper()); //分组元素 var w3 = words.GroupBy(word => word.Length, (Key, g) => Key + ":" + g.Count());
//归集每个部门信息 var deptList = from emp in empList where emp.Status == "在职" //筛选“在职”员工 orderby emp.DeptID ascending //按“部门ID”排序 group emp by new //按“部门ID”和“部门名称”分组 { emp.DeptID, emp.DeptName } into g select new DeptInfo() { DeptID = g.Key.DeptID, DeptName = g.Key.DeptName, EmplayeeCount = g.Count(), //统计部门员工数量 WageSum = g.Sum(a => a.Wage), //统计部门工资总额 WageAvg = g.Average(a => a.Wage), //统计部门平均工资 EmplayeeList = (from e in g //归集部门员工列表 select new Emplayee() { EmpID = e.EmpID, EmpName = e.EmpName } ).ToList() });
A.8 连接
有两个操作符用于连接,即 Join 和 GroupJoin ,我们在11.5节分别使用 join 和 join...into 查 询表达式子句的时候看到过这两个操作符。每个方法都可以获取几个参数:两个序列、用于每个序 列的键选择器,应用于每个匹配元素对的投影,以及可选的键比较器。
对于 Join ,投影从每个序列中获取一个元素,并生成一个结果;对于 GroupJoin ,投影从 左边序列获取一个元素,然后从右边序列获取一个由匹配元素构成的序列。两者都是延迟执行, 并流处理左边序列,而对于右边序列,在请求第一个结果时便读取其全部内容。
对于在表A-8的连接例子中,我们将按照姓名和颜色的第一个字母把一个姓名序列(Robin、 Ruth、Bob、Emma)匹配到一个颜色序列(Red、Blue、Beige、Green),例如,Robin将会和Red 连接上,Blue和Beige连接。
string[] names = { "Robin", "Ruth", "Bob", "Emma" }; string[] colors = { "Red", "Blue", "Beige", "Green" }; var j1 = names.Join //左边序列 (colors, //右边序列 name => name[0], //左边选择器 color => color[0], //右边选择器 //为结果投影 (name, color) => name + " - " + color); /* Robin - Red * Ruth - Red * Bob - Blue * Bob - Beige */ var j2 = names.GroupJoin (colors, name => name[0], color => color[0], //为键/序列对投影 (name, matches) => name + ":" + string.Join("/", matches.ToArray())); /* Robin:Red Ruth:Red Bob:Blue/Beige Emma: */
注意,Emma未匹配任何颜色——所以,该姓名不会显示在第一个例子的结果中,但却会显 示在第二个例子中,只是颜色序列是空的。
A.9 分部
分部操作符中,既可以跳过(skip)序列的开始部分,只返回剩余元素,也可以只获取(take) 序列的开始部分,而忽略剩余元素。在每种情况下,你都可以设置序列的第一部分包含多少个元 素,或设定相应的条件——只要条件满足,序列的第一部分就继续往前进行。在条件第一次不满 足时,就不再往下判断——而不管序列的后面元素是否匹配。所有的部分操作符(见表A-9)都 是延迟执行和流式数据。
通过位置或谓词,分组可以将一个序列有效地划分成两个单独的部分。在这两种情况下, 如果你将 Take 或 TakeWhile 的结果与相对应的 Skip 或 SkipWhile 的结果进行连接,并为两种 调用提供相同的参数,将得到原始序列:每个元素都按原始顺序出现一次。表A-9演示了这种 调用。
string[] words = { "zero", "one", "two", "three", "four" }; var w1 = words.Take(2); var w2 = words.Skip(2); var w3 = words.TakeWhile(word => word.Length <= 4); var w4 = words.SkipWhile(word => word.Length <= 4);
A.10 投影
我们在第11章看到过这两个投影操作符( Select 和 SelectMany )。 Select 是一种简单 的从源元素到结果元素的一对一投影。 SelectMany 在查询表达式中有多个 from 子句的时候 使用;原始序列中的每个元素都用来生成新的序列。两个投影操作符(见表A-10)都是延迟 执行。
string[] words = { "zero", "one", "two", "three", "four" }; var w1 = words.Select(word => word.Length); var w2 = words.Select( (word, index) => index + ":" + word); var w3 = words.SelectMany(word => word.ToCharArray()); var w4 = words.SelectMany( (word, index) => Enumerable.Repeat(word, index));
这里包含了第11章没有介绍的重载。这两种方法都含有允许在投影中使用原始序列索引的重载,并且 SelectMany 还可以将所有生成的序列合并成一个单独的序列,而不包含原始序列 ,或使用投影来为每个元素对生成结果元素。多个 from 子句通常使用的是包含投影的重载。(篇幅所限,恕不举例,详细内容参见第11章。)
.NET 4引入了一个新的操作符 Zip 。根据MSDN所述,它并不是一个标准的查询操作符,但 还是有必要了解一下。它包含两个序列,并对每个元素对应用指定的投影:先是每个序列的第一 个元素,然后是每个序列的第二个元素,以此类推。任何一个源序列达到末尾时,结果序列都将 停止产生。表A-11展示了 Zip 的两个示例,使用了A.8节中的名称和颜色。 Zip 使用了延迟执行和 流式数据。
string[] names = { "Robin", "Ruth", "Bob", "Emma" }; string[] colors = { "Red", "Blue", "Beige", "Green" }; var n1 = names.Zip(colors, (x, y) => x + "-" + y); //第二个序列提前停止生成 var n2 = names.Zip(colors.Take(3), (x, y) => x + "-" + y);
A.11 数量
数量操作符(见表A-12)都返回一个Boolean值,使用立即执行。
All 操作符检查在序列中的所有元素是否满足指定的条件。
Any 操作符检查在序列中的任意元素是否满足指定的谓词,如果使用没有参数的重载,则检查序列中是否存在元素。
Contains 操作符检查序列是否包含特殊的元素,可选设置要使用的比较方式。
string[] words = { "zero", "one", "two", "three", "four" }; var w1 = words.All(word => word.Length > 3); var w2 = words.All(word => word.Length > 2); var w3 = words.Any(); var w4 = words.Any(word => word.Length == 6); var w5 = words.Any(word => word.Length == 5); var w6 = words.Contains("FOUR"); var w7 = words.Contains("FOUR", StringComparer.OrdinalIgnoreCase);
A.12 过滤
两个过滤操作符是 OfType 和 Where 。有关 OfType 操作符的细节和例子,请参见转换操作符 一节(A.3节)。 Where 运算符返回一个序列,这个序列包括与给定谓词匹配的所有元素。它还有 一个重载方法,以便谓词能使用元素索引。通常是不需要索引的,而在查询表达式中的 where 子 句也不使用这个重载方法。 Where 总是使用延迟执行和流式数据,表A-13列出了两个重载方法。
string[] words = { "zero", "one", "two", "three", "four" }; var w1 = words.Where(word => word.Length > 3); //"zero", "three", "four" var w2 = words.Where((word, index) => index < word.Length); // /* "zero", // index=0, length=4 "one", // index=1, length=3 "two", // index=2, length=3 "three", // index=3, length=5 // Not "four", index=4, length=4 */
A.13 基于集的操作符
把两个序列看作元素的集合是很自然的。4个基于集合的运算符都具有两个重载方法,一个 使用元素类型的默认相等比较,一个用于在额外的参数中指定比较。所有的集合运算符都是延迟 执行。
Distinct 操作符最简单——它只对单个序列起作用,并且返回所有不重复元素(已经排除了重复项)的新序列。其他的运算符也确保只返回不重复的元素,不过它们对两个序列起作用。
1. Intersect 返回在两个序列中都出现的元素。
2. Union 返回出现在任一序列中的元素。
3. Except 返回出现在第一个序列,但不出现在第二个序列中的元素(出现第二个序列但不在第一个中的元素也不返回)。
表A-14中这些操作符的示例使用了两个新序列,即 abbc ( "a" , "b" , "b" , "c" )和 cd ( "c" , "d" )。
string[] abbc = { "a", "b", "b", "c" }; string[] cd = { "c", "d" }; var a1 = abbc.Distinct(); //"a", "b", "c" var a2 = abbc.Intersect(cd); // "c" var a3 = abbc.Union(cd); // "a", "b", "c", "d" var a4 = abbc.Except(cd); // "a", "b" var a5 = cd.Except(abbc); // "d"
所有这些操作符都使用延迟执行,但有的使用了缓冲,有的使用了流式处理,缓冲和流式处理显然更复杂一些。 Distinct 和 Union 都对输入序列进行了流式处理,而 Intersect 和 Except先是读取整个右边输入序列,然后像连接操作符那样对左边输入序列进行流式处理。所有这些操作符都保存已返回元素的集,以确保不返回重复的元素。这意味着即使是 Distinct 和 Union ,也不适合那些过大而不适于放入内存的序列,除非你知道经过处理后的集并不会包含很多元素。
A.14 排序
我们之前见过所有的排序运算符: OrderBy 和 OrderByDescending 提供了“主要的”排序 方式,而 ThenBy 和 ThenByDescending 提供了次要的排序方式,用以区别使用主要的排序方式 无法区别的元素。在每种情况中,都要指定从元素到排序键的投影,也指定键之间的比较。不像 框架中的其他排序算法(比如 List<T>.Sort ),LINQ排序比较稳定——换句话说,如果两个元 素根据它们的排序关键字被认为是相等的,那么将按照它们在原始序列中的顺序返回。
最后一个排序操作符是 Reverse ,它仅反转序列的顺序。所有的排序操作符(见表A-15)都 是延迟执行,不过会缓存其中的数据。
string[] words = { "zero", "one", "two", "three", "four" }; //以首字母顺序排序 var w1 = words.OrderBy(word => word); //"four", "one","three","two", "zero" // 依据第二个字母对单词排序 var w2 = words.OrderBy(word => word[1]); //"zero", "three","one","four", "two" // 依据长度对单词排序,如长度相同则以原顺序返回 var w3 = words.OrderBy(word => word.Length); //"one", "two","zero","four", "three" var w33 = words.OrderByDescending(word => word.Length); //"three", "zero", "four", "one", "two" // 依据长度排序,如长度相同则以首字母顺序排序 var w4 = words.OrderBy(word => word.Length).ThenBy(word => word); //"one", "two","four","zero", "three" // 依据长度排序,如长度相同则按反向字母顺序排序 var w5 = words.OrderBy(word => word.Length).ThenByDescending(word => word); //"two", "one","zero","four", "three" //反转序列 var w6 = words.Reverse(); //"four", "three", "two","one", "zero"