2

考虑这个人为的例子:

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变量的状态是什么?foobarbazcallback

如果callback从不被调用,将baz 永远被处置(因为包含在callback内的引用actions?),那么actions.Clear()被调用的东西会baz被处置吗?

(我不在一台装有编译器或 IDE 的计算机上供我测试)

4

2 回答 2

1

编译器将重写匿名方法,以保存对堆上与本地范围相同的内存区域的引用。垃圾收集器将发现此引用处于活动状态,并且在匿名方法也被收集之前不会对目标进行垃圾收集。

但是...如果您不是在堆上分配,而是在可能被新方法调用覆盖的堆栈上分配怎么办?;)

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

于 2013-04-03T09:51:49.537 回答
1

好吧,提醒一下,如果在匿名方法中使用局部变量的生命周期,它们的生命周期将会延长。这并不意味着在创建匿名方法时会复制变量的值。所以每次都会用“456”和第二个创建的对象调用“DoSomething”。

如果您创建一个新的 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”,但它们中的每一个都引用堆内存中的同一个对象。

于 2013-04-03T09:29:44.630 回答