遍历List<Action>时出现意外输出的原因咨询——C#面试疑难代码解析
我正在搜集有挑战性的面试代码片段,发现了两段无法解释的代码,于是将它们合并以便同时运行。代码如下:
using System.Collections.Generic; public class Program { public static void Main(string[] args) { var intActions = new List<Action>(); for (int i = 0; i < 4; i++) intActions.Add(() => { Console.WriteLine(i); }); foreach (var action in intActions) action(); string[] strings = { "abc", "def", "ghi" }; var stringActions = new List<Action>(); foreach (string str in strings) stringActions.Add(() => { Console.WriteLine(str); }); foreach (var action in stringActions) action(); } }
实际输出为:4 4 4 4 abc def ghi。我原本预期的结果要么是四个“4”加四个“ghi”,要么是“0 1 2 3”加“abc def ghi”,请问为何实际输出会是这样?
问题根源:闭包对变量的捕获方式差异
这其实是C#里闭包捕获变量的经典面试坑,核心在于闭包捕获的是「变量本身」,而不是变量当时的取值,但for和foreach循环在变量声明逻辑上有本质区别:
1. 第一个for循环的情况
在for (int i = 0; i < 4; i++)里,变量i是整个循环共用的——逻辑上它只被声明了一次,每次迭代只是修改它的值。当你把() => { Console.WriteLine(i); }添加到列表时,每个Lambda表达式都捕获了同一个i变量的引用。
等你执行这些Action的时候,for循环早就跑完了,此时i的值已经变成了4(循环结束的触发条件是i >=4),所以所有Action执行时都会读取到这个最终值,输出四次4。
2. 第二个foreach循环的情况
在C# 5及以后的版本中,foreach循环的行为做了针对性优化:每次迭代都会创建一个全新的str变量。也就是说,你添加到stringActions里的每个Lambda表达式,捕获的都是当前迭代专属的独立str变量,这些变量分别持有了数组中对应元素的值。
所以当你执行这些Action时,每个Lambda都会输出各自捕获的那个str的取值,也就是abc def ghi。
如果是C# 4及更早的版本,foreach的行为和for循环类似,所有Lambda会捕获同一个变量,最终会输出三次ghi——这也是你最初预期其中一种结果的原因,但现在的C#版本已经修复了这个反直觉的设计。
内容的提问来源于stack exchange,提问作者Behold The Light

