c#提供了语法编写LINQ查询的快捷方式,称为查询表达式。
和前一节一样的字符串提取、按长度排序、并转为大写,query expression 语法是这样的:
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); } }
查询表达式总是以from子句开头,以select或group子句结尾。
-
编译器通过将 query expression 翻译成 fluent syntax 来处理它。
Range Variables
紧跟着from关键字语法的标识符称为范围变量。
编译器机械的转换到 fluent syntax 是像下面这样的:
names.Where (n => n.Contains ("a")) // Locally scoped n .OrderBy (n => n.Length) // Locally scoped n .Select (n => n.ToUpper()) // Locally scoped n
正如您所看到的,n的每个实例的作用域都是自己的lambda表达式。query operator 中的 n 都是局部范围有效。
查询表达式还允许您通过以下子句引入新的范围变量:
• let
• into
• An additional from clause
• join
Query Syntax 对比 SQL Syntax
表面上看,它们两个非常不同的。
-
LINQ中的子查询只是另一个c#表达式,因此不需要特殊的语法。SQL中的子查询受特殊规则的约束。
-
使用LINQ,数据逻辑上从左向右流经查询。对于SQL,顺序在数据流方面的结构不是很好,SQL的书写顺序和实际执行顺序有很大不同。
-
LINQ 是和集合的元素顺序有关系的,SQL 对数据集合的顺序不敏感,也可以是无序的。
Query Syntax Versus Fluent Syntax
-
Query 和 fluent syntax 各有优势,但是它们的本质是一样的,看实际情况来使用它们.
Mixed - Syntax Queries
如果查询操作符不支持查询语法,则可以混合使用查询语法和连贯语法。
怎么方便怎么来,例如:
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" }; int matches = (from n in names where n.Contains ("a") select n).Count(); //3 string first = (from n in names orderby n select n).First(); // Dick
Deferred Execution 延时执行
一个重要特性是 大多数查询操作符,它们不是在构造时执行,而是在枚举时执行。
看下面这个 query:
var numbers = new List<int> { 1 }; IEnumerable<int> query = numbers.Select (n => n * 10); // Build query numbers.Add (2); // Sneak in an extra element foreach (int n in query) Console.Write (n + "|"); // 10|20|
上面的例子说明了Select 操作符的计算延时到 foreach运行时执行。这被称为延时执行。同样的事情也发生在委托上。
Action a = () => Console.WriteLine ("Foo"); // We've not written anything to the Console yet. Now let's run it: a(); // Deferred execution!
所有标准查询操作符都提供延迟执行,但以下情况除外::
• 操作符返回单个 element or scalar value, 例如 First or Count
• 还有下例 conversion operators:
ToArray, ToList, ToDictionary, ToLookup
上述这些操作符会导致 query execution 立马执行, 因为它们的返回类型没有提供延迟执行的机制。
Count 方法,返回一个整数,它不是一个可枚举的类型,所以会立马执行。
延迟执行很重要,因为使得 查询构造 与 查询执行 解耦。这允许你在几个步骤中构建一个查询,并使数据库查询成为可能。子查询中的所有内容都受制于延迟执行—包括聚合和转换方法。
Reevaluation 重新求值
延迟执行还有另一个结果,当你重新枚举时,一个延迟执行查询被重新求值:
有几个原因说明重新求值的不好的地方:
-
想要缓存某个时间点的结果时。
-
有些查询是计算密集型的(或者依赖于查询远程数据库),所以不想不必要地重复它们。
解决办法是:
定义一个临时变量 缓存它:
var numbers = new List<int>() { 1, 2 }; List<int> timesTen = numbers .Select (n => n * 10).ToList(); // Executes immediately into a List numbers.Clear(); Console.WriteLine (timesTen.Count); // Still 2
Captured Variables
延时查询的另一个问题就是,If your query’s lambda expressions capture outer variables, the query will honor the value of those variables at the time the query runs: (查询将在查询运行时尊重这些变量的值:)
啥叫在查询时尊重这些变量的值呢?看下面的例子就能明白,因为是延时计算,在表达式中的变量只是一个占位符,你写好LINQ 语句后,这个占位符在你不注意时换成其他的,结果就会和你预期的 不一样了。
int factor = 10; IEnumerable<int> query = numbers.Select (n => n * factor); factor = 20; foreach (int n in query) Console.Write (n + "|"); // 20|40| ,本来以为会是10,20.
在for循环中构建查询时,这可能是一个陷阱。例如,假设我们想从字符串中删除所有元音。下面虽然效率不高,但给出了正确的结果:
IEnumerable<char> query = "Not what you might expect"; query = query.Where (c => c != 'a'); query = query.Where (c => c != 'e'); query = query.Where (c => c != 'i'); query = query.Where (c => c != 'o'); query = query.Where (c => c != 'u');
foreach (char c in query) Console.Write (c); //输出会是这样: Nt wht y mght xpct
现在我们重构了一下这个for 循环,看一看能发生什么。
IEnumerable<char> query = "Not what you might expect"; string vowels = "aeiou"; for (int i = 0; i < vowels.Length; i++) query = query.Where (c => c != vowels[i]); foreach (char c in query) Console.Write (c); //输出会是这样: Not what yo might expect
解决办法是,定义一个临时变量,每次for 循环时使用的都是新的值:
for (int i = 0; i < vowels.Length; i++) { char vowel = vowels[i]; query = query.Where (c => c != vowel); }
How Deferred Execution Works(延时执行是怎么工作的呢?)
看一下书上是怎么说的
Query operators provide deferred execution by returning decorator sequences。
a decorator sequence (in general) has no backing structure of its own to store elements。 Instead, it wraps another sequence that you supply at runtime, to which it maintains a permanent dependency. Whenever you request data from a decorator, it in turn must request data from the wrapped input sequence.
查询操作符通过返回修饰符序列来提供延迟执行。
装饰器序列(通常)没有自己的底层结构来存储元素,相反,它封装了另一个你在运行时支持的序列,并对它保持永久依赖,无论你什么时候从装饰器中请求数据,它必须从封装的输入数据返回。
那啥是装饰器呢?
-
这个装饰器就是像 where 、select 这样的东西。
前面说到LINQ 就像一个生产流水线,那where 和 select 这样的操作符就是生产线上的一个个环节,你写的LINQ 语句是在搭建这个生产线,在你执行 ToArray, ToList, ToDictionary, ToLookup 这些操作符时,就可以在生产线的终端获得处理过的数据。
如果输出序列没有执行转换,那么它将是一个代理而不是装饰器。
你还可以写一个自己的操作符,用C# 的迭代器来实现装饰器序列(还有Func 委托,扩展函数),下面是如何编写自己的Select方法(参数验证除外):
public static IEnumerableSelect (this IEnumerable source, Func selector) { foreach (TSource element in source) yield return selector (element); }
因此,当您调用Select或Where等操作符时,所做的只是实例化一个修饰输入序列的可枚举类。
Chaining Decorators
链接查询操作符创建装饰器层。
看下面的例子:
IEnumerablequery = new int[] { 5, 12, 3 }.Where (n => n < 10) .OrderBy (n => n) .Select (n => n * 10);
每个查询操作符实例化一个新的装饰包装前面的序列,(如同一个俄罗斯嵌套玩偶)。
当您枚举查询时,您是在查询通过分层或装饰器链转换的原始数组。(本质好似是一个委托链,)
一个查询的执行过程如下图所示:
LINQ follows a demand-driven pull model, rather than a supply-driven push model. This is important—as we’ll see later —in allowing LINQ to scale to querying SQL databases.