如果局部变量在超出作用域后被匿名函数重新引用,会发生什么?

本文关键字:引用 什么 新引用 局部变量 作用域 函数 如果 | 更新日期: 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实例)在callbackfoobarbaz的状态是什么?

如果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",但每个回调都指向堆内存中的相同对象。