Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add default interface methods to IEnumerable<T> and friends #29513

Closed
GrabYourPitchforks opened this issue May 10, 2019 · 5 comments
Closed

Add default interface methods to IEnumerable<T> and friends #29513

GrabYourPitchforks opened this issue May 10, 2019 · 5 comments

Comments

@GrabYourPitchforks
Copy link
Member

The interfaces IEnumerable<T> and IEnumerator<T> are ideal candidates for some of the existing interface methods to gain default implementations since many of the non-generic IEnumerable / IEnumerator methods simply wrap the generic versions and box the return value.

Proposal below. I may have gotten the syntax a bit wrong but you get the gist. :)

namespace System.Collections
{
    public interface IEnumerable
    {
        IEnumerator GetEnumerator();
    }
    public interface IEnumerator
    {
        virtual void Reset() => throw new NotSupportedException();
        bool MoveNext();

        object? Current { get; }
    }
}
namespace System.Collections.Generic
{
    public interface IEnumerable<out T> : IEnumerable
    {
        new IEnumerator<T> GetEnumerator();

        // DIMs for base members

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
    public interface IEnumerator<out T> : IDisposable, IEnumerator
    {
        new T Current { get; }

        // DIMs for base members

        void IDisposable.Dispose() { }
        object? IEnumerator.Current => (object?)Current;
    }
}

This would make it easier for devs writing custom enumerators since they'd need to write less boilerplate code. They could always choose to override the default implementations if desired, but these defaults would likely work for the 99% use case.

Hat tip to @agocke, who brainstormed this.

@AlgorithmsAreCool
Copy link
Contributor

Despite my irrational, bitter hatred of DIM. This is useful and very clever.

I am a little worried about default no-op Dispose() however. I have a vague feeling it will create more opportunities for folks to ignore it or re-implement it due to reduced discoverability.

@msftgits msftgits transferred this issue from dotnet/corefx Feb 1, 2020
@msftgits msftgits added this to the 5.0 milestone Feb 1, 2020
@maryamariyan maryamariyan added the untriaged New issue has not been triaged by the area owner label Feb 23, 2020
@maryamariyan maryamariyan removed the untriaged New issue has not been triaged by the area owner label Mar 3, 2020
@bartonjs
Copy link
Member

bartonjs commented Jun 5, 2020

We feel that the risk vs reward for this particular interface doing a DIM to provide a base member isn't justified.

We're open to experimentation, but not quite this interface...at this time.

@bartonjs bartonjs closed this as completed Jun 5, 2020
@TylerBrinkley
Copy link
Contributor

While just as risky I believe the reward for using a DIM in #31001 is far greater and would be worth it.

@YairHalberstadt
Copy link
Contributor

@bartonjs I'm wondering what the risks are for doing this?

@bartonjs
Copy link
Member

The general form of the risk of using DIMs to provide base implementations is the introduction of "the diamond problem", which can manifest itself as a recompile break (existing code fails to compile simply by upgrading to a newer version), and possibly also runtime failures.

I'm having trouble finding, offhand, where we'd be likely to introduce the diamond internally here, so I'll have to have to introduce a random external interface... but that's actually the place where the biggest risk to the community-at-large comes in.

So, let's pretend that on the first version that supported DIMs (3.1?) I made this gem:

public interface ITypedEnumerable : IEnumerable
{
    TypedEnumerator GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public class TypedEnumerator : IEnumerator
{
    ...
}

To complicate matters, let's also add another interface, which could be in the same package/assembly or a downstream one:

public interface IMoarInterfaces : ITypedEnumerable, IList<Whatever>
{
    ...
}

So then .NET N comes along and has IEnumerable<T> also DIM IEnumerable.GetEnumerator(). Certainly any downstream library that uses IMoarInterfaces will fail to compile due to an ambiguous resolution of IEnumerable.GetEnumerator, because it was either the one provided by IEnumerable<T> (via IList) or the one provided by ITypedEnumerable (IMoarInterfaces could resolve this at the chokepoint, but it's in a library that hasn't updated yet.. which is the problem). So that's not nice.

But what does the runtime do? When loading SomeType : IMoarInterfaces, when no type-local implementation is present, it also sees two possible versions. (At least, I'm recalling that with DIMs the compiler doesn't lift the implementation to the types that benefit, just lets the runtime deal with it. That would happen for a net-new method provided by a DIM, at least.) So that's probably a TypeLoadException. The package that provided IMoarInterfaces is now unusable on .NET N+ until it publishes a new version. That's really not nice. (Sure, it's the breaking change that you "can expect" on major versions with SemVer, but did adding this DIM look like a breaking change?)


Those are the complications we've thought of. Part of the problem with DIMs is we haven't used them, so we don't actually know when they introduce more problems than they solve; but this one seems to have at least "some" risk while only replacing a very easy line of boilerplate, so it doesn't pass the risk/reward evaluation.

Diamonds are avoided if you never use DIMs for base implementations, just for aliasing or pure default (throw, false, zero, whatever) implementations.

public interface ISomeNewInterface
{
    // Maybe some implementation wants to pick a different encoding than UTF8,
    // but now there's "with the object's default encoding".
    // Or maybe it can benefit from not needing to do the null check on Encoding.
    //
    // No matter what, simplification overloads are now less cumbersome on interfaces,
    // and don't have the extension method problem of requiring strong type references
    // to replace.
    //
    // But we haven't learned all the versioning risks yet, so do it on the first
    // version the type was introduced and don't split-compile to a version without it :)
    virtual object DoSomething(string s) => DoSomething(s, Encoding.UTF8);

    object DoSomething(string s, Encoding encoding);
}

@ghost ghost locked as resolved and limited conversation to collaborators Dec 13, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

7 participants