LINQ中Enumerable.Repeat与Last导致副作用执行次数不符预期的原因
LINQ中Enumerable.Repeat与Last方法的异常行为解析
问题重现
测试代码
[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
对比实现
替换为自定义的MyRepeat或MyLast后测试均可通过:
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; } }
(测试基于.NET 6,最初发现于.NET Core 3.1环境)
原因解析
1. Enumerable.Repeat的特殊实现
.NET中的Enumerable.Repeat并非像自定义的MyRepeat那样通过yield逐个生成元素,而是返回一个实现了IList<T>接口的自定义集合类型(如RepeatIterator<T>)。这个集合支持直接通过索引访问元素,无需遍历整个序列。
2. Enumerable.Last的智能优化
针对Select后调用Last的场景,.NET LINQ会进行针对性优化:当Select的源序列实现了IList<T>时,Last方法不会遍历整个Select序列,而是直接获取源序列的最后一个元素,仅对该元素执行一次Select的转换委托。
具体执行流程
当执行Enumerable.Repeat(0,10).Select(x => {i++; return x;}).Last()时:
Enumerable.Repeat返回一个包含10个0的IList<T>集合Last方法检测到Select的源是可索引的IList<T>,直接通过索引[9]获取源的最后一个元素- 仅对该元素执行一次
Select的委托逻辑,i仅递增1次 - 最终返回转换后的元素,导致测试中
i的值为1而非预期的10
而自定义的MyRepeat是普通的yield迭代器(未实现IList<T>),MyLast也没有这种优化逻辑,因此会遍历整个序列,i递增10次,测试通过。
内容的提问来源于stack exchange,提问作者Earth Engine
相关产品推荐
相关产品推荐

