传递给委托时,整数作为引用类型处理

本周我参加了荷兰的TechDays 2013,我得到了一个有趣的测验问题。 问题是:以下程序的输出是什么。 这是代码的样子。

class Program { delegate void Writer(); static void Main(string[] args) { var writers = new List(); 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.有人可以解释为什么以及如何解释?

我认为,因为int是一个值类型,所以传递给Console.WriteLine()的实际值会被复制

这是完全正确的。 当您调用WriteLine ,将复制该值

那么,你什么时候打电话给WriteLine ? 它不在for循环中。 你不是在那个时候写任何东西,你只是创建一个委托。

直到你调用委托的foreach循环时,它才会将变量i中的值复制到堆栈以调用WriteLine

那么,在foreach循环期间i的价值是什么? 对于foreach循环的每次迭代,它都是10。

所以现在你问,“好吧,在foreach loop, isn't it out of scope i是怎么回事foreach loop, isn't it out of scope 。嗯,不,它不是。这certificate了什么是”闭包“。当匿名方法引用变量时该变量的范围需要持续的时间与匿名方法一样长,可以是任何时间段。如果没有什么特别的,那么读取变量将是随机垃圾,包含发生在内存中该位置的任何事情.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(); 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次。

这是因为它是一个捕获的变量。 请注意,这也常常发生在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(); 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 ,而当你对writers ctx.i时它只有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

在您的情况下,委托方法是访问局部变量的匿名方法for循环索引i )。 也就是说,这些都是clousures

由于匿名方法在for循环之后被调用十次,因此它获取i的最新值。

访问相同参考的各种clousures的简单示例

这是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(); 

请查看StackOverflow上的其他问答( .NET中的内容是什么 ),了解有关C#/。NET clousures的更多信息!

对我来说,通过将旧行为和新行为与本机Action类进行比较来代替自定义Writer ,可以更容易理解。

在for,foreach变量和局部变量捕获的情况下,在C#5闭包之前捕获相同的变量(不是变量的值)。 所以给出代码:

  var anonymousFunctions = new List(); 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循环已被修改。 现在我们捕获不同的变量。

例如

  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中保持不变的原因。