The real meaning of IDisposable
is that an object knows of something, somewhere which has been put into a state that should be cleaned up, and it has the information and impetus necessary to perform such cleanup. Although the most common "states" associated with IDisposable
are things like files being open, unmanaged graphic objects being allocated, etc. those are only examples of uses, and not a definition of "proper" use.
The biggest issue to consider when using IDisposable
and using
for scoped behavior is that there is no way for the Dispose
method to distinguish scenarios where an exception is thrown from a using
block from those where it exits normally. This is unfortunate, since there are many situations where it would be useful to have scoped behavior which was guaranteed to have one of two exit paths depending upon whether the exit was normal or abnormal.
Consider, for example, a reader-writer lock object with a method that returns an IDisposable
"token" when the lock is acquired. It would be nice to say:
using (writeToken = myLock.AcquireForWrite())
{
... Code to execute while holding write lock
}
If one were to manually code the acquisition and release of the lock without a try/catch or try/finally lock, an exception thrown while the lock was held would cause any code that was waiting on the lock to wait forever. That is a bad thing. Employing a using
block as shown above will cause the lock to be released when the block exits, whether normally or via exception. Unfortunately, that may also be a bad thing.
If an unexpected exception is thrown while a write-lock is held, the safest course of behavior would be to invalidate the lock so that any present or future attempt to acquire the lock will throw an immediate exception. If the program cannot usefully proceed without the locked resource being usable, such behavior would cause it to shut down quickly. If it can proceed e.g. by switching to some alternate resource, invalidating the resource will allow it to get on with that much more effectively than would leaving the lock uselessly acquired. Unfortunately, I don't know of any nice pattern to accomplish that. One could do something like:
using (writeToken = myLock.AcquireForWrite())
{
... Code to execute while holding write lock
writeToken.SignalSuccess();
}
and have the Dispose
method invalidate the token if it's called before success has been signaled, but an accidental failure to signal the success could cause the resource to become invalid without offering indication as to where or why that happened. Having the Dispose
method throw an exception if code exits a using
block normally without calling SignalSuccess
might be good, except that throwing an exception when it exits because of some other exception would destroy all information about that other exception, and there's no way Dispose
can tell which method applies.
Given those considerations, I think the best bet is probably to use something like:
using (lockToken = myLock.CreateToken())
{
lockToken.AcquireWrite(Describe how object may be invalid if this code fails");
... Code to execute while holding write lock
lockToken.ReleaseWrite();
}
If code exits without calling ReleaseWrite
, other threads that try to acquire the lock will receive exceptions that include the indicated message. Failure to properly manually pair the AcquireWrite
and ReleaseWrite
will leave the locked object unusable, but not leave other code waiting for it to become usable. Note that an unbalanced AcquireRead
would not have to invalidate the lock object, since code inside the read would never put the object into an invalid state.