I am starting to think that putting IDisposable
on an interface can cause some problems. It implies that the lifetime of all objects implementing that interface can be safely synchronously ended. I.e., it allows anyone to write code like this and requires all implementations to support IDisposable
:
using (ISample myInstance = GetISampleInstance())
{
myInstance.DoSomething();
}
Only the code which is accessing the concrete type can know the right way to control the lifetime of the object. For example, a type might not need disposal in the first place, it might support IDisposable
, or it might require awaiting
some asynchronous cleanup process after you are done using it (e.g., something like option 2 here).
An interface author cannot predict all the possible future lifetime/scope management needs of implementing classes. The purpose of an interface is to allow an object to expose some API so that it can be made useful to some consumer. Some interfaces may be related to lifetime management (such as IDisposable
itself), but mixing them with interfaces unrelated to lifetime management can make writing an implementation of the interface hard or impossible. If you have very few implementations of your interface and structure your code so that the interface’s consumer and lifetime/scope-manager are in the same method, then this distinction is not clear at first. But if you start passing your object around, this will be clearer.
void ConsumeSample(ISample sample)
{
// INCORRECT CODE!
// It is a developer mistake to write “using” in consumer code.
// “using” should only be used by the code that is managing the lifetime.
using (sample)
{
sample.DoSomething();
}
// CORRECT CODE
sample.DoSomething();
}
async Task ManageObjectLifetimesAsync()
{
SampleB sampleB = new SampleB();
using (SampleA sampleA = new SampleA())
{
DoSomething(sampleA);
DoSomething(sampleB);
DoSomething(sampleA);
}
DoSomething(sampleB);
// In the future you may have an implementation of ISample
// which requires a completely different type of lifetime
// management than IDisposable:
SampleC = new SampleC();
try
{
DoSomething(sampleC);
}
finally
{
sampleC.Complete();
await sampleC.Completion;
}
}
class SampleC : ISample
{
public void Complete();
public Task Completion { get; }
}
In the code sample above, I demonstrated three types of lifetime management scenarios, adding to the two you provided.
SampleA
is IDisposable
with synchronous using () {}
support.
SampleB
uses pure garbage collection (it does not consume any resources).
SampleC
uses resources which prevent it from being synchronously disposed and requires an await
at the end of its lifetime (so that it may notify the lifetime management code that it is done consuming resources and bubble up any asynchronously encountered exceptions).
By keeping lifetime management separate from your other interfaces, you can prevent developer mistakes (e.g., accidental calls to Dispose()
) and more cleanly support future unanticipated lifetime/scope management patterns.