Dmi*_*nov 3 c# linq lazy-evaluation
我正在阅读这篇关于 LINQ 的文章,但无法理解查询是如何在惰性求值方面执行的。
因此,我将文章中的示例简化为以下代码:
void Main()
{
var data =
from f in GetFirstSequence().LogQuery("GetFirstSequence")
from s in GetSecondSequence().LogQuery("GetSecondSequence", f)
select $"{f} {s}";
data.Dump(); // I use LINQPAD to output the data
}
static IEnumerable<string> GetFirstSequence()
{
yield return "a";
yield return "b";
yield return "c";
}
static IEnumerable<string> GetSecondSequence()
{
yield return "1";
yield return "2";
}
public static class Extensions
{
private const string path = @"C:\dist\debug.log";
public static IEnumerable<string> LogQuery(this IEnumerable<string> sequence, string tag, string element = null)
{
using (var writer = File.AppendText(path))
{
writer.WriteLine($"Executing query {tag} {element}");
}
return sequence;
}
}
Run Code Online (Sandbox Code Playgroud)
执行此代码后,我在 debug.log 文件中得到了可以逻辑解释的输出:
执行查询 GetFirstSequence
执行查询 GetSecondSequence a
执行查询 GetSecondSequence b
执行查询 GetSecondSequence c
当我想将前三个元素与最后三个元素交错时,事情变得很奇怪,如下所示:
void Main()
{
var data =
from f in GetFirstSequence().LogQuery("GetFirstSequence")
from s in GetSecondSequence().LogQuery("GetSecondSequence", f)
select $"{f} {s}";
var shuffle = data;
shuffle = shuffle.Take(3).LogQuery("Take")
.Interleave(shuffle.Skip(3).LogQuery("Skip")).LogQuery("Interleave");
shuffle.Dump();
}
Run Code Online (Sandbox Code Playgroud)
当然,我需要添加扩展方法来交错两个序列(来自上述文章):
public static IEnumerable<string> Interleave(this IEnumerable<string> first, IEnumerable<string> second)
{
var firstIter = first.GetEnumerator();
var secondIter = second.GetEnumerator();
while (firstIter.MoveNext() && secondIter.MoveNext())
{
yield return firstIter.Current;
yield return secondIter.Current;
}
}
Run Code Online (Sandbox Code Playgroud)
执行这些代码行后,我在 txt 文件中得到以下输出:
执行查询 GetFirstSequence
执行查询 Take
执行查询 Skip
执行查询 Interleave 执行查询
GetSecondSequence a
执行查询 GetSecondSequence a
执行查询 GetSecondSequence b
执行查询 GetSecondSequence c
执行查询 GetSecondSequence b
这让我很尴尬,因为我不明白查询的执行顺序。
为什么查询会这样执行?
小智 5
var data =
from f in GetFirstSequence().LogQuery("GetFirstSequence")
from s in GetSecondSequence().LogQuery("GetSecondSequence", f)
select $"{f} {s}";
Run Code Online (Sandbox Code Playgroud)
只是另一种写作方式
var data = GetFirstSequence()
.LogQuery("GetFirstSequence")
.SelectMany(f => GetSecondSequence().LogQuery("GetSecondSequence", f), (f, s) => $"{f} {s}");
Run Code Online (Sandbox Code Playgroud)
让我们逐步浏览一下代码:
var data = GetFirstSequence() // returns an IEnumerable<string> without evaluating it
.LogQuery("GetFirstSequence") // writes "GetFirstSequence" and returns the IEnumerable<string> from its this-parameter without evaluating it
.SelectMany(f => GetSecondSequence().LogQuery("GetSecondSequence", f), (f, s) => $"{f} {s}"); // returns an IEnumerable<string> without evaluating it
var shuffle = data;
shuffle = shuffle
.Take(3) // returns an IEnumerable<string> without evaluating it
.LogQuery("Take") // writes "Take" and returns the IEnumerable<string> from its this-parameter without evaluating it
.Interleave(
shuffle
.Skip(3) // returns an IEnumerable<string> without evaluating it
.LogQuery("Skip") // writes "Skip" and returns the IEnumerable<string> from its this-parameter without evaluating it
) // returns an IEnumerable<string> without evaluating it
.LogQuery("Interleave"); // writes "Interleave" and returns the IEnumerable<string> from its this-parameter without evaluating it
Run Code Online (Sandbox Code Playgroud)
到目前为止的代码负责输出的前四行:
执行查询 GetFirstSequence 执行查询 Take 执行查询 跳过 执行查询交错
尚未评估任何 IEnumerable<string>。
最后,shuffle.Dump()迭代shuffle并评估 IEnumerables。
迭代会data打印以下内容,因为对 中的每个元素SelectMany()调用GetSecondSequence()和:LogQuery()GetFirstSequence()
执行查询 GetSecondSequence a 执行查询 GetSecondSequence b 执行查询 GetSecondSequence c
迭代shuffle与迭代相同
Interleave(data.Take(3), data.Skip(3))
Run Code Online (Sandbox Code Playgroud)
Interleave()交错两次迭代的元素data,因此也交错由迭代它们引起的输出。
firstIter.MoveNext();
// writes "Executing query GetSecondSequence a"
secondIter.MoveNext();
// writes "Executing query GetSecondSequence a"
// skips "a 1" from second sequence
// skips "a 2" from second sequence
// writes "Executing query GetSecondSequence b"
// skips "b 1" from second sequence
yield return firstIter.Current; // "a 1"
yield return secondIter.Current; // "b 2"
firstIter.MoveNext();
secondIter.MoveNext();
// writes "Executing query GetSecondSequence c"
yield return firstIter.Current; // "a 2"
yield return secondIter.Current; // "c 1"
firstIter.MoveNext();
// writes "Executing query GetSecondSequence b"
secondIter.MoveNext();
yield return firstIter.Current; // "b 1"
yield return secondIter.Current; // "c 2"
Run Code Online (Sandbox Code Playgroud)