执行的副作用低于预期(LINQ)

问题描述 投票:0回答:1

这是使用 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 是如何以导致如此奇怪行为的方式实现这些方法的?

c# linq .net-core side-effects
1个回答
0
投票

.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
的性能优化位于此源代码文件中:

© www.soinside.com 2019 - 2024. All rights reserved.