提问人:Earth Engine 提问时间:9/30/2023 最后编辑:Theodor ZouliasEarth Engine 更新时间:9/30/2023 访问量:69
副作用执行效果低于预期 (LINQ)
Side effects executed less than expected (LINQ)
问:
下面是使用 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
令人惊讶的是,增加的副作用只执行了一次,而不是十次。因此,我尝试用我自己的直观/朴素实现替换这些 LINQ 方法:i
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 实现包括对已知类型的可枚举序列的各种优化。因此,像 和 这样的一些运算符可能会使用较短的路径来返回结果,而不是逐个元素枚举序列元素。例如,可以优化对 的查询,因为此集合提供对其元素的索引访问,而 则不提供。显然,由 Enumerable.Repeat
和 Enumerable.Range
生成的序列也可以优化。下面是再现观察结果的另一个示例:Last
ElementAt
List<T>
Queue<T>
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
另一个答案是有用的,但它并没有完全回答“如何”,而是关于“为什么”(优化)。
我深入研究了它,这是我的发现。
- 所有的方法,对于LinQ进行这种优化至关重要。因此,替换其中任何一个都会使优化无效并使测试通过。
Enumerable.Repeat
Enumerable.Select
Enumerable.Last
- 该函数给出一个标记为适合优化的类型,并提供最终实现。
Enumerable.Repeat
- 该函数检查源类型是否适合优化,如果适合,则将优化委托给它。其返回类型也被标记为适合优化。
Enumerable.Select
- 该函数检查源类型,如果它适合优化,则委托给优化。
Enumerable.Last
有一些简单的方法可以绕过这种优化。例如,在测试用例中,如果唯一重要的是完成了多少次迭代,我们可以编写
[Test]
public void Test()
{
var i = 0;
var v = Enumerable.Repeat(0, 10).Select(x =>
{
i++;
return x;
}).Append(0).Last();
Assert.That(v, Is.EqualTo(0));
Assert.That(i, Is.EqualTo(10));
}
这将通过。如果元素确实要在 Lambda 中计算,因此无法提前确定,请写入Last
[Test]
public void Test()
{
var i = 0;
var v = Enumerable.Repeat(0, 10).Select(x =>
{
i++;
return x + i;
}).Concat(new int[] {}).Last();
Assert.That(v, Is.EqualTo(10));
Assert.That(i, Is.EqualTo(10));
}
教训:在 LinQ 中使用副作用时再想一想
此问题证明 LinQ 不是为使用具有副作用的 lambda 而设计的。所以一般来说,我们不应该编写有副作用的 LinQ lambda。因此,这种代码模式自动成为代码的不良品味,即使它可能在您的代码中工作得很好。
评论
上一个:状态更改是否会卸载功能组件
评论