这是使用 NUnit 风格的测试:
[Test]
public void Test()
{
var i = 0;
var v = Enumerable.Repeat(0, 10).Select(x =>
{
i++;
return x;
}).Last();
Assert.That(v, Is.EqualTo(0));
Assert.That(i, Is.EqualTo(10));
}
没想到,失败了:
Message:
Expected: 10
But was: 1
令人惊讶的是,增加
i
的副作用只出现了一次,而不是十次。因此,我尝试用我自己的直观/天真的实现来替换那些 LINQ 方法:
private T MyLast<T>(IEnumerable<T> values)
{
var enumerator = values.GetEnumerator();
while (enumerator.MoveNext())
{
}
return enumerator.Current;
}
private IEnumerable<T> MyRepeat<T>(T value, int count)
{
for(var i = 0; i<count; ++i)
{
yield return value;
}
}
我省略了更改的代码;但您可以验证,如果代码使用
MyRepeat
而不是 Enumerable.Repeat
,或者使用 MyLast
而不是 Enumerable.Last
,则测试通过。显然这两种方法的实现方式不同。
(以上是在.NET 6中测试的,但最初的观察是在使用.NET Core 3.1的一段代码中)
所以我的问题是:LINQ 是如何以导致如此奇怪行为的方式实现这些方法的?
.NET Core LINQ 实现包括针对已知类型的可枚举序列的各种优化。因此,某些运算符(例如
Last
和 ElementAt
)可能会使用较短的路径来返回结果,而不是逐个元素枚举序列。例如,可以优化对 List<T>
的查询,因为该集合提供对其元素的索引访问,而 Queue<T>
则不提供。显然,由 Enumerable.Repeat
和 Enumerable.Range
生成的序列也可以进行优化。这是另一个重现您的观察结果的示例:
Test(new List<int>(Enumerable.Range(0, 10)));
Test(new Queue<int>(Enumerable.Range(0, 10)));
Test(Enumerable.Range(0, 10));
static void Test(IEnumerable<int> source)
{
int iterations = 0;
int result = source.Select(x => { iterations++; return x; }).ElementAt(5);
Console.WriteLine($"{source}, result: {result}, Iterations: {iterations}");
}
输出
System.Collections.Generic.List`1[System.Int32], result: 5, Iterations: 1
System.Collections.Generic.Queue`1[System.Int32], result: 5, Iterations: 6
System.Linq.Enumerable+RangeIterator, result: 5, Iterations: 1
在线演示.
Enumerable.Repeat
的性能优化位于此源代码文件中: