为什么 lambda 表达式在方法终止后保留封闭范围变量值?
Why does a lambda expression preserve enclosing scope variable values after method terminates?
我的印象是 C# 中的 lambda 表达式上下文包含对其中使用的父函数作用域变量的引用。考虑:
public class Test
{
private static System.Action<int> del;
public static void test(){
int i = 100500;
del = a => System.Console.WriteLine("param = {0}, i = {1}", a, i);
del(1);
i = 10;
del(1);
}
public static void Main()
{
test();
}
}
产出
param = 1, i = 100500
param = 1, i = 10
但是,如果这是真的,则以下内容将是非法的,因为 lambda 上下文将引用超出范围的局部变量:
public class Test
{
private static System.Action<int> del;
public static void test(){
int i = 100500;
del = a => System.Console.WriteLine("param = {0}, i = {1}", a, i);
}
public static void Main()
{
test();
del(1);
}
}
但是,这样编译运行并输出
param = 1, i = 100500
这意味着发生了一些奇怪的事情,或者上下文保留了局部变量的值,而不是对它们的引用。但如果这是真的,它就必须在每次 lambda 调用时更新它们,而且我不知道当原始变量超出范围时它会如何工作。此外,这似乎在处理大值类型时会产生开销。
我知道,例如,在 C++ 中,这是 UB(已在 的回答中确认)。
问题是,这是 C# 中定义明确的行为吗? (我认为 C# 有一些 UB,或者至少有一些 IB,对吗?)
如果它定义明确,那么它实际上是如何工作的,为什么会起作用? (实现逻辑会很有趣)
与 C# 中的 lambda 语法相关的闭包概念是一个非常大的主题,对于我来说太大了,无法仅在这个答案中涵盖所有内容,但让我们至少尝试在这里回答具体问题。实际答案在底部,其余部分是理解答案所需的背景。
当编译器尝试使用匿名方法编译方法时,会在某种程度上重写该方法。
基本上就是生成一个新的class,把匿名方法提升到这个class里面。它被赋予了一个名称,尽管是一个内部名称,因此对于编译器来说,它有点从匿名方法转换为命名方法。但是,您不必知道或处理该名称。
此方法需要的任何变量,除匿名方法外声明的变量,但在与 used/declared 匿名方法相同的方法中,将被提升为好吧,然后这些变量的所有用法都被重写了。
现在这里涉及到几个方法,所以上面的文字变得难以阅读,所以我们来举个例子:
public Func<int, int> Test1()
{
int a = 42;
return value => a + value;
}
这个方法重写成这样:
public Func<int, int> Test1()
{
var dummy = new <>c__DisplayClass1();
dummy.a = 42;
return dummy.<Test1>b__0;
}
internal class <>c__DisplayClass1
{
public int a;
public int <Test1>b__0(int value)
{
return a + value;
}
}
编译器可以处理所有这些时髦的名称(是的,它们确实是用所有括号命名的)因为它指的是带有 id 和对象引用的东西,名称不再是编译器的问题。但是,您永远不能声明 class 或具有这些名称的方法,因此编译器不会生成恰好已经存在的 class 的风险。
这是一个 LINQPad 示例,它显示了我声明的 class,虽然名称中的括号较少,但看起来与编译器生成的相同:
void Main()
{
var f1 = Test1();
f1(10).Dump();
f1.Dump();
var f2 = Test2();
f2(10).Dump();
f2.Dump();
}
public Func<int, int> Test1()
{
int a = 42;
return value => a + value;
}
public Func<int, int> Test2()
{
var dummy = new __c__DisplayClass1();
dummy.a = 42;
return dummy._Test2_b__0;
}
public class __c__DisplayClass1
{
public int a;
public int _Test2_b__0(int value)
{
return a + value;
}
}
输出:
如果您查看上面的屏幕截图,您会注意到每个委托变量有两件事,一个 Method
属性 和一个 Target
属性.
调用该方法时,会使用引用 Target
对象的 this
引用来调用它。因此,委托捕获两件事:调用哪个方法,以及调用它的对象。
所以基本上,生成的对象 class 作为委托的一部分存在,因为它是方法的目标。
考虑到所有这些,让我们看看您的问题:
为什么 lambda 表达式在方法终止后保留封闭范围变量值?
A: 如果 lambda 存活下来,所有捕获的变量也会存活下来因为它们不再是它们在[中声明的方法的局部变量] =56=]。相反,它们被提升到一个也有 lambda 方法的新对象上,因此 "follows" lambda 无处不在。
我的印象是 C# 中的 lambda 表达式上下文包含对其中使用的父函数作用域变量的引用。考虑:
public class Test
{
private static System.Action<int> del;
public static void test(){
int i = 100500;
del = a => System.Console.WriteLine("param = {0}, i = {1}", a, i);
del(1);
i = 10;
del(1);
}
public static void Main()
{
test();
}
}
产出
param = 1, i = 100500
param = 1, i = 10
但是,如果这是真的,则以下内容将是非法的,因为 lambda 上下文将引用超出范围的局部变量:
public class Test
{
private static System.Action<int> del;
public static void test(){
int i = 100500;
del = a => System.Console.WriteLine("param = {0}, i = {1}", a, i);
}
public static void Main()
{
test();
del(1);
}
}
但是,这样编译运行并输出
param = 1, i = 100500
这意味着发生了一些奇怪的事情,或者上下文保留了局部变量的值,而不是对它们的引用。但如果这是真的,它就必须在每次 lambda 调用时更新它们,而且我不知道当原始变量超出范围时它会如何工作。此外,这似乎在处理大值类型时会产生开销。
我知道,例如,在 C++ 中,这是 UB(已在
问题是,这是 C# 中定义明确的行为吗? (我认为 C# 有一些 UB,或者至少有一些 IB,对吗?)
如果它定义明确,那么它实际上是如何工作的,为什么会起作用? (实现逻辑会很有趣)
与 C# 中的 lambda 语法相关的闭包概念是一个非常大的主题,对于我来说太大了,无法仅在这个答案中涵盖所有内容,但让我们至少尝试在这里回答具体问题。实际答案在底部,其余部分是理解答案所需的背景。
当编译器尝试使用匿名方法编译方法时,会在某种程度上重写该方法。
基本上就是生成一个新的class,把匿名方法提升到这个class里面。它被赋予了一个名称,尽管是一个内部名称,因此对于编译器来说,它有点从匿名方法转换为命名方法。但是,您不必知道或处理该名称。
此方法需要的任何变量,除匿名方法外声明的变量,但在与 used/declared 匿名方法相同的方法中,将被提升为好吧,然后这些变量的所有用法都被重写了。
现在这里涉及到几个方法,所以上面的文字变得难以阅读,所以我们来举个例子:
public Func<int, int> Test1()
{
int a = 42;
return value => a + value;
}
这个方法重写成这样:
public Func<int, int> Test1()
{
var dummy = new <>c__DisplayClass1();
dummy.a = 42;
return dummy.<Test1>b__0;
}
internal class <>c__DisplayClass1
{
public int a;
public int <Test1>b__0(int value)
{
return a + value;
}
}
编译器可以处理所有这些时髦的名称(是的,它们确实是用所有括号命名的)因为它指的是带有 id 和对象引用的东西,名称不再是编译器的问题。但是,您永远不能声明 class 或具有这些名称的方法,因此编译器不会生成恰好已经存在的 class 的风险。
这是一个 LINQPad 示例,它显示了我声明的 class,虽然名称中的括号较少,但看起来与编译器生成的相同:
void Main()
{
var f1 = Test1();
f1(10).Dump();
f1.Dump();
var f2 = Test2();
f2(10).Dump();
f2.Dump();
}
public Func<int, int> Test1()
{
int a = 42;
return value => a + value;
}
public Func<int, int> Test2()
{
var dummy = new __c__DisplayClass1();
dummy.a = 42;
return dummy._Test2_b__0;
}
public class __c__DisplayClass1
{
public int a;
public int _Test2_b__0(int value)
{
return a + value;
}
}
输出:
如果您查看上面的屏幕截图,您会注意到每个委托变量有两件事,一个 Method
属性 和一个 Target
属性.
调用该方法时,会使用引用 Target
对象的 this
引用来调用它。因此,委托捕获两件事:调用哪个方法,以及调用它的对象。
所以基本上,生成的对象 class 作为委托的一部分存在,因为它是方法的目标。
考虑到所有这些,让我们看看您的问题:
为什么 lambda 表达式在方法终止后保留封闭范围变量值?
A: 如果 lambda 存活下来,所有捕获的变量也会存活下来因为它们不再是它们在[中声明的方法的局部变量] =56=]。相反,它们被提升到一个也有 lambda 方法的新对象上,因此 "follows" lambda 无处不在。