引言:困扰新手的谜题
\n\n想象一下这样的场景:你在C#中写一个简单的foreach循环,在循环内部保存了一个lambda表达式或启动了一个异步任务,结果却得到了完全出乎意料的结果。所有你“精心捕获”的变量,突然都指向了集合中的最后一个元素。熟悉吗?
在Stack Overflow上,"Is there a reason for C#'s reuse of the variable in a foreach?"这个问题获得了数千次浏览和大量评论。这不仅仅是学术上的兴趣——这是一个连经验丰富的开发者都会掉进去的实际陷阱。在本文中,我们将探讨C#为何这样做,它随着语言版本如何变化,以及最重要的是,如何避免相关的错误。
\n\n我们将用通俗易懂的语言,配合代码示例,让即使是C#新手也能理解问题的本质。开始吧!
\n\n问题的本质:“变量重用”是什么意思?
\n\n在经典的C# foreach循环中(5.0版本之前),迭代变量在整个循环中只声明一次,而不是每次迭代都重新创建。这意味着,如果你在闭包中捕获了这个变量(例如在lambda表达式或匿名方法中),所有闭包都将引用同一个变量。
让我们看一个典型的问题示例:
\n\nvar actions = new List<Action>();\nvar numbers = new List<int> { 1, 2, 3, 4, 5 };\n\nforeach (var number in numbers)\n{\n actions.Add(() => Console.WriteLine(number));\n}\n\n// 执行所有操作\nforeach (var action in actions)\n{\n action();\n}\n\n\n问题:这段代码会输出什么?直觉上似乎是1, 2, 3, 4, 5。但实际上(在C# 5之前),它会输出5, 5, 5, 5, 5。
\n\n为什么?因为变量number在整个循环过程中只存在一个实例。当循环结束时,它的值等于最后一个元素(5)。所有lambda捕获的是这个变量的引用,而不是创建时的值。
理解类比
\n\n想象你有一个盒子(变量),你依次往里面放不同的物品(值)。你给朋友们(lambda)的不是物品本身,而是盒子的钥匙。当朋友们来取他们的物品时,他们打开盒子,发现里面是你最后放进去的那个物品。这就是旧版foreach的工作方式。
历史背景:为什么这样做?
\n\n很多新手会问:“这明明是bug!为什么要这样做?”实际上,这是C#开发者的有意决定,基于性能和编译的简便性。
\n\n- \n
- 性能:每次迭代都创建新变量会带来额外的内存分配。对于大多数简单循环来说这并不关键,但在高负载系统中,每一次微优化都很重要。 \n
- 与for循环的一致性:在
for循环中,变量也只声明一次。开发者希望保持一致性。 \n - 编译器的简便性:旧方法在编译器中更容易实现。但代价是程序员面临不直观的行为。 \n
然而,社区很快发现了这个“特性”并开始提交bug报告。Stack Overflow上的问题“Is there a reason for C#'s reuse of the variable in a foreach?”成为了最热门的问题之一。微软听取了意见,并在C# 5.0(2012年)中改变了这一行为。
\n\nC# 5.0中发生了什么变化?
\n\n从C# 5.0开始,编译器为foreach循环的每次迭代都创建一个新变量。现在,上面的示例将按直觉正确工作,输出1, 2, 3, 4, 5。
// 这是