为何下述LINQ查询中第二个from子句未执行?
这事儿我之前也踩过类似的坑,核心问题出在LINQ的延迟执行特性,再加上你写的LogQuery扩展方法的执行时机不对,咱们一步步拆解:
先还原你的问题场景
你写了一个嵌套LINQ查询,给两个子序列都加了LogQuery日志,但运行后只看到了Suit Generation和Starting Deck的日志,完全没看到Rank Generation的输出:
// 你的LINQ查询 var startingDeck = (from s in Suits().LogQuery("Suit Generation") from r in Ranks().LogQuery("Rank Generation") select new {Suit = s, Rank = r}) .LogQuery("Starting Deck"); // 自定义的LogQuery扩展方法 public static IEnumerable<T> LogQuery<T>(this IEnumerable<T> sequence, string tag) { using (var writer = File.AppendText("StartingDeckQuery.log")) { writer.WriteLine($"Executing Query {tag}"); } return sequence; } // 迭代器方法 static IEnumerable<string> Suits() { yield return "clubs"; yield return "diamonds"; yield return "hearts"; yield return "spades"; } static IEnumerable<string> Ranks() { yield return "two"; yield return "three"; yield return "four"; yield return "five"; yield return "six"; yield return "seven"; yield return "eight"; yield return "nine"; yield return "ten"; yield return "jack"; yield return "queen"; yield return "king"; yield return "ace"; }
问题出在哪?
1. LINQ的延迟执行是核心
LINQ to Objects的查询是延迟执行的——也就是说,你定义startingDeck的时候,并没有真正执行任何迭代逻辑,只是构建了一个查询的"蓝图"。只有当你实际枚举这个序列的时候(比如调用ToList()、foreach遍历,或者用Count()这类触发枚举的方法),才会真正跑起来。
2. 你的LogQuery是"提前写日志"
现在你的LogQuery是在调用扩展方法的瞬间就写入日志,然后直接返回原序列。那咱们看查询的构建过程:
- 当你写
Suits().LogQuery("Suit Generation")时,这个方法会立刻执行:写入日志,然后返回Suits()的迭代器对象。 - 但
Ranks().LogQuery("Rank Generation")这部分,是嵌套在查询里的——它只有在枚举外层的每个花色元素时,才会被调用。而你现在只是定义了查询,根本没开始枚举,所以这行代码完全没机会执行,自然没日志。
3. 验证一下:触发枚举试试
如果你在定义startingDeck之后加一行代码:
var actualDeck = startingDeck.ToList();
这时候再看日志,你会看到:
Executing Query Suit Generation Executing Query Starting Deck Executing Query Rank Generation Executing Query Rank Generation Executing Query Rank Generation Executing Query Rank Generation
因为有4个花色,每个花色都会触发一次Ranks().LogQuery(),所以会出现4次Rank Generation的日志——这才是它真正的执行时机。
怎么让LogQuery准确记录枚举时机?
如果你想让日志反映序列真正被枚举的时刻,而不是构建查询的时刻,应该把LogQuery改成用yield return包裹原序列,这样日志会在枚举的时候才写入:
public static IEnumerable<T> LogQuery<T>(this IEnumerable<T> sequence, string tag) { // 只有当序列被枚举时,才会执行这里的代码 using (var writer = File.AppendText("StartingDeckQuery.log")) { writer.WriteLine($"Executing Query {tag}"); } foreach (var item in sequence) { yield return item; } }
这时候,如果你只是定义startingDeck,不会有任何日志;当你调用ToList()时,日志会按实际枚举顺序输出:
Executing Query Starting Deck Executing Query Suit Generation Executing Query Rank Generation Executing Query Rank Generation Executing Query Rank Generation Executing Query Rank Generation
因为最外层的LogQuery("Starting Deck")会先被触发,然后它会枚举内层查询,进而触发Suit Generation,接着每个花色触发一次Rank Generation。
内容的提问来源于stack exchange,提问作者bit

