C# lambda表达式捕获的局部变量?

C# lambda表达式捕获的局部变量?,c#,C#,我正在读一本书,上面写着: lambda表达式或匿名委托捕获的局部变量由编译器转换为字段,因此也可以共享: 但是如果编译器已将done转换为一个字段,那么如果您使用类似的工具,为什么我无法在另一个方法TestField?中访问它: 您会注意到,在编译代码时,会生成编译器生成的类,并且done被声明为该类的字段: [CompilerGenerated] private sealed class <>c__DisplayClass0_0 { public bool done;

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

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

但是如果编译器已将
done
转换为一个字段,那么如果您使用类似的工具,为什么我无法在另一个方法
TestField

中访问它:

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

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

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

这是一些令人遗憾的措辞。先忘掉那句话,让我们谈谈范围

简化一下,我们可以假设局部变量在包含它的整个块的作用域中

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

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

} // x exits scope
这不是它的工作原理,但它是一个有用的模型。在上述代码中,由于范围内没有
y
,因此如果
块是错误,则在
块外引用
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
由分配给
inc
print
的lambda捕获,并在它们之间共享。有趣的是,捕获的变量可以“转义范围”:

在这里,
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\uu显示类0\u0
{
公共int x;
内部空隙b__2()
{
x++;
}
内部空隙b__3()
{
控制台写入线(x);
}
}
静态(Action inc,Action print)Foo()
{
c_uuuDisplayClass0_0C_uuuuDisplayClass0_0=新的c_uuuDisplayClass0_0();
c_uuu显示类0_uuu.x=0;
Action inc=新操作(c__显示类0_uu.b__2);
动作打印=新动作(c__显示类别0_uu.b__3);
返回(项目,项目2);
}
静态void Main()
{
(inc,print)=Foo();
公司();
公司();
打印();
}
这比编译器生成的要少。
c\uu显示类0\uu
是为匿名函数生成的类。其名称不能用有效的C#表示。如您所见,局部变量被转换为类中的字段,匿名函数成为该类的方法,它们都引用相同的共享字段


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

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

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

在您的代码中,
done
是在
Main()
方法中定义的,因此该方法中的所有函数都可以使用它,包括像
action
这样的lambda函数

如果在
action
lambda中定义了另一个变量
x
,则该变量可用于该lambda中的所有变量,但不适用于
Main()

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


这就是为什么
done
TestField()
不可用的原因,因为
TestField()
没有在
Main()
中定义,我想是因为
done
Action
委托的范围内(其行为与函数参数中的“正常”相同)该字段不是为ThreadTest类生成的,而是为lambda函数的类容器生成的,如果
Main()
不是
static
,会发生什么?
0
1
1
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.
}
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();
}