C#中访问LINQ查询变量为何会重复执行查询触发多次枚举?
问题描述
如下代码中为何Square()方法会被重复调用?我推测values被重复计算,但为什么会出现该现象?我们此前已经获取了values,为何查询逻辑会被再次触发?编译器又为什么会提示"Possible multiple enumeration"警告?
示例代码
static IEnumerable<int> Square(IEnumerable<int> a) { foreach(var r in a) { Console.WriteLine($"From Square {r * r}"); yield return r * r; } } class Wrap { private static int init = 0; public int Value { get { return ++init; } } } static void Main(string[] args) { var w = new Wrap(); var wraps = new Wrap[3]; for(int i=0; i<wraps.Length; i++) { wraps[i] = w; } var values = wraps.Select(x => x.Value); var results = Square(values); int sum = 0; int count = 0; foreach(var r in results) { count++; sum += r; } Console.WriteLine("Count {0}", count); Console.WriteLine("Sum {0}", sum); Console.WriteLine("Count {0}", results.Count()); Console.WriteLine("Sum {0}", results.Sum()); }
运行结果
From Square 1 From Square 4 From Square 9 Count 3 Sum 14 From Square 16 From Square 25 From Square 36 Count 3 From Square 49 From Square 64 From Square 81 Sum 194
问题原因
核心原因是C#中IEnumerable<T>的**延迟执行(懒加载)**特性,以及迭代器的工作机制:
- 所有返回
IEnumerable<T>的逻辑,包括LINQ的Select()这类投影方法、用yield return实现的Square()方法,在赋值时不会执行任何内部逻辑,只是保存了"如何生成序列"的规则。只有当你主动枚举序列时(比如foreach遍历、调用Count()/Sum()这类会触发全量遍历的LINQ方法),才会从头开始执行完整的迭代逻辑,生成序列元素。 - 上述代码中一共对
results序列发起了3次独立的枚举:- 第一次:手写
foreach循环遍历results累加count和sum - 第二次:调用
results.Count()获取元素数量 - 第三次:调用
results.Sum()计算元素总和
每次枚举都会从头走完整个迭代链路:先进入Square()的迭代逻辑遍历入参,遍历入参values时又会触发Select()的投影逻辑,每次都会重新调用Wrap.Value的getter,让静态字段init持续自增,因此会看到Square()被重复执行,每次输出的平方值都不一样。
- 第一次:手写
- 编译器提示"Possible multiple enumeration"(可能存在多次枚举),正是因为检测到对同一个
IEnumerable实例做了多次枚举操作:这种写法一方面如果迭代逻辑带副作用(比如代码里Value的getter会修改静态变量),会出现结果不一致的问题;另一方面如果迭代逻辑是高成本操作(比如查询数据库、读取远程接口),重复枚举会带来不必要的性能损耗。
修复方式
如果需要避免重复枚举,只需要在序列第一次生成时,用ToList()或ToArray()将迭代结果固化为内存中的实际集合即可,例如:
// 执行完这行时,Square和Select的逻辑已经跑完,结果存在List中 var results = Square(values).ToList();
后续无论多少次遍历results,都是直接读取内存中已经计算好的值,不会重复触发迭代逻辑,执行结果也会保持一致。
内容的提问来源于stack exchange,提问作者Drompai
相关产品推荐
相关产品推荐

