【问题标题】:C#: how to avoid this potential memory leakC#:如何避免这种潜在的内存泄漏
【发布时间】:2017-11-21 13:22:01
【问题描述】:

假设我有一个如下所示的 C# 类:

public class MyClass {
    public SomeObject TheObject { get; }

    public MyClass() {
        TheObject = new SomeObject();
        TheObject.MyEvent += MyEventHandler;
    }

    private void MyEventHandler() {
        // some code
    }
}

该类创建一个名为 TheObject 的 SomeObject 类型的内部对象,并为该对象上的事件添加一个事件处理程序。

由于 TheObject 是一个公共属性,这意味着任何其他代码都可以维护指向该对象的指针;反过来,这将使 MyClass 类型的对象保持活动状态,因为 TheObject 以事件处理程序的形式具有指向 MyClass 的指针。

所以,我认为保护此代码免受此事件影响的唯一方法是向 MyClass 添加终结器:

public ~MyClass() {
    TheObject?.MyEvent -= MyEventHandler;
}

这太糟糕了,因为终结器会将 MyClass 类型的对象提升到下一代 GC,但我是否正确地说这是避免这种潜在内存泄漏的唯一方法?

【问题讨论】:

  • 在 MyClass 中实现 IDisposable?
  • @Jacek 我会让 SomeObject 实现 IDisposable ,因为其他类也可以包含对它的引用,因此当不再需要它时,它可以被 GC 处理和收集
  • 是的,所以你可以在 SomeObject 中实现 IDisposable 并利用这个答案stackoverflow.com/a/447960/2811109
  • 我在这个问题中看到了“C++ 心态”。它是 references 而不是 C# 中的 pointers,虽然它们有相似之处,但它们是不同的概念。当一个对象由于垃圾回收而被移动时,内部指针会被调整,而引用保持不变。此外,C# 终结器与 C++ 析构函数不同。终结器用于释放 unmanaged 资源。除非您直接与底层平台/操作系统交互,否则这种情况很少见。内存由垃圾收集器处理,但要实现 C++ 析构函数可以执行的任何其他清理,您可以使用IDisposable

标签: c# memory-leaks garbage-collection


【解决方案1】:

您的解决方案实际上不会解决问题,因为在可以收集对象之前不会调用终结器本身,并且您正确识别 TheObject 将通过事件处理程序使对象保持活动状态。

有两个潜在的修复方法:

  1. 使MyClass 实现IDisposable 并在Dispose 方法中取消注册事件处理程序。 C# 有 using 语法来帮助类的使用
  2. 对事件使用弱引用,不要依赖默认的事件语法。

一个简单的实现是:

public interface ISubscription
{
    bool IsAlive { get; }
    void Fire();
}

public class Subscrition<T> : ISubscription
    where T: class
{
    public Subscrition(T target, Action<T> fire)
    {
        this.Target = new WeakReference<T>(target);
        this.FireHandler = fire;
    }
    public WeakReference<T> Target { get; }
    public Action<T> FireHandler { get; }

    public bool IsAlive => this.Target.TryGetTarget(out var t);

    public void Fire()
    {
        if (this.Target.TryGetTarget(out var target))
        {
            this.FireHandler(target);
        }
    }
}

public class WeakEvent
{
    List<ISubscription> weakHandlers = new List<ISubscription>();

    public void Register<T>(T target, Action<T> fire)
        where T:class
    {
        this.Prune();
        this.weakHandlers.Add(new Subscrition<T>(target, fire));
    }
    public void Unregister(ISubscription subscription)
    {
        this.Prune();
        this.weakHandlers.Remove(subscription);
    }
    // Prune any dead handlers.
    public void Prune()
    {
        this.weakHandlers.RemoveAll(_ => !_.IsAlive);
    }
    public void Fire()
    {
        this.Prune(); 
        this.weakHandlers.ForEach(_ => _.Fire());

    }
}

用法:

public class SomeObject
{
    public WeakEvent WeakMyEvent = new WeakEvent();
}

public class MyClass
{
    public SomeObject TheObject { get; }

    public MyClass()
    {
        TheObject = new SomeObject();
        TheObject.WeakMyEvent.Register(this, t => t.MyEventHandler());
    }
    private void MyEventHandler()
    {
        // some code
    }
}

您还可以查看this article 以获得更复杂的实现

【讨论】:

  • 你说得对,我的解决方案实际上并没有达到预期的效果......感谢您的超级详细回复!
【解决方案2】:

你必须实现 IDISPOSABLE ,使用 -= 来避免内存泄漏是没有用的,因为你的对象总是活着的。 如果您的垃圾收集器无法释放您的代表。 您有更多关于同一主题的信息: Why and How to avoid Event Handler memory leaks?

【讨论】:

    【解决方案3】:

    如果您的类实现了 IDisposable 接口,您可以取消订阅 Dispose 方法上的事件,然后您必须调用实例上的 Dispose 方法。这种方法的问题在于,大多数时候我们不知道何时取消订阅(或何时调用 Dispose),如果是这种情况,我建议您使用 Weak Event Pattern .

    弱事件模式使用从源对象到侦听器的弱引用,因此这种关系不会阻止 GC 收集这些对象。 WPF 中的大多数控件已经实现了弱事件模式。使用它的一种方法是通过通用的 WeakEventManager 类。

    查看this article,查看一些使用弱事件模式的代码示例。

    【讨论】:

      猜你喜欢
      • 2016-08-14
      • 2011-10-09
      • 1970-01-01
      • 2020-04-08
      • 1970-01-01
      • 2012-02-03
      • 1970-01-01
      相关资源
      最近更新 更多