Pro LINQ 之二:LINQ to Objects
写在前面
LINQ to Objects,是整个LINQ的支柱。而LINQ的语法,又存在类sql语言和扩展方法调用两种书写方式。从个人的习惯或者喜好出发,在LINQ to Objects上,我更倾向于使用后者。客观上,使用扩展方法的编译,也比类sql语言的方式更直接和效率。
P54 LINQ to Objects的三大核心
IEnumerable<T>、序列、标准查询运算符是LINQ to Objects的三大核心。
其中,序列即数据源,需要支持IEnumerable接口。而序列的元素,则是序列的单位项,通常作为LINQ语句中形式参数的一种象征,出现在首个参数与返回值位置。扩展方法,或者说查询的运算符,是委托delegate的一种变形,这些方法均以序列的元素为首个参数,其本身则是构成LINQ子句的基本单位。
对LINQ to Objects而言,查询运算的第一个参数与查询的结果,均为IEnumerable<T>类型。查询运算,如P14所言,大多均为迟延查询,即在查询结果被引用时,查询才真正被执行。所以,LINQ语句的错误,也通常要在查询结果被引用、遍历时才会被发现,因此不要认为LINQ语句通过了编译检查,就是bug-free的了。
LINQ的迟延查询,可以用下面一段代码作为示意:
// 准备数据源 string[] strings = { "one", "two", null, "three" }; // 定义LINQ查询 Console.WriteLine("Before LINQ declared."); IEnumerable<string> ieStrings = strings.Where(s => s.Length == 3); Console.WriteLine("After LINQ declared."); // 执行查询,对序列进行遍历 foreach (string s in ieStrings) Console.WriteLine("Processing {0}", s);
由于字符串数组strings中有一个元素为null,查询会触发一个空引用的异常。而我们要注意的是,这个异常是在foreach循环已经遍历并输出元素one与two后触发的,因此可以确定之前定义的LINQ查询,直到foreach循环引用序列中的元素中时才被执行。而另一方面,ToArray()、ToList()以及一些聚合操作,则是即时查询或生成的。
P58 LINQ中的函数委托 Func<>
LINQ中的标准查询运算符,大多都需要一个委托作为参数。这个委托,通常被表达为如下形式:
public delegate TR Func<TR>(); public delegate TR Func<T0, TR>(T0 a0); public delegate TR Func<T0, T1, TR>(T0 a0, T1 a1); public delegate TR Func<T0, T1, T2, TR>(T0 a0, T1 a1, T2 a2); public delegate TR Func<T0, T1, T2, T3, TR>(T0 a0, T1 a1, T2 a2, T3 a3);
其中,T0通常代表作为数据源的输入序列,TR则代表作为结果的输出序列并且总在参数的最末尾。
P59 标准查询运算符列表
MSDN中的链接相比书中的更完整:标准查询运算符概述
LINQ的查询符,涉及提取元素的多可带Func<T, bool>的判断子,涉及判断和比较元素的多可带IComparer<T>或IEqualityComparer<T>的比较子。其中IComparer是三元的(1/0/-1),IEqualityComparer是二元的(true/false)。
Visual Studio的智能提示,对于我这样经常忘记运算符重载形式的人编写LINQ,帮助很大!
注:运算符触发的异常,通常是因为元素为null。
P65 Where
重点是Where的第2种形式,其中引入了序列的索引值作为Func<>的参数。其中,p指代序列元素,i指代序列索引。
//public static IEnumerable<T> Where<T>( // this IEnumerable<T> source, // Func<T, int, bool> predicate); IEnumerable<string> sequence = presidents.Where((p, i) => (i & 1) == 1);
P68 Select
同样有个引入了序列索引的、类似Where形式2的形式。
var nameObjs = presidents.Select((p, i) => new { Index = i, LastName = p });
Where与Select的结合:从字符串中找出所有Unicode编码的汉字。
string source = @"我wo 是shi 中zhong 国guo 人ren"; char[] result = source .Where(c => (c >= 0x4e00 && c <= 0x9fa5)) .Select(c => c).ToArray();
P73 SelectMany
SelectMany的源是序列,源的元素也是序列,查询返回的结果是源的元素的元素。可以理解为,按1:M的规则,把序列中的每个元素再拆成多个子元素。它同样有个引入序列索引的形式:
IEnumerable<char> chars = presidents .SelectMany((p, i) => i < 5 ? p.ToArray() : new char[] { });
上述语句,会把字符串数组中的前5个字符串全部拆成单个字符放入查询结果chars中。
P77 Take-TakeWhile-Skip-SkipWhile
Take | 从序列首部开始,取出固定数目的元素 |
TakeWhile | 从序列首部开始,取出符合条件的元素,直到遇到第一个条件不匹配的元素时停止,类似do...while |
Skip | 从序列首部跳过N个元素 |
SkipWhile | 从序列首部,跳过符合条件的元素,直到遇到第一个不匹配的元素时停止。 |
P83 Concat
Concat合并两个序列,保存重复项(区别于Union)。连接2个以上的序列,有个小技巧
new[]{sequence1, sequence2, …}.SelectMany()
IEnumerable<string> items = new[] { presidents.Take(5), presidents.Skip(5) }.SelectMany(s => s);
P86 OrderBy-OrderByDescending-ThenBy-ThenByDescending-Reverse
Order排序的难点在第2个形式,引入一个比较子IComparer<T>。
public static IOrderedEnumerable<T> OrderBy<T, K>( this IEnumerable<T> source, Func<T, K> keySelector, IComparer<K> comparer);
在先前的.NET版本中,比较子通常是经由接口IComparable与IComparer实现的。在MSDN的知识库中有个例子:如何使用 IComparable 和 IComparer 接口在 Visual C# 中。
通过派生于IComparable变身比较器的容器,再内嵌IComparer的子类作为比较器,将可以通过Array.Sort(cars, car.sortYearAscending())这样的方式提供特定成员的比较器。
另一方面,通过实现自IComparable派生来的接口方法Compare(),为Array.Sort(cars)这样的调用提供默认的比较器。
public class car : IComparable { // 使用内嵌的IComparer派生类,构造特定成员的比较器 private class sortYearAscendingHelper : System.Collections.IComparer { int System.Collections.IComparer.Compare(object a, object b) { car c1 = (car) a; car c2 = (car) b; if (c1.year > c2.year) return 1; if (c1.year < c2.year) return -1; else return 0; } } // 利用嵌套类,暴露特定成员的比较器 public static System.Collections.IComparer sortYearAscending() { return (System.Collections.IComparer) new sortYearAscendingHelper(); } // 实现派生的IComparable.CompareTo()方法,提供类car默认的比较子 int IComparable.CompareTo(object obj) { car c = (car) obj; if (this.year > c.year) return 1; if (this.year < c.year) return -1; else return 0; } private int year; public car(int Year) { year = Year; } }
时代在进步,自.NET 2.0引入泛型之后,IComparer<T>接口进入我们的视野。通过从IComparer<T>派生子类,将直接生成对应于类型T的比较子,从而实现OrderBy((o=>o), new BoxComp())这样的排序。
public class BoxComp : IComparer<Box> { public int Compare(Box x, Box y) { if (x.Height.CompareTo(y.Height) != 0) { return x.Height.CompareTo(y.Height); } else if (x.Length.CompareTo(y.Length) != 0) { return x.Length.CompareTo(y.Length); } else if (x.Width.CompareTo(y.Width) != 0) { return x.Width.CompareTo(y.Width); } else { return 0; } } }
Reverse()就是简单地逆排序。
P102 Join-GroupJoin
Join的过程:用内选择子筛选内序列,用返回的对象借由外选择子筛选外序列,再按内外1:1的方式对匹配的内外元素调用结果的选择子生成结果序列中的元素。其中,外序列是主动的、必取的,在取出外元素的基础上,向结果选择子中添加匹配的内元素。结果选择子lambda表达式左边的参数是(outer, inner)。
public static IEnumerable<V> Join<T, U, K, V>( this IEnumerable<T> outer, // 外序列 IEnumerable<U> inner, // 内序列 Func<T, K> outerKeySelector, // 外选择子 Func<U, K> innerKeySelector, // 内选择子 Func<T, U, V> resultSelector); // 结果选择子
GroupJoin与Join的最大区别,在于GroupJoin是外内1:M的的匹配,即类似于数据库中父子表间通过外键FK建立的1:M的连接。其结果选择子Func<>中的第2个参数为IEnumerable<U>而不是U,因此选择子lambda表达式左边的参数表现为(outer, inners)。通常地,GroupJoin与inners.DefaultIfEmpty配合,可以得到左外连接的效果。
var employeeOptions = employees .GroupJoin( empOptions, // 内序列 e => e.id, // 外选择子,其作用范围穿透至结果选择子内部 o => o.id, // 内选择子,其作用范围无法穿透至结果选择子 (e, os) => os // 结果选择子 .DefaultIfEmpty() // 如果没有匹配的内元素,即用Default替代 // 用内结果序列IEnumerable<EmployeeOption>构造新的序列 .Select(o => new { id = e.id, name = e.firstName, // 内元素可能为null,故判断之 options = o != null ? o.optionsCount : 0 })) // 得到序列IEnumerable<IEnumerable<匿名类>>,遂SelectMany拆解之 .SelectMany(r => r);
P106 GroupBy
经GroupBy()运算后,原IEnumerable<T>的序列将得到一个KeyValuePair<key, T>的序列。GroupBy()使用IEqualityComparer<>比较子,重载形式颇多。
P112 Distinct-Union-Intersect-Except
Distinct | 剔除序列中的重复项 |
Union | 并集 |
Intersect | 交集 |
Except | 减集 |
P117 Cast-OfType-AsEnumerable
同前所述,多用OfType,少用Cast。这与C#的强制类型转换与as关键字或者TryParse()的区别是类似的。
AsEnumerable是将支持IEnumerable<T>的序列变为IEnumerable<T>,从而屏蔽其原本自有的属性与方法(包括运算符)。比如A派生于IEnumerable<T>,有特型化的Where、Select等运算符,经AsEnumerable()后,便只提供IEnumerable<T>默认的运算符了。
P128 Range-Repeat-Empty
Range(int start, int count) |
产生一个从start开始,count个的整数序列 |
Repeat<T>(int count) |
产生一个重复count次的T类型序列 |
Empty<T>() | 产生一个空的T类型序列 |
P136 ToArray-ToList-ToDictionary-ToLookup
Dictionary要求Key唯一,而Lookup反之。都要求利用lambda表达式选择一个key,且支持自定义IComparer<T>。
P148 SequenceEqual
比较两个序列是否相同,另带一支持IEqualityComparer<T>比较子的格式。
P151 First-Last-Single-ElementAt
上述运算符,均有一个OrDefault版本以保证结果非null,另均可带一个返回bool类型的Func<T, bool>作为判断子。
P164 Any-All-Contains
Any:有否任意元素符合条件,可带一Func<T, bool>判断。
All:所有元素是否符合条件,必须带一Func<T, bool>判断。
Contains因其语义,可带一个IEqualityComparer<T>的比较子。
P170 Count/LongCount-Min/Max-Sum/Average-Aggregate
Aggregate用以实现自定义的聚合操作。对源序列中的每个元素调用一次Func<>,并且将聚合值作为Func<>的第一个参数。不同的重载形式,区别在于用作聚合初始值的值。之后再用Func<>返回值替代当前的聚合值。