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

本文关键字:引用类型 处理 整数 | 更新日期: 2023-09-27 18:03:07

我参加了本周在荷兰举行的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();
        }
    }
}

显然,我给的答案是错误的。I参数结束,因为int是一个值类型,传递给Console.WriteLine()的实际值被复制,所以输出将是0…9。然而,在这种情况下,i是作为引用类型处理的。正确的答案是它会显示10乘以10。谁能解释一下为什么?

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

I参数结束,因为int是一个值类型,传递给Console.WriteLine()的实际值被复制

完全正确。当您调用WriteLine时,该值将被复制

那么,你什么时候调用WriteLine ?它不在for循环中。你当时没有写任何东西,你只是创建一个委托。

当你调用委托时,直到foreach循环,在那个时候,变量i中的值被复制到调用WriteLine的堆栈中。

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

所以现在你问,"iforeach 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次。

这是因为它是一个捕获的变量。注意,这个使用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

在您的例子中,委托的方法是匿名方法访问一个局部变量(循环索引i)。也就是说,这些都是元素

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

访问同一引用

的各种云的简单示例

下面是一个简化版本的闭包行为:

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#/的信息。净clousures !

对我来说,比较原生Action类代替自定义Writer的旧行为和新行为更容易理解。

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();
    }

我们只看到为变量 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中保持不变。