本周我参加了荷兰的TechDays 2013,我得到了一个有趣的测验问题。问题是:以下程序的输出是什么。这是代码的样子。
class Program
{
delegate void Writer();
static void Main(string[] args)
{
var writers = new List<Writer>();
for (int i = 0; i < 10; i++)
{
writers.Add(delegate { Console.WriteLine(i); });
}
foreach (Writer writer in writers)
{
writer();
}
}
}
显然,我给出的答案是错误的。我认为,因为int是一个值类型,传递给Console.WriteLine()
的实际值会被复制,因此输出将为0 ... 9。但是,在这种情况下,i
将作为引用类型处理。正确答案是它将显示十次10.有人可以解释原因和方法吗?
答案 0 :(得分:6)
我认为,因为int是一个值类型,传递给Console.WriteLine()的实际值被复制
这是完全正确的。 当您致电WriteLine
时,该值将被复制。
那么,你什么时候打电话给WriteLine
?它不在for
循环中。你不是在那个时候写任何东西,你只是在创建一个代表。
当你调用委托时,直到foreach
循环,那时变量i
中的值被复制到堆栈以调用WriteLine
。
那么,i
循环期间foreach
的价值是多少?对于foreach
循环的每次迭代,它都是10。
所以现在你问,“i
期间foreach loop, isn't it out of scope
是怎么回事。嗯,不,不是。这证明是一个”闭包“。当一个匿名的方法参考一个变量,变量的范围需要持续的时间与匿名方法一样长,可以是任何时间段。如果没有什么特别的,那么读取变量将是随机垃圾包含任何碰巧被困在内存中的位置C#积极确保不会发生这种情况。
那它是做什么用的?它创建了一个闭包类;它是一个包含许多字段的类,代表所有被关闭的字段。换句话说,代码将被重构为如下所示:
public class ClosureClass
{
public int i;
public void DoStuff()
{
Console.WriteLine(i);
}
}
class Program
{
delegate void Writer();
static void Main(string[] args)
{
var writers = new List<Writer>();
ClosureClass closure = new ClosureClass();
for (closure.i = 0; closure.i < 10; closure.i++)
{
writers.Add(closure.DoStuff);
}
foreach (Writer writer in writers)
{
writer();
}
}
}
现在我们都有一个匿名方法的名称(所有匿名方法都由编译器给出一个名称),我们可以确保变量将存在,只要引用匿名函数的委托存在。
看看这个重构,我希望很清楚为什么结果是10
被打印了10次。
答案 1 :(得分:4)
这是因为它是一个捕获的变量。请注意,此使用也会在foreach
中发生,但在C#5中 已更改 但是要重新编写代码你实际拥有的是什么:
class Program
{
delegate void Writer();
class CaptureContext { // generated by the compiler and named something
public int i; // truly horrible that is illegal in C#
public void DoStuff() {
Console.WriteLine(i);
}
}
static void Main(string[] args)
{
var writers = new List<Writer>();
var ctx = new CaptureContext();
for (ctx.i = 0; ctx.i < 10; ctx.i++)
{
writers.Add(ctx.DoStuff);
}
foreach (Writer writer in writers)
{
writer();
}
}
}
正如您所看到的:只有一个ctx
因此只有一个ctx.i
,当您foreach
超过writers
时,它就是10。
顺便说一下,如果你想让旧代码工作:
for (int tmp = 0; tmp < 10; tmp++)
{
int i = tmp;
writers.Add(delegate { Console.WriteLine(i); });
}
基本上,捕获上下文的范围与变量的范围相同;这里变量的范围是里面循环,因此生成:
for (int tmp = 0; tmp < 10; tmp++)
{
var ctx = new CaptureContext();
ctx.i = tmp;
writers.Add(ctx.DoStuff);
}
此处每个DoStuff
位于不同的捕获上下文实例上,因此具有不同且独立的i
。
答案 2 :(得分:1)
在您的情况下,委派的方法是匿名方法访问本地变量( for 循环索引i
)。也就是说,这些是 clousures 。
由于匿名方法在 for 循环之后被调用十次,因此它获取 i 的最新值。
以下是clousure行为的简化版本:
int a = 1;
Action a1 = () => Console.WriteLine(a);
Action a2 = () => Console.WriteLine(a);
Action a3 = () => Console.WriteLine(a);
a = 2;
// This will print 3 times the latest assigned value of `a` (2) variable instead
// of just 1.
a1();
a2();
a3();
答案 3 :(得分:0)
对我来说,通过将旧行为和新行为与原生Action
类进行比较来代替自定义Writer
,可以更容易理解。
在for,foreach变量和局部变量捕获的情况下,在C#5闭包之前捕获相同的变量(不是变量的值)。所以给出代码:
var anonymousFunctions = new List<Action>();
var listOfNumbers = Enumerable.Range(0, 10);
for (int forLoopVariable = 0; forLoopVariable < 10; forLoopVariable++)
{
anonymousFunctions.Add(delegate { Console.WriteLine(forLoopVariable); });//outputs 10 every time.
}
foreach (Action writer in anonymousFunctions)
{
writer();
}
我们只看到为变量 forLoopVariable
设置的最后一个值。但是,使用C#5,foreach循环已被修改。现在我们捕获不同的变量。
E.G。
anonymousFunctions.Clear();//C# 5 foreach loop captures
foreach (var i in listOfNumbers)
{
anonymousFunctions.Add(delegate { Console.WriteLine(i); });//outputs entire range of numbers
}
foreach (Action writer in anonymousFunctions)
{
writer();
}
因此输出更直观:0,1,2 ......
请注意,这是一个突破性的变化(虽然它被假定为次要变化)。这可能就是为什么for循环行为在C#5中保持不变。