用LINQ时被Deferred Execution的特性给坑了,于是写文章来纪录下>_<
Deferred Execution简单来说就是写一个不包含强制马上执行命令(如:Count, Max等)的LINQ表达式时,它只会将表达式保存起来。真正执行的时机则是在真正使用时。具体例子见下图:
18行中的result没有保存结果,反而保存query本身。如果不明白“保存query本身”的真正含义,很容易被坑。以下是我被坑的简化版(不妨手算下输出,看是否会被坑):
坑一
1 using System; 2 using System.Collections.Generic; 3 using System.Diagnostics; 4 using System.IO; 5 using System.Linq; 6 7 public class Class1 8 { 9 static void Main(string[] args) 10 { 11 var allData = new Listint, string>>() { 12 new Tuple<int, string>(0, "Zero"), 13 new Tuple<int, string>(1, "and"), 14 new Tuple<int, string>(2, "Feng"), 15 new Tuple<int, string>(3, "yan") 16 }; 17 18 var t = 0; 19 var result = allData.Where(tuple => tuple.Item1 != t); 20 21 t = 1; 22 foreach (var r in result) 23 { 24 Console.WriteLine("The " + (t++) + " item: " + r.Item2); 25 } 26 Console.ReadLine(); 27 } 28 }
实际输出如下:
预期输出应该不包含"Zero"的,那么为什么实际输出会包含呢?原因在于19行的result保存query本身,而这个query包含一个外部变量。详细分析如下:
foreach语句第一次执行时,"query“实际上是”allData.Where(tuple => tuple.Item1 != 1)","Zero"符合。
foreach语句第二次执行时,"query“实际上是”allData.Where(tuple => tuple.Item1 != 2)","and"符合。
foreach语句第三次执行时,"query“实际上是”allData.Where(tuple => tuple.Item1 != 3)","Feng"符合。
foreach语句第四次执行时,"query“实际上是”allData.Where(tuple => tuple.Item1 != 4)","yan"符合。
解决这坑的方法是强制query马上执行。即将19行变为
var result = allData.Where(tuple => tuple.Item1 != t).ToList();
此时就能得到期望的结果了。为了加深理解,可以判断下面的输出:
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; public class Class1 { static void Main(string[] args) { var allData = new Listint, string>>() { new Tuple<int, string>(0, "Zero"), new Tuple<int, string>(1, "and"), new Tuple<int, string>(2, "Feng"), new Tuple<int, string>(3, "yan") }; var t = 0; var result = allData.Where(tuple => tuple.Item1 == t); foreach (var r in result) { Console.WriteLine(r.Item2); t += 2; } Console.ReadLine(); } }
解释(字体为白色):
foreach语句第一次执行时,"query“实际上是”allData.Where(tuple => tuple.Item1 == 0)","Zero"符合。
foreach语句第二次执行时,"query“实际上是”allData.Where(tuple => tuple.Item1 == 2)","and"不符合,”Feng"符合
foreach语句第三次执行时,"query“实际上是”allData.Where(tuple => tuple.Item1 == 4)","yan"不符合,allData没有元素了,终止。
所以实际输出为:
Zero
Feng
坑二
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; public class Class1 { public static void Main(string[] args) { var allData = new Listint, string>>() { new Tuple<int, string>(0, "Zero"), new Tuple<int, string>(1, "and"), new Tuple<int, string>(2, "Feng"), new Tuple<int, string>(3, "yan") }; var result = allData.Where(tuple => tuple.Item1 != 0); output(result); allData.RemoveAt(3); output(result); Console.ReadLine(); } private static void output(IEnumerable int, string>> data) { foreach (var d in data) { Console.WriteLine(d.Item2); } Console.WriteLine("************"); } }
实际输出如下
这个输出说明即使query已经执行过一次,也不会保存结果。当再次使用query时,query会重新执行,并且使用当前的data source执行。
那为什么会这样呢?因为query保存的是query本身。也许你会说,这样做低效。嗯,在以上情景的确如此。但看下MS对LINQ的介绍就会理解MS为什么会选择这个”低效“的做法:
Language-Integrated Query (LINQ) is a set of features introduced in Visual Studio 2008 that extends powerful query capabilities to the language syntax of C# and Visual Basic. LINQ introduces standard, easily-learned patterns for querying and updating data, and the technology can be extended to support potentially any kind of data store. Visual Studio includes LINQ provider assemblies that enable the use of LINQ with .NET Framework collections, SQL Server databases, ADO.NET Datasets, and XML documents.
MS想实现在非database的情景中也能使用类似于sql语句的功能。那么同一个sql语句在database改变的情形下,自然应该得到不同的结果。当想要得到同一结果时,那么自然不应该让保存query,而应该保存结果,即强制query马上执行。
总结
在使用非马上执行的LINQ语句时,得到的是一个query。每次用到query时,query都会重新执行,并且执行时用的是当前的data source和外部变量的值,而不是定义这个query时这些变量的值。以下是两篇扩展阅读,以兴趣可看下:
LINQ and Deferred Execution
Don’t be too lazy – LINQ’s lazy evaluation gotchas