Fluent 语法
Fluent 语法的灵活性给我们操作数据带来了极大的便利。接下来我们看一看如何链结查询操作符以形成更复杂的查询,并说明扩展方法在此过程中的重要性。
Chaining Query Operators 连接查询操作符
我们展示了两个简单的查询,每个查询都包含一个查询操作符。要构建更复杂的查询,可以向表达式添加附加查询操作符,创建一个链。为了说明这一点,下面的查询提取了所有包含字母“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 = names .Where (n => n.Contains ("a")) .OrderBy (n => n.Length) .Select (n => n.ToUpper()); foreach (string name in query) Console.WriteLine (name); } }
注意:(这里很重要,写代码时很容易出现变量作用域的问题)
1 在上面的示例中,变量'n'被私有地限定为每个lambda表达式的作用域。我们可以重新使用标识符n.
2 查询操作符永远不会改变输入序列;相反,它返回一个新的序列。(这与函数式编程范式一致,LINQ的灵感来自于此,Spark 的算子也是这样)。
数据流从左到右通过操作符链,因此首先过滤数据,然后排序,然后投影。它的自然线性形状反映了从左到右的数据流,并将lambda表达式与其查询操作符(中缀表示法)放在一起。
如果没有扩展方法,查询将失去流畅性,下面是这些扩展方法的签名:
public static IEnumerableWhere (this IEnumerable source, Func bool> predicate) public static IEnumerable OrderBy (this IEnumerable source, Func keySelector) public static IEnumerable Select (this IEnumerable source, Func selector)
一个完整的 LINQ 查询类似于传送带生产线。
下面我们可以逐步构造相同的查询,我们可以使用传统的静态方法语法来调用查询操作符,而不是使用扩展方法语法。例如:
IEnumerable<string> filtered = Enumerable.Where (names, n => n.Contains ("a")); IEnumerable<string> sorted = Enumerable.OrderBy (filtered, n => n.Length); IEnumerable<string> finalQuery = Enumerable.Select (sorted, n => n.ToUpper());
用传统的方式实现,里面的方法返回结果是外层方法的参数,一团糟是不是。
IEnumerable<string> query = Enumerable.Select ( Enumerable.OrderBy ( Enumerable.Where ( names, n => n.Contains ("a") ), n => n.Length ), n => n.ToUpper() ); /******************对比一下扩展方法链**************/ IEnumerable<string> query = names .Where (n => n.Contains ("a")) .OrderBy (n => n.Length) .Select (n => n.ToUpper());
结合 Lambda 表达式
回顾一下上面的例子,我们将以下lambda表达式输入Where运算符:
n => n.Contains ("a") // Input type=string, return type=bool.
lambda 表达式在一个 query operator 里总是作用于单个elements 在输入序列中,而不是在整个序列上。也就是说,这个查询操作符 通常 需要你的lambda 表达式 在每一个输入序列的元素上计算一次。
Lambda表达式允许您在查询操作符中提供自己的逻辑。这使查询操作符 活多变,也让底层操作更简单。
这是IEnumerable的完整实现,除了异常处理:
public static IEnumerableWhere (this IEnumerable source, Func bool> predicate) { foreach (TSource element in source) if (predicate (element)) yield return element; }
可以看出标准操作符的实现是用lambda表达式实现Func委托。Func中的类型参数以与lambda表达式相同的顺序出现。也就是说你可以调用 在Enumerable中的查询操作符 通过引用的方式使用传统的委托, 而代替 lambda 表达式。
注意,它不适用于基于iquerizable
Lambda 表达式 And element 类型
标准操作符使用以下类型参数名:
public static IEnumerableSelect (this IEnumerable source, Func selector)
一个输入元素对应一个输出元素,TSource 和 TResult 可以是不同的类型,所以lambda 表达式可以更变每个元素的类型。更进一步说,lambda 表达式可以决定这个输出序列类型。
下面的查询使用Select将字符串类型元素转换为整数类型元素:
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表达式的返回值推断TResult的类型。在这种情况下,n.Length返回一个int值,因此TResult被推断为int。这种方法可以有效地简化某些类型的本地查询—特别是使用LINQ to XML
Natural Ordering
在LINQ中,输入序列中元素的原始顺序非常重要。一些查询操作符依赖于这种顺序,比如Take、Skip和Reverse。
Take操作符输出前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 }
Other Operators
不是所有查询操作符都返回一个序列,element 操作符从输入序列中提取一个元素;
例如: First, Last , and 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
aggregation 操作符
int count = numbers.Count(); // 5; int min = numbers.Min(); // 6;
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 }