Lambda表达式捕获的局部变量?

时间:2020-10-21 10:55:21

标签: c#

我正在读一本书,上面写着:

由lambda表达式或匿名委托捕获的局部变量由编译器转换为字段,因此也可以共享:

class ThreadTest
{
    static void Main()
    {
       bool done = false;
       ThreadStart action = () =>
       {
           if (!done) { done = true; Console.WriteLine ("Done"); }
       };
       new Thread (action).Start();
       action();
    }

    /*
    void TestField() 
    {
       bool b = done; //error cannot reference `done`
    }
    */
}

但是如果编译器已将done转换为字段,那么为什么我无法通过另一方法TestField访问它?

3 个答案:

答案 0 :(得分:3)

如果您使用this之类的工具,

您会注意到,在编译代码时,会生成编译器生成的类,并且将done声明为此类的字段:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    public bool done;

    internal void <Main>b__0()
    {
        if (!done)
        {
            done = true;
            Console.WriteLine("Done");
        }
    }
}

因此您无法通过您的代码访问该字段。 done没有定义为ThreadTest类的类字段,而是定义为编译器generate类中的字段。稍后,它会在您的Main方法中使用,如下所示:

private static void Main()
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.done = false;
    ThreadStart threadStart = new ThreadStart(<>c__DisplayClass0_.<Main>b__0);
    new Thread(threadStart).Start();
    threadStart();
}

答案 1 :(得分:0)

这是一些不幸的措辞。忘掉那句话一秒钟,让我们谈谈范围

为简化起见,我们可以假定局部变量在包围它的整个块中在范围内

void Foo()
{
  var x; // x is in scope now

  if (...)
  {
      var y; // y is in scope now
      
      ...
  } // y exits scope

} // x exits scope

这不是确切的工作方式,但这是一个有用的模型。在上面的代码中,由于范围内没有y,因此在if块之外引用y是错误的。

这足以说明您的代码为什么不起作用。 done变量在整个Main方法的范围内,但不在TestField的范围内。

声明的作者很可能试图传达闭包的语义。匿名函数创建闭包,并且可以从封闭范围中捕获变量。这意味着例如以下代码:

void Foo()
{
    var x = 0;

    Action inc = () => { x += 1; };
    Action print = () => { Console.WriteLine(x); };

    print();
    inc();
    print();
    Console.WriteLine(x);
}

将打印

0
1
1

局部变量x被分配给incprint的两个lambda 捕获,并在它们之间共享。有趣的部分是捕获的变量可以“转义范围”:

static (Action inc, Action print) Foo()
{
    var x = 0;
    Action inc = () => { x += 1; };
    Action print = () => { Console.WriteLine(x); };

    return (inc, print);
}

static void Main()
{
    var (inc, print) = Foo();

    inc();
    inc();
    print(); // Prints 2.
}

此处x变量的生存期超过其范围,因为即使在Foo方法返回后也必须可以从lambda函数访问它。

要解决报价中“字段”部分的难题,我们必须问自己编译器如何实现这一点。方法如下:

private sealed class <>c__DisplayClass0_0
{
    public int x;

    internal void <>b__2()
    {
        x++;
    }

    internal void <>b__3()
    {
       Console.WriteLine(x);
    }
}

static (Action inc, Action print) Foo()
{
    <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
    <>c__DisplayClass0_.x = 0;

    Action inc = new Action(<>c__DisplayClass0_.<>b__2);
    Action print = new Action(<>c__DisplayClass0_.<>b__3);
    return (item, item2);
}

static void Main()
{
    (inc, print) = Foo();
    inc();
    inc();
    print();
}

这几乎是编译器生成的。 <>c__DisplayClass0_是为匿名函数生成的类。其名称无法在有效的C#中表达。如您所见,局部变量将转换为类中的字段,匿名函数成为该类的方法,并且它们都引用相同的共享字段。

因此,除非您开始调用反射和其他黑魔法,否则您通常无法从不同的范围访问该字段(谢天谢地,这将使您真正的讨厌的意大利面!)。

答案 2 :(得分:0)

除了没有被问到的问题,没有任何问题是愚蠢的。其他一些答案提供了更多的技术信息,但是正如您所说的,您是C#的新手,我认为更简单的解释可能会有所帮助。

在C#中,变量范围(有效地)由花括号定义。

在您的代码中,done是在Main()方法中定义的,因此该方法内 的所有内容都可以使用,包括action之类的lambda函数。 / p>

如果您在x lambda中定义了另一个变量action ,则该变量对所有{em> inside 的lambda都可用,但{{{ 1}}。

条件语句也是如此。如果在Main()块内声明变量,则该块内的所有 都可以使用它,但调用方法不可用。

这就是ifdone不可用的原因,因为{<1> inside TestField()中没有定义TestField()