Why should Dispose() be non-virtual?

2019-02-05 13:49发布

问题:

I'm new to C#, so apologies if this is an obvious question.

In the MSDN Dispose example, the Dispose method they define is non-virtual. Why is that? It seems odd to me - I'd expect that a child class of an IDisposable that had its own non-managed resources would just override Dispose and call base.Dispose() at the bottom of their own method.

Thanks!

回答1:

Typical usage is that Dispose() is overloaded, with a public, non-virtual Dispose() method, and a virtual, protected Dispose(bool). The public Dispose() method calls Dispose(true), and subclasses can use this protected virtual method to free up their own resorces, and call base.Dispose(true) for parent classes.

If the class owning the public Dispose() method also implements a finalizer, then the finalizer calls Dispose(false), indicating that the protected Dispose(bool) method was called during garbage collection.

If there is a finalizer, then the public Dispose() method is also responsible for calling GC.SuppressFinalize() to make sure that the finalizer is no longer active, and will never be called. This allows the garbage collector to treat the class normally. Classes with active finalizers generally get collected only as a last resort, after gen0, gen1, and gen2 cleanup.



回答2:

This is certainly not an obvious one. This pattern was especially choosen because it works well in the following scenario's:

  • Classes that don't have a finalizer.
  • Classes that do have a finalizer.
  • Classes that can be inheritted from.

While a virtual Dispose() method will work in the scenario where classes don't need finalization, it doesn't work well in the scenario were you do need finalization, because those types often need two types of clean-up. Namely: managed cleanup and unmanaged cleanup. For this reason the Dispose(bool) method was introduced in the pattern. It prevents duplication of cleanup code (this point is missing from the other answers), because the Dispose() method will normally cleanup both managed and unmanaged resources, while the finalizer can only cleanup unmanaged resources.



回答3:

Although methods in an interface are not "virtual" in the usual sense, they can nevertheless still be implemented in classes that inherit them. This is apparently a convenience built into the C# language, allowing the creation of interface methods without requiring the virtual keyword, and implementing methods without requiring the override keyword.

Consequently, although the IDisposable interface contains a Dispose() method, it does not have the virtual keyword in front of it, nor do you have to use the override keyword in the inheriting class to implement it.

The usual Dispose pattern is to implement Dispose in your own class, and then call Dispose in the base class so that it can release the resources it owns, and so on.

A type's Dispose method should release all the resources that it owns. It should also release all resources owned by its base types by calling its parent type's Dispose method. The parent type's Dispose method should release all resources that it owns and in turn call its parent type's Dispose method, propagating this pattern through the hierarchy of base types.

http://msdn.microsoft.com/en-us/library/fs2xkftw.aspx



回答4:

The Dispose method should not be virtual because it's not an extension point for the pattern to implement disposable. That means that the base disposable class in a hierarchy will create the top-level policy (the algorithm) for dispose and will delegate the details to the other method (Dispose(bool)). This top-level policy is stable and should not be overridden by child classes. If you allow child classes to override it, they might not call all the necessary pieces of the algorithm, which might leave the object in an inconsistent state.

This is akin to the template method pattern, in which a high-level method implements an algorithm skeleton and delegates the details to other overridable methods.

As a side note, I prefer another high-level policy for this particular pattern (which still uses a non-virtual Dispose).



回答5:

Calls through an interface are always virtual, regardless of whether a "normal" call would be direct or virtual. If the method that actually does the work of disposing isn't virtual except when called via the interface, then any time the class wants to dispose itself it will have to make sure to cast its self-reference to iDisposable and call that.

In the template code, the non-virtual Dispose function is expected to always be the same in the parent and the child [simply calling Dispose(True)], so there's never any need to override it. All the work is done in the virtual Dispose(Boolean).

Frankly, I think using the Dispose pattern is a little bit silly in cases where there's no reason to expect descendant classes to directly hold unmanaged resources. In the early days of .net it was often necessary for classes to directly hold unmanaged resources, but today in most situations I see zero loss from simply implementing Dispose() directly. If a future descendant class needs to use unmanaged resources, it can and typically should wrap those resources in their own Finalizable objects.

On the other hand, for certain kinds of method there can be advantages to having a non-virtual base class method whose job is to chain to a protected virtual method, and having the virtual method be called Dispose(bool) is really no worse than VirtDispose() even if the supplied argument is rather useless. In some situations, for example, it may be necessary for all operations on an object to be guarded by a lock which is owned by the base-class object. Having the non-virtual base-class Dispose acquire the lock before calling the virtual method will free all the base classes from having to worry about the lock themselves.



回答6:

The reason the sample's Dispose() method is non-virtual is because they take over the entire process in that example, and leave subclasses with the virtual Dispose(bool disposing) method to override. You'll notice that in the example, it stores a boolean field to ensure that the Dispose logic does not get invoked twice (potentially once from IDisposable, and once from the destructor). Subclasses who override the provided virtual method do not have to worry about this nuance. This is why the main Dispose method in the example is non-virtual.



回答7:

I've got a quite detailed explanation of the dispose pattern here. Essentially, you provide a protected method to override that is more robust for unmanaged resources instead.



回答8:

If the base class has resources that need to be cleaned up at Dispose() time, then having a virtual Dispose method that's overridden by an inheriting class prevents those resources from being released unless the inheriting class specifically calls the base's Dispose method. A better way would implement it would be to have each derived class implement IDisposable.