C# 传递到委托时作为引用类型处理的整数

C# 传递到委托时作为引用类型处理的整数,c#,delegates,C#,Delegates,本周,我参加了在荷兰举行的2013年TechDays,我收到了一个有趣的问答题。问题是:以下程序的输出是什么。下面是代码的样子 class Program { delegate void Writer(); static void Main(string[] args) { var writers = new List<Writer>(); for (int i = 0; i < 10; i++) {

本周,我参加了在荷兰举行的2013年TechDays,我收到了一个有趣的问答题。问题是:以下程序的输出是什么。下面是代码的样子

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();
        }
    }
}
类程序
{
委托无效编写器();
静态void Main(字符串[]参数)
{
var writers=新列表();
对于(int i=0;i<10;i++)
{
Add(委托{Console.WriteLine(i);});
}
foreach(编剧中的编剧)
{
作者();
}
}
}
显然,我给出的答案是错误的。I argumentend,因为int是一种值类型,所以传递到
Console.WriteLine()
的实际值会被复制,因此输出将是0…9。但是,在这种情况下,
i
被作为引用类型处理。正确答案是,它将显示10乘以10。有人能解释一下原因和方式吗

I argumentend,因为int是一种值类型,所以传入Console.WriteLine()的实际值会被复制

这完全正确调用
WriteLine
时,将复制该值

那么,您什么时候在呼叫
WriteLine
?它不在
for
循环中。此时您没有编写任何内容,您只是创建了一个委托

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

那么,在
foreach
循环期间,
i
的值是多少?对于
foreach
循环的每次迭代,它是10

现在你在问,“在
foreach循环中,
i
怎么样,它不在范围之外吗?不,不是。这是一个“闭包”“。当匿名方法引用变量时,该变量的作用域需要与该匿名方法的作用域保持相同的时间,该时间段可以是任何时间段。如果根本没有做任何特殊的事情,那么读取变量将是随机垃圾,其中包含内存中该位置中发生的任何东西。积极确保这种情况不会发生

那么它有什么作用呢?它创建一个闭包类;它是一个包含许多字段的类,这些字段表示封闭的所有内容。换句话说,将对代码进行重构,使其看起来像这样:

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();
        }
    }
}
public类closure类
{
公共国际一级;
公共空间
{
控制台写入线(i);
}
}
班级计划
{
委托无效编写器();
静态void Main(字符串[]参数)
{
var writers=新列表();
ClosureClass closure=新的ClosureClass();
for(closure.i=0;closure.i<10;closure.i++)
{
writers.Add(closure.DoStuff);
}
foreach(编剧中的编剧)
{
作者();
}
}
}
现在我们都有了匿名方法的名称(所有匿名方法都由编译器命名),并且我们可以确保变量在引用匿名函数的委托存在的时间内一直存在


看看这个重构,我希望很清楚为什么
10
会被打印10次。

在您的例子中,委托方法是访问局部变量的匿名方法(for循环索引
I
)。也就是说,这些是clousures

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

访问同一引用的各种clousure的简单示例 以下是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();


这是因为它是一个捕获的变量。请注意,
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();
        }
    }
}
基本上,捕获上下文的作用域与变量处于同一级别;在这里,变量的作用域位于循环内部,因此生成:

for (int tmp = 0; tmp < 10; tmp++)
{
    var ctx = new CaptureContext();
    ctx.i = tmp;
    writers.Add(ctx.DoStuff);
}
for(int-tmp=0;tmp<10;tmp++)
{
var ctx=新CaptureContext();
ctx.i=tmp;
添加(ctx.DoStuff);
}

在这里,每个
DoStuff
都位于不同的捕获上下文实例上,因此有一个不同且独立的
i
对于我来说,通过将旧行为和新行为与本机
操作
类(而不是自定义
编写器
进行比较,更容易理解

在C#5之前,闭包在for、foreach变量和局部变量捕获的情况下捕获相同的变量(而不是变量的值)。因此,根据代码:

    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();
    }
所以输出更直观:0,1,2


请注意,这是一个突破性的变化(尽管它被认为是一个很小的变化)。这可能就是为什么for循环行为在C#5上保持不变。

@Oded我在4.0和4.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();
    }
    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();
    }