如果局部变量在超出作用域后被匿名函数重新引用,会发生什么?
本文关键字:引用 什么 新引用 局部变量 作用域 函数 如果 | 更新日期: 2023-09-27 18:07:15
考虑这个人为的例子:
public static class Test {
private static List<Action> actions = new List<Action>();
private static Int32 _foo = 123;
public static void Foo() {
Int32 foo = _foo += 123;
Object bar = new Object();
IDisposable baz = GetExpensiveObject();
Action callback = new Action(delegate() {
DoSomething( foo, bar, baz );
baz.Dispose();
});
foo = 456;
bar = new Object();
actions.Add( callback );
}
public static void Main() {
Foo();
Foo();
foreach(Action a in actions) a();
}
}
看Main
,假设Foo
被调用两次,actions
的内容(现在已经执行了2个Action
实例)在callback
中foo
、bar
、baz
的状态是什么?
如果callback
从未被调用,baz
是否会被处理(因为callback
中包含的引用包含在actions
中?),actions.Clear()
是什么,baz
会被处理吗?
(我没有在计算机上与编译器或IDE为我测试)
匿名方法将被编译器重写,以保存对堆上与本地作用域相同的内存区域的引用。垃圾收集器将发现此引用是活动的,并且在匿名方法也被收集之前不会对目标进行垃圾收集。
然而…如果不是在堆上分配,而是在可能被新方法调用覆盖的堆栈上分配,该怎么办?;)
private static void Main(String[] args) {
var rng = CreateRNG();
Console.WriteLine(rng());
Console.WriteLine(rng());
Console.ReadLine();
}
private static unsafe Func<Int32> CreateRNG() {
var v = stackalloc Int32[1];
v[0] = 4;
return () => v[0];
}
此代码第一次调用打印4,第二次调用打印半随机数。
实际代码,使用Reflector提取并手工清理,并对方法进行了重命名以使其能够编译(编译器在自动生成的方法名称中使用特殊字符,如<>):
private static unsafe Func<Int32> CreateRNG() {
Int32* numPtr = stackalloc Int32[1];
var class2 = new __c__DisplayClass1();
class2.v = numPtr;
class2.v[0] = 4;
return new Func<Int32>(class2._CreateRNG_b__0);
}
[CompilerGenerated]
public sealed class __c__DisplayClass1 {
public unsafe Int32* v;
public unsafe Int32 _CreateRNG_b__0() {
return v[0];
}
}
这表明编译器将匿名方法重写为一个新函数,在这种情况下是在一个新类中保存任何引用的局部值。如果不需要保留局部引用,则不需要该类。
我也可以猜测第一次调用是有效的,因为我们调用了返回的Func<Int32>
,它很容易读取值。方法体非常小,它可能可以内联。值4被传递给Console.WriteLine
,该方法调用可能会覆盖堆栈(或者Console.WriteLine
反过来调用方法),从而改变指针指向的值。
请注意,如果局部变量在匿名方法中使用,那么局部变量的生存期将会随着匿名方法的生存期而延长。这并不意味着在创建匿名方法时复制了变量的值。因此,每次调用"DoSomething"时都将使用"456"和第二个创建的对象。
你可以检查它,如果你创建一个新的WinForms-Project,在窗体上放置一个新的按钮,并添加以下代码:
private void Form1_Load(object sender, EventArgs e)
{
int i = 123;
this.button1.Click += (Lsender, Le) => { MessageBox.Show(i.ToString()); };
i = 456;
}
请注意这里的引用类型,因为如果你要写
{
private static Foo(object value)
{
object bar = value;
//...
}
private static void Main()
{
object obj = new object();
Foo(obj);
Foo(obj);
//...
}
}
在这种情况下,每个回调都有自己的变量"bar",但每个回调都指向堆内存中的相同对象。