Background and motivation
It remains problematic that when a disposable class implements multiple interfaces, the lifetime of those interfaces could be subtly different, affected by dispose call order, but the current Dispose pattern is unable to handle that, because typically callers just cast to IDisposable and then call Dispose, meaning the context of what the caller thinks it is disposing is unknown to the implementation of the Dispose method.
The C# solution to a semi-similar problem, is when two interfaces have the same method signature, and then you solve it by explicit interface implementation --> this lets the implementation class differentiate what the caller thinks they are calling, and act accordingly. But since the pattern for IDisposable is to let the caller cast to the general IDisposable interface first, there is zero differentiation information available when the call reaches the implementing class.
In conclusion the dispose pattern is subtly broken under multi-interface inheritance in C#, and in hindsight the only way to correctly implement dispose semantics would have been to use Duck-Typing just as foreach does: Add a dispose(...) keyword similar to foreach(...) have it resolve Dispose via Duck-Typing just like foreach: If an interface has a Dispose method, it compiles. Now the implementing class can implement differentiated Dispose methods via explicit interface implementation, which solves the broken dispose pattern.
See this post for a simple code example of when the current Dispose pattern makes it impossible to handle lifetimes correctly:
#70004 (comment)
For backwards compatibility the new dispose keyword should check if casting to IDisposable is possible too, similar to using, but should prefer the duck-typed Dispose method on the compile-time type if it exists. Types where the compile-time type does not reveal a Dispose method should probably give compiler Warning or Information message, but not an error since it's probably best to have dispose work similar to using in that it silently does nothing at runtime if the runtime-type isn't disposable.
TL;DR: C# does not actually support a class to implement / inherit multiple disposable interfaces correctly.
At the very least we should have a code-analyzer warning for this.
API Proposal
// Since the "API" surface is compiler sugar, I'm not sure what exactly *that* code would look like,
// but here is the consumable surface example:
public record DisposeCallingConsumer(
ISomeDisposableThing interface1,
ISomeOtherDisposableThing interface2,
SomeClassWithMultipleInterfaces theClass)
{
void DisposeExample()
{
// (A record should not dispose like this, but just to keep the example code small and focused on the proposed syntax.)
// Disposing via new proposed "dispose(...)" keyword that works with duck-typing similar to "foreach(...)":
dispose(interface1); // <-- compiler looks for Dispose method on ISomeDisposableThing (duck-typing)
dispose(interface2); // <-- compiler looks for Dispose method on ISomeOtherDisposableThing (duck-typing)
dispose(theClass);// <-- compiler looks for Dispose method on SomeClassWithMultipleInterfaces (duck-typing)
// In all calls above the implementing class knows what the caller (this) *thinks* it is disposing,
// and can use explicit implementation to act accordingly.
}
}
API Usage
public interface ISomeDisposableThing
{
void DoSomethingElse();
void Dispose();
}
public interface ISomeOtherDisposableThing
{
void DoSomeOtherThing();
void Dispose();
}
public class SomeClassWithMultipleInterfaces : ISomeDisposableThing, ISomeOtherDisposableThing
{
public void Dispose() => /* called if class is disposed */;
void ISomeDisposableThing.Dispose() => /* called if ISomeDisposableThing is disposed */;
void ISomeOtherDisposableThing.Dispose() => /* called if ISomeOtherDisposableThing is disposed */;
}
public record DisposeCallingConsumer(
ISomeDisposableThing interface1,
ISomeOtherDisposableThing interface2,
SomeClassWithMultipleInterfaces theClass)
{
void DoStuff()
{
// (A record should not dispose like this, but just to keep the example code small and focused on the proposed syntax.)
// Disposing via new proposed "dispose(...)" keyword that works with duck-typing similar to "foreach(...)":
dispose(interface1);
dispose(interface2);
dispose(theClass);
// In all calls above the implementing class knows what the caller (this) *thinks* it is disposing,
// and can use explicit implementation to act accordingly.
}
}
Alternative Designs
None.
Risks
- What kind of dispose action should consumers expect when using the
using keyword?
- Should calling
dispose when duck-typing cannot find a Dispose method at compile-time be a compile error, warning, or information? We are torn between how similar we want it to be to foreach and how similar we want it to be to using.
- If a user updates their existing code by using the proposed
dispose keyword, there is a small risk of behavioral change if they already had another Dispose method declared somewhere with the same signature as IDisposable.Dispose(). The good thing is that unless the user refactors their code to use dispose it shouldn't happen, but it does complicate possible code analyzers - i.e. if we wanted an analyzer to recommend a refactor to the user to call dispose instead, it would have to check that no duck-typed Dispose method aside from the method that IDisposable.Dispose would resolve to exists before it proposes the change.
Background and motivation
It remains problematic that when a disposable class implements multiple interfaces, the lifetime of those interfaces could be subtly different, affected by dispose call order, but the current Dispose pattern is unable to handle that, because typically callers just cast to IDisposable and then call Dispose, meaning the context of what the caller thinks it is disposing is unknown to the implementation of the Dispose method.
The C# solution to a semi-similar problem, is when two interfaces have the same method signature, and then you solve it by explicit interface implementation --> this lets the implementation class differentiate what the caller thinks they are calling, and act accordingly. But since the pattern for IDisposable is to let the caller cast to the general IDisposable interface first, there is zero differentiation information available when the call reaches the implementing class.
In conclusion the dispose pattern is subtly broken under multi-interface inheritance in C#, and in hindsight the only way to correctly implement dispose semantics would have been to use Duck-Typing just as foreach does: Add a dispose(...) keyword similar to foreach(...) have it resolve Dispose via Duck-Typing just like foreach: If an interface has a Dispose method, it compiles. Now the implementing class can implement differentiated Dispose methods via explicit interface implementation, which solves the broken dispose pattern.
See this post for a simple code example of when the current Dispose pattern makes it impossible to handle lifetimes correctly:
#70004 (comment)
For backwards compatibility the new dispose keyword should check if casting to IDisposable is possible too, similar to
using, but should prefer the duck-typed Dispose method on the compile-time type if it exists. Types where the compile-time type does not reveal a Dispose method should probably give compiler Warning or Information message, but not an error since it's probably best to havedisposework similar tousingin that it silently does nothing at runtime if the runtime-type isn't disposable.TL;DR: C# does not actually support a class to implement / inherit multiple disposable interfaces correctly.
At the very least we should have a code-analyzer warning for this.
API Proposal
API Usage
Alternative Designs
None.
Risks
usingkeyword?disposewhen duck-typing cannot find a Dispose method at compile-time be a compile error, warning, or information? We are torn between how similar we want it to be toforeachand how similar we want it to be tousing.disposekeyword, there is a small risk of behavioral change if they already had another Dispose method declared somewhere with the same signature asIDisposable.Dispose(). The good thing is that unless the user refactors their code to usedisposeit shouldn't happen, but it does complicate possible code analyzers - i.e. if we wanted an analyzer to recommend a refactor to the user to calldisposeinstead, it would have to check that no duck-typed Dispose method aside from the method that IDisposable.Dispose would resolve to exists before it proposes the change.