线程如何在方法结束后访问局部变量?

10

假设我有一个C#方法,如下所示:

public void MyMethod()
    {
        int i = 0;

        var thread = new Thread(() =>
        {
            Thread.Sleep(100);

            if (i == 0)
            {
                Console.WriteLine("Value not changed and is {0}", i);
            }
            else
            {
                Console.WriteLine(" Value changed to {0}.", i);
            }
        });

        thread.Start();


        i = 1;
    }

这个方法创建了一个线程,访问在该方法中创建的局部变量。当它访问该变量时,方法已经结束,因此局部变量 i 不应存在。但是代码可以正常运行。据我理解,在方法块完成后,局部变量不应该存在。我无法理解这一点。

5个回答

15

这是因为编译器会重写你的代码并使用闭包

由于你在Lambda内部使用了变量,所以变量最终会被更改成类的成员。编译后的代码不包含i的局部变量 - 即使你是这样写的。相反,它会重写你的代码并使用由编译器生成的类,该类包含一个Int32作为成员变量,并且"本地代码"以及Lambda都将引用此类成员。

有关详细信息,请参阅此闭包博客文章,它可以让你大致了解编译器在此处的工作方式。


5
根据我的理解,在方法块完成后,局部变量不存在。我无法理解这一点。
您的理解是错误的。局部变量的定义特征是,它的名称仅在其声明块内部范围内有效。这就是为什么它被称为“局部”变量——因为它的名称只能在局部可见。
您错误地认为“局部”意味着“短暂的”。这是不正确的;当它可以时,局部变量是短暂的,但有三种情况下它不能是短暂的:当它是匿名函数的闭合外部变量时,当它在迭代器块中时,或者当它在异步块中时。
巧合的是,您的问题是我上周博客的主题,请参阅更多详细信息:

http://blogs.msdn.com/b/ericlippert/archive/2012/01/16/what-is-the-defining-characteristic-of-a-local-variable.aspx


你会用什么术语来描述在IL中用于保存未提升为类字段的变量的实体类型?从执行的角度来看,它被写作一个变量,但保存在编译器类中,就像是一个类字段。 - supercat
@supercat:我会称之为局部变量。但那时我指的是MSIL语言,而不是C#语言。 - Eric Lippert
出于好奇,您是否知道创建C#的人是否考虑要求在闭包中使用的变量被声明性地标记?我认识到闭包可能很有用,但是将变量隐式提升到闭包中似乎很危险,有时会产生不清晰的语义。明确声明闭包将使语义更清晰。 - supercat
@supercat,如果你想要这样的语言,可以看看C++11。它允许你明确指定哪些本地变量可以在lambda中使用。 - svick

4

这被称为闭包
它可以将局部变量的生命周期延长超出方法的范围。


0

你启动的作为新线程的 lambda 表达式在 i 上创建了一个闭包;由于闭包是基于变量而不是值关闭的,因此该线程引用与外部范围相同的对象,并且即使外部范围已结束,它仍将能够访问其变量。


0
C#编译器通常将C#代码转换为一个中间语言MSIL,就像C#一样,它有本地变量。在MSIL中,局部变量的行为与您所期望的C#变量的行为相同:它们在定义它们的例程退出时停止存在。此外,当使用本地变量的C#代码被翻译为使用本地变量的MSIL代码时,这些C#变量的行为就像MSIL变量一样:它们在定义函数退出时停止存在。然而,并不是所有使用本地变量的C#代码都使用MSIL本地变量来存储它们。
在各种情况下,编译器会采用写成使用本地变量的代码,并在将其翻译成MSIL之前,重写它以定义一个新的类对象,其中包含相关变量的字段,然后重写对这些变量的访问,使它们访问新的字段。如果“变量”被委托使用,则该委托将获得对该新类对象的引用。即使定义对象的函数退出,对象本身也将继续存在,只要任何内容,包括委托的任何副本,持有引用。
编译器确定何时使用MSIL本地变量以及何时重写使用类字段的规则可能很棘手,对于例程的某个部分进行微小更改可能会更改其他看似不相关部分的语义。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接