如何确保以下情况中没有内存泄漏

4
如果一个类有一个包含非托管资源的属性,如何确保在使用该类时不会出现内存泄漏。
Class A
{
         B {get; set;}
}

B 包含未经管理的资源。

4个回答

7

实现 IDisposable 接口并通过调用 Dispose() 方法清理您的非托管资源,最好将对 Dispose 的调用置于 finally 语句中,以便在发生异常时也能清理资源。

C# 中有一个 using 关键字,您可以使用它来确保调用 Dispose 方法,即使抛出异常也是如此。

编辑:根据 Ran 的答案添加了调用 GC.SuppressFinalize 和终结器实现。

class A : IDisposable
{
    private bool _disposed;

    ~A()
    {
        this.Dispose(false);
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // dispose managed resources
            }

            // clean up unmanaged resources

            _disposed = true;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var someInstance = new A())
        {
            // do some things with the class.
            // once the using block completes, Dispose
            // someInstance.Dispose() will automatically
            // be called
        }
    }
}

我认为在OP的情况下不需要使用Finalizer。请查看我的答案以获取更多细节... - SpeksETC

4

仅使用 IDisposable 可能不够,因为它依赖于用户记得调用 Dispose 或使用 using 等。

要完整解决问题,可以将 IDisposable 和终结器结合使用。像这样:

编辑:根据 SpeksETC 的评论,在 Dispose 方法中进行了一些更正。

class MyClass : IDisposable
{
    ~MyClass() 
    {
        Dispose(false);
    }

    public void Dispose()
    {
        GC.SupressFinalize();
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposing)
        {
            // clear unmanaged resources here (can only be called once)
            ...
        }

        // dispose called explicitly by the user, clean up managed resources here
        ...
    }
}

这确保了本地资源总是会被清除,即使用户忘记调用Dispose,同时仍允许用户提前清除资源。
Dispose实现中的if语句是必需的,因为如果这个类正在被终结,你可能无法调用Dispose来处理成员,因为它们可能已经被垃圾回收。

+1:很好,还将终结器联系起来了。由于某种原因,我曾认为微软开始不鼓励这种做法。 - Phil Hunt
Finalizers确实会影响性能,但我认为只有在实际调用finalizers时才会产生影响(因为GC处理finalizable队列的开销等)。所以,在您自己的应用程序中,如果可以的话,应始终Dispose对象。但是,在编写库时,您可能希望保护可能会忽略调用Dispose的用户。对于他们来说,性能损失比内存泄漏好得多... - Ran
"你应该始终处理对象的释放。我完全同意,甚至认为调用Finalizer是一种错误。为了防止这些错误,我建议在Finalizer中添加Debug.Assert以便轻松捕获它们..." - SpeksETC
既然b是托管的,而且在A被终结时可能已经被GC回收了,那么b.Dispose()不应该放在你提到的if条件语句中吗?我认为在这种情况下不需要使用终结器,请参考我的答案。 - SpeksETC
@Ran,关于Debug.Assert,我同意这取决于您的要求,但即使您编写库,您仍然可以向用户部署发布版本,并且他们不会受到Debug.Assert的影响。 - SpeksETC
显示剩余2条评论

1

我认为需要指出的是,B是一个托管资源,仅包含非托管资源。

因此,在其Dispose()方法中,A应该实现IDisposable并处理B,但不需要终结器,因为它没有任何需要清理的非托管资源 - 终结器需要在B本身中实现。即使您实现了终结器,它也会调用类似于以下内容的Dispose:

protected virtual void Dispose(bool disposing) 
    { 
        if (disposing) 
        { 
            // dispose called explicitly by the user, clean up managed resources here 
            b.Dispose() 
        } 

        // no unmanaged resources to clean up so do nothing which makes Finalizer unneccesary
        ... 
    } 

没错。拥有非托管资源的类应该清理它们。在我的例子中,MyClass类实际上是B,而A只需要处理B,因为A并没有真正拥有任何非托管资源。 - Ran

0

SpeksEtc的观点是正确的,如果B包含非托管资源,则B应确保没有泄漏,而不是A。

但是什么是非托管资源?SafeHandle类是否有帮助?然后B将包含一个SafeHandle,其中包含非托管资源,您就不必担心它了。


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