【问题标题】:C# Action, Closure, and Garbage CollectionC# 动作、闭包和垃圾回收
【发布时间】:2011-11-17 16:28:33
【问题描述】:

我是否需要将 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();
    }
}

【问题讨论】:

    标签: c# garbage-collection closures


    【解决方案1】:

    我是否需要将 MyAction 设置为 null 以便垃圾收集能够继续处理这些类中的任何一个?

    没有。每当您“处置”托管资源时,很有可能您做错了。让垃圾收集器完成它的工作。

    当两个班级的寿命几乎相同时,我不太担心。当 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的生命周期是多少?局部变量的生命周期通常;通常局部变量的生存时间不超过方法激活。但是,在闭包中会任意延长局部变量的生命周期,直到闭包的生命周期。在这种特殊情况下,两个 lambdas 共享一个闭包,并且意味着局部变量昂贵Local 的生命周期至少与局部变量cheapLocal 的生命周期一样长,后者具有无限长的生命周期,因为对闭包的引用刚刚存储在一个永久存在的静态字段中。那个大字节数组可能永远被回收,即使似乎唯一引用它的东西很久以前就被收集了;闭包是一个隐藏的引用。

    很多语言都有这个问题; C#、VB、JScript 等都有词法闭包,它们没有按生命周期划分为分组变量。我们正在考虑更改 C# 和 VB 以对闭包进行更好的生命周期管理,但目前这是遥遥无期的工作,因此无法保证。

    【讨论】:

    • 如果您能详细说明“共享闭包”的含义,那将非常有用。从视觉上看,昂贵的Local 和cheapLocal 变量似乎完全不相关。昂贵的Local 似乎只与 shortLived 一样长,即方法 M 的范围。当这些不相关的变量成为同一个闭包的一部分时,规则是什么?
    • @Konstantin:很多人似乎认为局部变量的定义特性是它的生命周期很短。尽管许多局部变量确实的生命周期确实很短,而且确实有益,但生命周期短并不是定义 特色。使局部变量“局部”的不是它的生命周期,而是它的作用域。请记住,范围被定义为可以通过名称引用实体的代码区域。本地是本地的,因为它仅通过名称 locally 引用到方法(或 lambda)的主体。
    • @Konstantin:本地人的生命周期通常在控制权离开范围时结束;然而,封闭的当地人的寿命延长了。该规范没有给出关于该扩展必须有多 的任何规则,仅规定它必须有多长:它必须至少扩展到委托的生命周期。 生命周期可以延长很多,而在这种不幸的情况下,实际上是这样。 没有规则 用于两个代表共享闭包的情况;这些是编译器的实现细节,可能随时更改。
    • 好的,现在我尝试编译你的代码,我看到 C# 确实为两个闭包生成了一个类型,并且在“M”中只创建了一个这种类型的实例。便宜和昂贵的本地人都成为这种类型/实例的字段,巨大的数组变得不朽!这太出乎意料了。现在,我以完全不同的方式理解了您对编译器保证/规则的理论解释。我可以谦虚地提议将此作为您伟大博客的帖子主题吗?我想很多人会非常惊讶:)
    • @Konstantin:好主意。我在 2007 年写了那篇文章:blogs.msdn.com/b/ericlippert/archive/2007/06/06/…
    【解决方案2】:

    没有。无需将引用设置为 null。

    Dispose() 用于清理非托管资源。 Action&lt;string&gt; 是一个托管资源,当Class1 的实例在DoSomething() 末尾超出范围时,CLR 将对其进行正确处理。

    【讨论】:

    • 问题 - EventHandlers 是托管资源,但如果它们没有正确断开连接,它们似乎会阻止您的对象被垃圾收集......
    • @PaulMatovich - 是的,但这只是在某些情况下(一个类将引用另一个类的方法,这意味着事情可能不会超出预期的范围)。这不是这里的情况......您只是将 Action 设置为匿名函数。
    • 谢谢,这很有帮助。我看到的问题是 class1 持有对 class2 提供的代码的引用和 class2 的类级别变量,因此不是垃圾收集的候选对象。但我知道关闭提供了一些烟雾和镜像操作来断开该引用。
    【解决方案3】:

    不,你没有。垃圾收集器会处理它。

    【讨论】:

      【解决方案4】:

      一旦 DoSomething 返回,myCLass1 就是垃圾回收的候选对象。当一个类被垃圾收集时,如果没有未完成的引用,它的所有成员也会被收集。所以你不需要做你正在做的事情。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-09-19
        • 2015-06-10
        • 2011-11-29
        • 2021-12-20
        • 2015-11-13
        • 2010-11-01
        • 1970-01-01
        相关资源
        最近更新 更多