The guidelines should read:
When working with unmanaged resources:
use an existing SafeHandle if possible
If not possible: subclass SafeHandle to create one that meets your needs. This class should not do anything more than managing unmanaged resources. It should be sealed.
If that's not possible: create your own class which implements IDisposable + Finalizer
3.1 The class should be sealed
3.2 If sealing the class is not possible: add protected void Dispose(bool disposeManagedResources)
99% of .net developers should never ever actually implement the "full blown" dispose pattern.
CA1063 should be updated accordingly.
⚠Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.
@Maoni0 can you help with this feedback about GC?
sounds reasonable to me. but this is more a BCL issue. @stephentoub do we already have some sort of guidelines wrt using SafeHandle somewhere?
I don't believe the Framework Design Guidelines that @KrzysztofCwalina previously published covered SafeHandle, other than in comments included in the annotated version. Guidance like the above is reasonable. In particular, I view the discussion related specifically to finalizers: you should really think hard before adding a finalizer to manage unmanaged resources... if you find yourself thinking you need to, you most likely should just use a SafeHandle.
@bartonjs and I talked about starting an effort that would serve as an "errata"/update to the FDGs. @bartonjs, maybe this is a good candidate for the first update topic?
FDG2e has a pretty solid opinion here:
9.4.2 (Finalizable Types)
Tagging @sharwell @gpetrou @duracellko as we hit this issue in https://github.com/dotnet/roslyn-analyzers/issues/1950
CA1063 FxCop analyzer tried to aggressively flag all derived types overriding the destructor/finalizer and suggested to remove them, which caused CA1063 to fire on the documentation snippet cited here.
As we concluded in https://github.com/dotnet/roslyn-analyzers/issues/1950 and seems to match with the consensus on this issue, we should strongly push customers towards using SafeHandles instead of providing their own finalizer override for cleanup. We have updated the analyzer to be bit more conservative and only flag finalizer override on a Derived type _if_ there is a finalizer override in at least one of its base type in its inheritance chain, as this is almost certainly likely to cause duplicate Dispose invocations.
Can we please update this documentation to not suggest people towards writing finalizers?
I disagree with item 3 in the original list. I am not aware of a circumstance where a user needs to write a finalizer. If a user reaches a step where they are writing a finalizer, they have already made a mistake in an earlier step.
New guidance for IDisposable should be written with the following rules:
IDisposable, identifiable by a method with the signature Dispose(bool) should only be followed if and only if the following is true:A type implementing IDisposable should have the following method:
public virtual void Dispose()
{
}
The Dispose() implementation SHOULD NOT be sealed unless the containing type is also sealed
Dispose() implementation SHOULD NOT call GC.SuppressFinalize(this) (the method is always unnecessary as the type does not have a finalizer)object.FinalizeI disagree with item 3 in the original list. I am not aware of a circumstance where a user needs to write a finalizer. If a user reaches a step where they are writing a finalizer, they have already made a mistake in an earlier step.
There are certainly times when custom finalizers are valuable; it's just the 1% case.
@sharwell Most of that guidance is in direct disagreement with the established rules (and guidance published in Framework Design Guidelines).
If nothing else, doing a 180 on the guidance would make for a very hard universe to understand, so I don't see us rewriting them as you suggested.
@sharwell @stephentoub
Quoting @stephentoub
There are certainly times when custom finalizers are valuable; it's just the 1% case.
Which would be covered by item 2 on the original list (subclassing SafeHandle) (https://github.com/dotnet/docs/issues/8463#issue-371470607):
- use an existing SafeHandle if possible
- If not possible: subclass SafeHandle to create one that meets your needs. This class should not do anything more than managing unmanaged resources. It should be sealed.
Now, I personally can't fathom a case where it be necessary and sensible to have a finalizer outside of a SafeHandle. But maybe that's my lack of knowledge/imagination, but still, we'd be in agreement it's a very rare case, right? So:
@bartonjs
@sharwell Most of that guidance is in direct disagreement with the established rules (and guidance published in Framework Design Guidelines).
If nothing else, doing a 180 on the guidance would make for a very hard universe to understand, so I don't see us rewriting them as you suggested.
Could you elaborate on that? Personally I find it a very hard universe to understand where documentation is illogical and pointing me in the wrong direction (and, as a result, over the years I've had repeated discussions about this specific topic with many a co worker). Are you sure the universe is easier to understand when documentation is "wrong" as when it's "right" but it's more complicated?
I believe, though, that it's necessary to make the change and it's reasoning "public", so that this "universe" is not harder to understand than necessary.
Focus should be given to the new guidance but the old guidance needs to be mentioned.
(put this in a box...)
The recommended implementation of IDisposable has been significantly changed in 2019. It is now recommended to handle unmanaged resource cleanup solely in SafeHandles. See [Change History].
(doesn't need a header...)
... written as if the "legacy" documentation didn't exist.
The legacy guidance (see [Change History]) for IDisposable, identifiable by a method with the signature Dispose(bool) should only be followed if and only if the following is true:
The type is derived from a type already following the legacy guidance
... Insert Legacy Guidance here...
...reason why documentation has changed and how it was changed. Not a full blown git revision list but really just why and how the guideline was changed. Add a link to [How to handle Legacy code]
Most of that guidance is in direct disagreement with the established rules (and guidance published in Framework Design Guidelines).
@bartonjs I agree, and I've been an ardent supporter of the established rules for more than a decade. However, there are two issues that came to light over the past two years which eventually resulted in me changing my mind:
SafeHandle is implemented). It has been argued that additional exceptions to this rule exist, but they are so uncommon that the guidance does not need to account for their existence. I can think of only one case over the past decade where it was used for a purpose, though that case occurred many years ago and I'm not sure it would stand up to scrutiny today. The updated rules would be a dramatic simplification which focuses on the only recommended implementation strategy (managed resources), eliminating consideration for a scenario which users should not encounter (finalizers). As a special case, they allow for the ongoing existence of the legacy pattern without introducing API incompatibilities.
I write finalizers all the time, they're useful in tracing object lifetimes. They don't release, but they do get written...
The current rule bullets from FDG:
9.4 (Dispose Pattern)
9.4.1 (Basic Dispose Pattern)
protected virtual void Dispose(bool disposing) method to centralize all logic related to releasing unmanaged resources.IDisposable interface by simply calling Dispose(true) followed by GC.SuppressFinalize(this).Dispose method virtual.Dispose].Dispose(bool) method to be called more than once. The method might choose to do nothing after the first call.Dispose(bool)...ObjectDisposedException from any member that cannot be used after the object has been disposed of.Close(), in addition to the Dispose(), if close is standard terminology in the area.9.4.2 (Finalizable Types)
SafeHandle..."SafeHandle". It says that your Finalize method should simply be Dispose(false).Finalize method protected.Yeah, it's a lot of bullets, but it makes sense with the step by step explanations. A simpler form would be:
On a type that directly implements IDIsposable:
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }protected virtual void Dispose(bool disposing) { ... }On a type that extends an IDisposable type, when needed:
*
```C#
protected override void Dispose(bool disposing)
{
if (disposing)
{
_disposableField?.Dispose();
// other "live graph" operations
}
// If there are any struct fields representing resources that need to be released, do so here.
// Make sure to clean up state so you don't try to release it twice.
//
// If you followed the rest of the guidelines you never have code here,
// because it was a SafeHandle released in the above if.
base.Dispose(disposing);
}
```
Don't produce new finalizable types
SafeHandle _faciendi_.Dispose(false)I'm not saying that the current docs are great (or necessarily even say what we want them to say); just that I don't think that changing the guidelines is justified:
Also, guidance would still have to be written for the 1% case of a new public finalizable type. It would have to explain how Finalize and Dispose interact, and how to properly handle that for unsealed finalizable types.
If we were making the framework anew, I'd probably prefer the simplified alternative.
My main position, I guess, is that @BrunoJuchli's original description is the right thing to do... to update the documentation to say:
Which would be covered by item 2 on the original list (subclassing SafeHandle)
No, they all won't.
Now, I personally can't fathom a case where it be necessary and sensible to have a finalizer outside of a SafeHandle.
Here are just a few examples off the top of my head (I'm sure there are more):
await new TaskCompletionSource().Task;), your async method is never going to complete, and things that await it will never complete, and so on. So, we fire an EventSource / ETW event if we detect this happening, which we can tell by seeing whether the boxed async method builder has completed by the time it's collected. How do we do that? A finalizer. Again, SafeHandle doesn't make sense here, and we couldn't derive from it even if we wanted to.ThreadLocal<T> (https://github.com/dotnet/coreclr/blob/eff427c02a89e135c38a96032feb3c9a6a13cf5b/src/System.Private.CoreLib/shared/System/Threading/ThreadLocal.cs#L141). When all threads keeping a particular ThreadLocal<T> alive go away, its associated slot should be reclaimed and used for another thread local, or else we leak. How can we know that? A finalizer. Again, SafeHandle isn't relevant.I write finalizers all the time, they're useful in tracing object lifetimes
@bartonjs I don't know enough about your application to know why you are tracing lifetimes in this way. However, when (rarely) faced with similar situations in the past, I've always found alternative solutions viable (typically event tracing / profiling, but could also be dependent objects).
Here are just a few examples off the top of my head
@stephentoub Your examples are fascinating, but to me they highlight how far the core framework goes out of the way to ensure users do not need to write their own finalizers. I don't see dotnet/coreclr or dotnet/corefx as the primary audience for the updated guidance.
TaskExceptionHolder, but derived from CriticalHandle). In a case where it was observable (and mattered), the client would likely be interested in finding a way to avoid the finalizer cost altogether, instead focusing on ensuring all relevant objects are disposed at the correct times.TimerHolder could arguably be derived from CriticalHandle. Maybe there's a reason the core framework cannot do this, but for users facing a similar situation it would be a viable option.Dispose or IAsyncLifetime.DisposeAsync that all handles have been closed. Unlike the finalization approach, this would result in deterministic validation of cleanup during the test lifetime, and failures would appear as test failures for the test that failed to clean up. This approach would not require (or benefit from) the use of a finalizer.TimerHolder could arguably be derived from CriticalHandle
It doesn't have an IntPtr handle. So this is just another way of writing a finalizer (a harder way), because you have to override everything to do with that handle, instead of just writing a finalizer. Why?
This code could likely be improved by tracking open handles in the test class
That then requires way more boilerplate, doesn't work well with nesting (which these do), etc. Your suggesting a workaround for something that doesn't need a workaround.
HttpConnection
You're very focused on deriving from CriticalHandle. It represents a handle! That's not relevant here at all. There is no IntPtr handle. Any such handle we create would be completely fabricated, and we'd doing it purely to use a different syntax for authoring a finalizer.
but to me they highlight how far the core framework goes out of the way to ensure users do not need to write their own finalizers
I disagree. These aren't about ensuring users don't need to write finalizers, these are because a class was needed that needed such functionality.
My claim is the current guidance for disposable objects is a large cognitive overhead, and that the cognitive overhead is substantially reduced by disallowing a specific construct (finalizers). I further claim that this is reasonable because both of the following are true:
CriticalHandle cannot be leveraged to avoid the use of a custom finalizer are exceedingly rare.Simplification of this pattern would allow average developers to be more successful (make fewer mistakes) by avoiding a known problem area.
We should not be "disallowing" finalizers; they are a valuable and viable tool. And I agree with @bartonjs on the guidance:
I don't believe the need for custom finalizers is "exceedingly" rare. A large majority of the cases are nicely handled by SafeHandle, and for everything else, there are finalizers. Saying that CriticalHandle should be used instead in such cases (where there is no handle) makes zero sense to me: you're still writing a finalizer, you're just doing it in a way that's more convoluted and more expensive.
@stephentoub
I don't believe the need for custom finalizers is "exceedingly" rare.
I tried searching github for C# code containing ": IDisposable" and "\~". Unfortunately the search ignores both ":" and "\~". So it's a bit hard to get unbiased data.
So all i've got is annecdotal data: In 10 years of development I encountered exactly one finalizer implementation not part of the .Net Framework. It was in MahApps.Metro and it was faulty - the only reason it got relevant for me (fixing a bug...).
Most of my work colleagues during that years would not have been able to detect and understand the issue. They've never written a finalizer and never needed one. They have, however, been implementing IDisposable many, many times.
Almost every time the following IDisposable implementation sufficed:
public void Dispose()
{
this.something.Dispose();
}
Sometimes this was sensible (but mostly would not be implemented...):
public void Dispose()
{
this.something.Dispose();
this.isDisposed = true;
}
public void Foo()
{
if (this.isDisposed)
{
throw new ObjectDisposedException(this.ToString());
}
}
And in rare cases double disposal needed to be handled (i.E. perform cleanup only the first time Dispose is called, the second time "do nothing").
Actually, quite a lot of "our" IDisposable implementations might be considered a "misuse". They only exist to be able to write:
using(var foo = new Foo())
{
foo....
}
but do not represent an actual resource which requires cleanup.
Now, quite obviously I've never been tasked with implementing a framework or a library...
I do believe the need for custom finalizers to be very rare.
I'm pretty convinced that the majority of dev's is not creating librarys and frameworks. I even do believe the suggested "basic dispose pattern" is unnecessary complex. In most cases you can keep things extremely simple by making the class sealed.
Now, I also believe it's bad to hide existing complexity from users too much. I believe users should be guided to the "simple path" but, if necessary, should be educated about the complex use cases.
I also understand the need for documentation to cover the "legacy" guidance - as implementations will be common for many years to come.
I also believe documentation should not have the same guidance (focus, preferrence) as the Framework design guidelines. Developers are commonly not creating frameworks.
Therefore, personally, I'm back at my original suggestion: https://github.com/dotnet/docs/issues/8463#issue-371470607
I'd also like documentation to contain a proper explanation for the need of non-throwing, safe, double-disposal and the "basic dispose pattern" and proper extensibility in case of non-sealed IDisposable implementations. And of course: how to actually use SafeHandles and how to extend SafeHandle.
The issues I see with the current guidelines is that:
.Dispose() or .Close() etc on managed objects outside of the if (disposing) block. Usually this is from people mistakenly thinking that a FileStream (or similar class) counts as an unmanaged resource, since they know it must in some way wrap a file handle. (This is safe for CriticalFinalizerObjects like SafeHandles, but I'm not referring to those cases.)if (disposing) block, must be finalizer-safe, and/or do not understand what finalizer-safe code entails. There is a lot that is not safe to do/assume in a finalizer.Dispose() simply directly dispose on the each IDisposable field the class owns, is good enough.this. This means that any objects stored in fields of your class, might still be being used if they were copied into a local variable in some method, even if they were not otherwise exposed.To summarize writing finalizers: you cannot risk letting an exception escape your finalizer. Touching any reference-type members of your object is risky, and should probably be avoided. You cannot even assume that your constructor ran to completion (or even ran at all), so even your value type members might be in an inconsistent state.
Now remember that all of this applies to all the code in Dispose(bool) outside of the if(disposing) block, even when your class does not have a finalizer at all (just in case a subclass adds one). I think it should come as no surprise that many people mess this up.
Stephen Toub is of course correct that there are other valid uses of finalizers, besides unmanaged resources. And obviously this pattern is useful when a class has both managed disposable resources, and another use for a finalizer.
But let's spend a minute and pretend that these cases are weird instances of unmanaged resources, even though they really are not. In that case, the general advice would be to create some small sealed finalizable class that wraps this resource. And of course, even though these cases are not really unmanaged resources, this approach is almost always possible. (For these, I would agree with Stephen Toub that abusing CriticalHandle is wrong here. If creating a separate tiny class, it should either inherit from CriticalFinalizerObject or just from regular object.)
Putting the finalizer directly in the main object is really just an optimization to avoid an allocation, and for some reason we don't seem to be too worried about that extra allocation when using a SafeHandle, so I'm not sure why it would be that critical in these other cases.
So in practice these alternate finalizer uses can be treated like unmanaged resources.
I'll go out on a limb here, and say that I strongly suspect that the majority of implementations of Dispose(bool) which include code outside of the if block are buggy, and are not fully safe for running in a finalizer.
Having this guidance means that instead of a straightforward Dispose() implementation that simply calls the dispose method of each owned IDisposable resource (will almost always work correctly), some people trying to do the right thing keep ending up writing buggy Dispose(bool) methods, and often add needless finalizers, all for the benefit of a hypothetical subclass that might want to directly own an unmanaged resource. (Which is a discouraged practice in the first place, )
Most helpful comment
The issues I see with the current guidelines is that:
.Dispose()or.Close()etc on managed objects outside of theif (disposing)block. Usually this is from people mistakenly thinking that aFileStream(or similar class) counts as an unmanaged resource, since they know it must in some way wrap a file handle. (This is safe forCriticalFinalizerObjects likeSafeHandles, but I'm not referring to those cases.)if (disposing)block, must be finalizer-safe, and/or do not understand what finalizer-safe code entails. There is a lot that is not safe to do/assume in a finalizer.Dispose()simply directly dispose on the each IDisposable field the class owns, is good enough.As a reminder of how tricky finalizers are be:
this. This means that any objects stored in fields of your class, might still be being used if they were copied into a local variable in some method, even if they were not otherwise exposed.To summarize writing finalizers: you cannot risk letting an exception escape your finalizer. Touching any reference-type members of your object is risky, and should probably be avoided. You cannot even assume that your constructor ran to completion (or even ran at all), so even your value type members might be in an inconsistent state.
Now remember that all of this applies to all the code in
Dispose(bool)outside of theif(disposing)block, even when your class does not have a finalizer at all (just in case a subclass adds one). I think it should come as no surprise that many people mess this up.Other uses of finalizers
Stephen Toub is of course correct that there are other valid uses of finalizers, besides unmanaged resources. And obviously this pattern is useful when a class has both managed disposable resources, and another use for a finalizer.
But let's spend a minute and pretend that these cases are weird instances of unmanaged resources, even though they really are not. In that case, the general advice would be to create some small sealed finalizable class that wraps this resource. And of course, even though these cases are not really unmanaged resources, this approach is almost always possible. (For these, I would agree with Stephen Toub that abusing
CriticalHandleis wrong here. If creating a separate tiny class, it should either inherit fromCriticalFinalizerObjector just from regular object.)Putting the finalizer directly in the main object is really just an optimization to avoid an allocation, and for some reason we don't seem to be too worried about that extra allocation when using a
SafeHandle, so I'm not sure why it would be that critical in these other cases.So in practice these alternate finalizer uses can be treated like unmanaged resources.
Conclusion
I'll go out on a limb here, and say that I strongly suspect that the majority of implementations of
Dispose(bool)which include code outside of the if block are buggy, and are not fully safe for running in a finalizer.Having this guidance means that instead of a straightforward
Dispose()implementation that simply calls the dispose method of each ownedIDisposableresource (will almost always work correctly), some people trying to do the right thing keep ending up writing buggyDispose(bool)methods, and often add needless finalizers, all for the benefit of a hypothetical subclass that might want to directly own an unmanaged resource. (Which is a discouraged practice in the first place, )