只要类层次结构中的所有类只使用托管资源,那么拥有虚拟Dispose()方法是否可以?

6

我知道实现dispose pattern的通用指南警告不要实现虚拟Dispose()。然而,我们大多数情况下只在类内部处理托管资源,因此完整的dispose模式似乎有些过度 - 也就是说,我们不需要一个finalizer。在这种情况下,在基类中有一个虚拟的Dispose()是否可以呢?

考虑这个简单的例子:

abstract class Base : IDisposable
{
    private bool disposed = false;
    public SecureString Password { get; set; } // SecureString implements IDisposable.

    public virtual void Dispose()
    {
        if (disposed)
            return;

        if (Password != null)       
            Password.Dispose();     

        disposed = true;
    }
}

class Sub : Base
{
    private bool disposed = false;
    public NetworkStream NStream { get; set; } // NetworkStream implements IDisposable.

    public override void Dispose()
    {   
        if (disposed)
            return;

        if (NStream != null)
            NStream.Dispose();

        disposed = true;
        base.Dispose();
    }
}

我认为这种方法比完整的清理模式更容易理解。我明白如果我们将非托管资源引入Base类中,那么这个例子可能会变得棘手。但是假设这不会发生,那么上面的例子是否有效/安全/健壮,或者它会带来任何问题?即使没有使用非托管资源,我是否应该坚持使用标准的完整清理模式?还是上述方法在这种情况下确实是完全可以的 - 在我的经验中,这种情况比处理非托管资源要常见得多?

1
“Full blown” 的意思是指一个额外的受保护的虚拟方法,它接受一个布尔值吗? - Yuval Itzchakov
为什么你的子类也有IDisposable接口?可能不需要,因为基类已经实现了它。 - sumeet kumar
2
“Dispose模式是一种可憎的东西。在我的整个人生中,我只写过3个finalizer。将非托管句柄推入句柄类中。这样,您几乎不需要在自己的类中使用finalizer。从.NET 2.0开始,.NET Framework使用SafeHandle类识别该模式。” - usr
@YuvalItzchakov:是的,就是我所说的“完全成熟”。 - w128
@sumeetkumar 因为现场编写示例而错过了,谢谢。 - w128
显示剩余5条评论
2个回答

5

在我不涉及非托管资源的情况下,我已经放弃了完整的IDisposable模式。

如果你可以确保派生类不会引入非托管资源,我认为你没有理由不放弃该模式并将Dispose设置为虚拟的(但这里存在一个问题,因为你无法强制执行这一点。你无法控制派生类将添加哪些字段,因此使用虚拟的Dispose并不是完全安全的。但即使你使用完整的模式,派生类也可能出现错误,所以始终需要信任子类型遵守某些规则。)

首先,在只处理托管对象的情况下,具有终结器是无意义的:如果从终结器中调用Dispose(根据完整的模式,调用Dispose(disposing: false)),我们可能无法安全地访问/取消引用其他引用类型的托管对象,因为它们可能已经不存在了。只有从正在终结的对象可达的值类型对象可以被安全访问。

如果终结器是无意义的,那么使用disposing标志来区分Dispose是显式调用还是由终结器调用也是无意义的。

如果没有终结器,那么做GC.SuppressFinalize也是不必要的。

在仅涉及托管对象的情况下,我甚至不确定Dispose是否仍然必须不抛出任何异常。据我所知,这个规则主要是为了保护终结器线程。(请参见@usr的下面的评论。)

正如你所看到的,一旦终结器消失,许多经典的可处置模式就不再必要了。


1
我甚至不确定在仅管理的情况下Dispose是否仍然必须不抛出任何异常。这很重要,这样您就可以只使用“using”而不必担心异常被新的Disposed-originated异常替换。 - usr
@usr:同意,我一时忘了这个。我已经相应地编辑了我的答案。感谢您的纠正! - stakx - no longer contributing
谢谢,这正是我的想法。我担心我开始出现认知下降的迹象,因为无论我在网上看到什么,他们都推广完整的dispose模式,尽管实际上很少需要(即使是SecureString实现也不使用它,因为它依赖于安全句柄)。 - w128

2
我理解如果在Base类中引入一个未管理的资源,这个例子将会有问题。但是假设这不会发生,那么上面的例子是否有效/安全/健壮?它是否存在问题?即使没有使用未托管的资源,我是否应该坚持使用标准的完整Dispose模式?
尽管您的基类仅使用托管资源(并且将来也不会更改),但无法保证派生类不会使用未托管的资源。因此,在基类中实现Basic Dispose Pattern(具有虚拟Dispose(bool)但没有终结器的类)即使你总是以true调用Dispose(bool)方法也要考虑。
当某天从您的Base派生出一个使用未托管资源的新子类时,其终结器可能希望使用false调用Dispose(bool)。当然,您可以在派生类中引入一个新的Dispose(bool):
public class SubUnmanaged : Base
{
    IntPtr someNativeHandle;
    IDisposable someManagedResource;

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

    ~SubUnmanaged();
    {
        base.Dispose();
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (someNativeHandle != IntPtr.Zero)
            Free(someNativeHandle);

        if (disposing)
           someManagedResource.Dispose();
    }
}

但我认为这有点不好。通过封闭原始的虚拟Dispose()方法,您可以防止进一步的继承者因多个虚拟Dispose方法而感到困惑,并且您可以从终结器和封闭的Dispose()中都调用通用的base.Dispose()。您必须在每个使用非托管资源并且派生自Base或完全托管的子类中执行相同的解决方法。但这已经远离了模式,总是让事情变得更加困难。


派生类不应该有终结器。它应该将非托管资源移动到一个专用类中,该类不参与继承。我认为这里展示的模式永远不应该被使用。它可以,但不是最好的。 - usr
@usr:我基本上同意;但是,如果基类不使用非托管资源,则不应将其设置为可终结。因此,最好在派生类中引入终结器(当然不是通过我在答案中发布的恶心解决方法),并从那里简单地调用Dispose(false)。另请参见:https://msdn.microsoft.com/en-us/library/vstudio/b1yfkh5e%28v=vs.110%29.aspx#finalizable_types - György Kőszeg
是的,你在这里使用的模式比其他人使用的“丑陋物”要好。这是真的。 - usr

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