Skip to content

[API Proposal]: Solve the broken multi-interface Dispose lifetime problem with duck-typed dispose(...) keyword. #128182

@AnorZaken

Description

@AnorZaken

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions