C# 中的 Action、闭包和垃圾回收

18

我需要将MyAction设置为null,以便垃圾回收可以处理这两个类吗?

当这两个类的寿命几乎相同时,我不那么担心。我的问题更适用于Class1的寿命比Class2长得多或Class2的寿命比Class1长得多的情况。

这里的代码被剥离了。假设Class1和Class2都包含其他成员和方法,这些可能会影响它们的寿命。

public class Class1 : IDisposable
{
    public Action<string> MyAction { get; set; }

    // Is this necessary?
    public void Dispose()
    {
        MyAction = null;
    }
}

public class Class2
{
    string _result = string.Empty;

    public void DoSomething()
    {
        Class1 myClass1 = new Class1();
        myClass1.MyAction = s => _result = s;
        myClass1.Dispose();
    }
}
4个回答

22

为了让垃圾回收能够处理这两个类中的任何一个,我是否需要将MyAction设置为null?

不需要。任何时候“Dispose”托管资源的时候,很可能你做错了。让垃圾回收器完成它的工作。

当两个类的寿命几乎相同时,我就不那么担心了。我的问题更适用于Class1的寿命比Class2长得多或者Class2的寿命比Class1长得多的情况。

这个问题没有任何意义;没有生命周期存储位置有生命周期。你担心哪些存储位置?你能澄清一下问题吗?

我注意到通过闭包来扩展闭合变量的寿命存在非常严重的问题;但是你的问题如此模糊,以至于很难理解你是否遇到了这种情况。让我来举个例子:

class Expensive
{
    public byte[] huge = MakeHugeByteArray();
}

class Cheap
{
    public int tiny;
}

class C
{
    public static Func<Cheap> longLived;
    public static void M()
    { 
        Expensive expensiveLocal = new Expensive();
        Cheap cheapLocal = new Cheap();
        Func<Expensive> shortLived = ()=>expensiveLocal ;
        C.longLived = ()=>cheapLocal;
    }
}
什么是局部变量 expensiveLocal 的生命周期? 通常情况下,局部变量的生命周期是短暂的;通常情况下,局部变量的寿命不会超过方法激活。然而,当一个局部变量在闭包中时,它的生命周期将任意延长到闭包的生命周期。在这种特定情况下,两个 lambda 表达式共享一个闭包,这意味着局部变量 expensiveLocal 的生命周期至少与局部变量 cheapLocal 的生命周期一样长,而后者的生命周期是无限期的,因为“对闭包的引用刚刚被存储在永久存在的静态字段中”。那个大字节数组可能永远不会被回收,即使似乎唯一引用它的东西早已被收集; 闭包是一个隐藏的引用。

许多语言都有这个问题;C#、VB、JScript 等所有语言都具有词法闭包,这些闭包没有通过生命周期来分组变量。我们正在考虑改变 C# 和 VB 的闭包生命周期管理方式,但这是遥远的未来工作,目前不能保证。


1
如果您能稍微详细说明一下“共享闭包”的含义,那将非常有用。从视觉上看,expensiveLocal和cheapLocal变量似乎完全没有关联。expensiveLocal似乎只存在于shortLived中,在方法M的作用域中存活。当这些不相关的变量成为同一闭包的一部分时,规则是什么? - Konstantin
@Konstantin:许多人似乎认为局部变量的定义特征是它具有短寿命。虽然许多局部变量确实具有短寿命,而且这确实是有益的,但短寿命并不是定义特征。使局部变量“局部”的不是其寿命,而是其作用域。请记住,作用域被定义为代码区域,在其中可以通过名称引用实体。局部变量之所以是局部变量,是因为它仅在方法(或lambda)的主体中本地地按名称引用。 - Eric Lippert
1
@Konstantin:当控制离开作用域时,局部变量的生命周期通常结束;然而,闭合的局部变量会延长其生命周期。规范没有给出任何关于这种延长必须有多的规则,只是说明了它必须至少延长到委托的生命周期。生命周期可以被延长得更久,而在这种不幸的情况下,事实上就是这样。对于两个委托何时共享一个闭包没有规则;这些是编译器的实现细节,随时可能发生变化。 - Eric Lippert
2
好的,现在我尝试编译你的代码,我发现C#确实为闭包生成了一个单一类型,并且在“M”中只创建了一个此类型的实例。无论是cheap还是expensive局部变量都成为了该类型/实例的字段,而巨大的数组则变得不朽!这太出乎意料了。现在我以完全不同的方式理解了你的理论解释关于编译器保证/规则。我可以谦虚地提议将此作为您博客的主题吗?我认为很多人会感到非常惊讶 :) - Konstantin
3
@Konstantin: 很棒的想法。我在2007年写了那篇文章:http://blogs.msdn.com/b/ericlippert/archive/2007/06/06/fyi-c-and-vb-closures-are-per-scope.aspx - Eric Lippert
显示剩余2条评论

4

不需要将引用设置为null。

Dispose()方法是用于清理非托管资源的。而Action<string>是托管资源,在DoSomething()结束时,CLR会适当的处理Class1实例。


问题 - 事件处理程序是受管理的资源,但如果它们没有正确断开连接,似乎会阻止您的对象被垃圾回收... - Paul Matovich
@PaulMatovich - 是的,但这只在某些情况下才会发生(一个类将持有另一个方法的引用,这意味着事物可能不会按预期的那样超出范围)。但这里并非如此...你只是将Action<string>设置为匿名函数。 - Justin Niessner
谢谢,这很有帮助。我看到的问题是class1持有了class2提供的代码和一个类级别变量的引用,因此不适合进行垃圾回收。但我理解闭包提供了一些干扰操作来断开该引用。 - Paul Matovich

0
不需要,垃圾回收器会处理它。

0
一旦 DoSomething 返回,myClass1 就成为垃圾回收的候选对象。当一个类被垃圾回收时,如果没有未解除的引用,它的所有成员也会被回收。因此,你不需要做你正在做的事情。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接