dotnet / csharplang

The official repo for the design of the C# programming language
11.61k stars 1.03k forks source link

A Tour of Default Interface Methods for C# ("traits") #288

Closed gafter closed 7 years ago

gafter commented 7 years ago

This is an explanatory summary of the proposed default interface methods feature for C#, intended to lead the LDM through an understanding of the proposed feature, with examples, and to guide the discussion. We present the feature as it applies to methods, but the intent is that is also applies to properties and indexers. For simplicity of exposition, we confine our discussion to methods.

Open Issue: Should this also apply to events?

Similarly, it applies equally to classes and structs, but we confine our exposition to classes.

This proposal adds support for virtual extension methods - methods in interfaces with concrete implementations. A class that implements such an interface is required to have a single most specific implementation for the interface method inherited from its base classes or interfaces.

The principal motivations for this feature are

(Based on the likely implementation technique) this feature requires corresponding support in the CLI/CLR. Programs that take advantage of this feature cannot run on earlier versions of the platform.

Modifiers in interfaces

Because this proposal includes modifiers that can newly be applied to methods in interfaces (private, static, and override), as we will describe later, we propose that the default modifiers public and abstract be permitted to be explicit as well. For clarity, we sometimes use these modifiers explicitly in examples of this feature.

Open Issue: should we permit the modifiers abstract and public on methods in interfaces, even though that is the default? Open issue: should we permit virtual?

Concrete methods in interfaces

The simplest form of this feature is the ability to declare a concrete method in an interface, which is a method with a body.

interface IA
{
    void M() { WriteLine("IA.M"); }
}

A class that implements this interface need not implement its concrete method.

class C : IA { } // OK

IA i = new C();
i.M(); // prints "IA.M"

The final override for IA.M in class C is the concrete method M declared in IA. Note that a class does not inherit members from its interfaces; that is not changed by this feature:

new C().M(); // error: class 'C' does not contain a member 'M'

The basic feature is particularly useful to enable evolution of existing interface types by the addition of new virtual methods.

Overrides in interfaces

An interface can override a method declared in a base interface, with or without explicitly naming the overridden method's declaring interface

interface IA
{
    void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
    override void IA.M() { WriteLine("IB.M"); } // explicitly named
}
interface IC : IA
{
    override void M() { WriteLine("IC.M"); } // implicitly named
}

If the interface is not named in the override declaration, then all matching methods (from direct or indirect base interfaces) are overridden. There must be at least one such method or the override declaration is an error.

Open issue: should that "direct and indirect" be "direct" here?

Overrides in interfaces are useful to provide a more specific (e.g. more efficient) implementation of a base interface's method. For example, a new First() method on IEnumerable may have a much more efficient implementation on the interface IList.

A method declared in an interface is never treated as an override of another method unless it contains he override modifier. This is necessary for compatibility.

interface IA
{
    void M();
}
interface IB : IA
{
    void M(); // not related to 'IA.M'; not an override
}

Reabstraction

A virtual (concrete) method declared in an interface may be overridden to be abstract in a derived interface

interface IA
{
    void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
    override abstract void M();
}
class C : IB { } // error: class 'C' does not implement 'IA.M'.

The abstract modifier is not required in the declaration of IB.M (that is the default in interfaces), but it is probably good practice to be explicit in an override declaration.

This is useful in derived interfaces where the default implementation of a method is inappropriate and a more appropriate implementation should be provided by implementing classes.

The most specific override rule

We require that every interface and class have a most specific override for every interface method among the overrides appearing in the type or its direct and indirect interfaces. If there is no override, the method itself is considered the most specific override. One override M1 is considered more specific than another override M2 if M1 is declared on type T1, M2 is declared on type T2, and T1 contains T2 among its direct or indirect interfaces. The most specific override is a unique override that is more specific than every other override.

interface IA
{
    void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
    override void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
    override void IA.M() { WriteLine("IC.M"); }
}
interface ID : IB, IC { } // error: no most specific override for 'IA.M'
abstract class C : IB, IC { } // error: no most specific override for 'IA.M'
abstract class D : IA, IB, IC // ok
{
    public abstract void M();
}

The most specific override rule ensures that a conflict (i.e. an ambiguity arising from diamond inheritance) is resolved explicitly by the programmer at the point where the conflict arises.

Because we support explicit abstract overrides in interfaces, we could do so in classes as well

abstract class E : IA, IB, IC // ok
{
    abstract void IA.M();
}

Open issue: should we support explicit interface abstract overrides in classes?

In addition, it is an error if in a class declaration the most specific override of some interface method is an an abstract override that was declared in an interface. This is an existing rule restated using the new terminology.

interface IF
{
    void M();
}
abstract class F : IF { } // error: 'F' does not implement 'IF.M'

static and private methods

Because interfaces may now contain executable code, it is useful to abstract common code into private and static methods. We now permit these in interfaces.

Open issue: Should we support private methods? Should we support static methods?

Open issue: should we permit interface methods to be protected or internal or other access? If so, what are the semantics? Are they virtual by default? If so, is there a way to make them non-virtual?

Open issue: If we support static methods, should we support (static) operators?

Base interface invocations

An instance (nonstatic) method is permitted to invoke an accessible instance method override in a direct base interface nonvirtually by naming it using the syntax Type.base.M. This is useful when an override that is required to be provided due to diamond inheritance is resolved by delegating to one particular base implementation.

interface IA
{
    void M() { WriteLine("IA.M"); }
}
interface IB : IA
{
    override void IA.M() { WriteLine("IB.M"); }
}
interface IC : IA
{
    override void IA.M() { WriteLine("IC.M"); }
}

class D : IA, IB, IC
{
    void IA.M() { IB.base.M(); } 
}

Open issue: what syntax should we use for base invocation?

Effect on existing programs

The rules presented here are intended to have no effect on the meaning of existing programs.

Example 1:

interface IA
{
    void M();
}
class C: IA // Error: IA.M has no concrete most specific override in C
{
    public static void M() { } // method unrelated to 'IA.M' because static
}

Example 2:

interface IA
{
    void M();
}
class Base: IA
{
    void IA.M() { }
}
class Derived: Base, IA // OK, all interface members have a concrete most specific override
{
    private void M() { } // method unrelated to 'IA.M' because private
}

The same rules give similar results to the analogous situation involving default interface methods:

interface IA
{
    void M() { }
}
class Derived: IA // OK, all interface members have a concrete most specific override
{
    private void M() { } // method unrelated to 'IA.M' because private
}

Open issue: confirm that this is an intended consequence of the specification.

Further areas to be specified

/cc @dotnet/csharplangdesign @dotnet/roslyn-compiler

yaakov-h commented 7 years ago

What are the impacts on source and binary compatibility? Would adding a new method to an interface be non-breaking if it has a default implementation?

orthoxerox commented 7 years ago

I have to think a while before I can say anything about the semantics, but I feel that IB.base.M() would look better as base<IB>.M().

AlgorithmsAreCool commented 7 years ago

I'm genuinely amazed that the LDM is pushing to add multiple inheritance to C# so many years after launch. Not mad, just amazed. It feels like altering a major design principle of the language.

My layman's impression is that there will be such minor differences between classes and interfaces now it almost seems to put the two features into redundancy.

gafter commented 7 years ago

@AlgorithmsAreCool C# has had multiple inheritance in interfaces since the start. MI is problematic when you can inherit state, but this proposal does not put state into interfaces.

gafter commented 7 years ago

@yaakov-h yes, that question is posed in the penultimate bullet of the OP. The answer is longish and I expect it will form a new discussion issue.

HaloFour commented 7 years ago

It comes down to classes carry state and interfaces only convey behavior. That eliminates much of the issue with multiple inheritance. It works fairly well in Java and ought to work better in C# as explicit overrides could help resolve ambiguities.

Since interfaces can't manage state is there a lot of purpose to default event implementations? Seems at best you could have them be no-ops, manage subscription through some globally-accessible mechanism or maybe translate the event to something expected by one of the other methods, like a callback interface. But I can't imagine that would add any additional complication to this proposal.

As for static methods, purely a question of design. VB.NET has supported that since 1.0. It's not CLS but C# allows plenty of things that aren't CLS.

louthy commented 7 years ago

Since interfaces can't manage state is there a lot of purpose to default event implementations

EDIT: Misread the original comment. I see now it's referring to events. My bad.

@HaloFour They'd be very useful for doing ad-hoc polymorphism, i.e.:


    public interface Semigroup<A>
    {
        A Append(A x, A y);
    }

    public interface Monoid<A> : Semigroup<A> 
    {
        A Empty();

        A Concat(IEnumerable<A> xs) =>
            xs.Fold(Empty(), Append);

        A Concat(params A[] xs) =>
            xs.Fold(Empty(), Append);
    }

    public struct MString : Monoid<string>
    {
        public static readonly MString Inst = new MString();

        public string Append(string x, string y) =>
            x + y;

        public string Empty() =>
            "";
    }

    public struct MEnumerable<A> : Monoid<Enumerable<A>>
    {
        public static readonly MEnumerable<A> Inst = new MEnumerable<A>();

        public Enumerable<A> Append(Enumerable<A> x, Enumerable<A> y) =>
            Enumerable.Concat(x, y);

        public Enumerable<A> Empty() =>
            new A[0];
    }

    var x = MEnumerable<A>.Inst.Concat(new [] {  new [] { 1,2,3 }, new [] { 4,5,6 } }); // [1,2,3,4,5,6]
    var y = MString.Inst.Concat("Hello", " ", "World"); // "Hello World"

So I think this has value for the work on Shapes. And as someone who's using this technique quite extensively already, I think this work is very welcome.

smoothdeveloper commented 7 years ago

it feels the syntax for calling base (non ambiguously) should inherit from C++ or the way we disambiguate with assembly qualfier.

interface A {
  void Foo(){}
}
interface B {
  void Foo(){}
}

class C : A,B {
  override Foo(){
    A::Foo();
  }
}
lloydjatkinson commented 7 years ago

I don't see the advantage of this over having a class that implements an interface and inherits an abstract class. This suggestion seems to make interfaces no longer just an interface, and it feels "messy".

Jorenkv commented 7 years ago

The final override for IA.M in class C is the concrete method M declared in IA. Note that a class does not inherit members from its interfaces; that is not changed by this feature: new C().M(); // error: class 'C' does not contain a member 'M'

Is this necessary? It seems pretty awful to have to cast an object just because the member you'd like to access happens to be defined in an interface instead of in the class itself.

When you're only using this feature to provide default behaviour for an interface this won't be an issue, but I'd also like to use the feature to allow code reuse without needing to derive from a class.

DavidArno commented 7 years ago

My thoughts on this idea:

  1. The fact that @jcouv is on twitter saying he's excited to be working on this feature, yet @gafter is talking about it being a proposal highlights the fact that comms from the team to the community are still broken. There's no LDM notes for February still, let alone Mach. The promised improved interaction between @MadsTorgersen & the team and the community still hasn't happened. The community is once again in the dark as to what the team is doing.
  2. Why is the team putting effort into this feature now? Why is it suddenly a priority? Does this mean that features that many of us are still waiting for, such as better pattern matching and records, are going to be delayed yet again?
  3. The main cited use case is for allowing interfaces to add new members without breaking existing code. This is a good use case, and the basic idea of concrete methods in interfaces, that can only be accessed via referencing that interface nicely solves that use case in a simple way. So why then does the proposal quickly become muddled and over-complicated with ideas around overriding in other interfaces, forcing back to abstract and even talk of protected and internal? What are the use cases for these complicating additions?
lloydjatkinson commented 7 years ago

The main cited use case is for allowing interfaces to add new members without breaking existing code. This is a good use case, and the basic idea of concrete methods in interfaces, that can only be accessed via referencing that interface nicely solves that use case in a simple way.

Personally I disagree that they are a good use case. Interfaces are meant to be a contract, not an implementation. So why now should implementation be added to an interface?

The argument about not breaking existing code I do not feel has a lot of weight. Having to make breaking changes to your own code is just part of the process of refactoring. This proposal seems like a huge workaround/"hack" just to avoid having to update your implementations of an interface.

ilexp commented 7 years ago

I'm not against evolving interfaces as C# grows, but this proposal seems to overcomplicate things on the user side. Interfaces right now are a very simple concept, and I'd consider this a plus that is worth keeping.

jmazouri commented 7 years ago

Personally, I would much prefer an implementation of #164 rather than muddying interface definitions with concrete implementations.

mgarrettm commented 7 years ago

Completely agree with @jmazouri, expanded extension methods would be a much cleaner way of addressing the cited use case, rather than compromising the intention of interfaces.

AlgorithmsAreCool commented 7 years ago

@DavidArno Lets hold off on oiling the pitchforks for a little while, i think there is just some enthusiasm for this feature from a few team members and they are putting together a prototype to sell to the LDM. Any of us could do that with our proposals if we had the time and expertise.

Thinking about this more, I don't think this is a good idea. It solves a narrow problem, and in the process it creates confusion in the core design of the language without great benefit. C# has made all this way with very limited MI via interfaces, so why does it need attention now?

CyrusNajmabadi commented 7 years ago

C# has made all this way with very limited MI via interfaces, so why does it need attention now?

We could literally make thta argument about any feature :) After all, every feature is one that we made it all this way without doing.

The simple fact of the matter is that this has been an issue with .Net for a very long time. To the point that working with interfaces can be quite a struggle. We've felt this pain in the .Net APIs themselves as well as through many other APIs that MS has exposed.

This is an area we've definitely wanted to improve things in in the past, but not all the right pieces were in place for that to happen, or other things were felt to be more important. Now it's the case that we think we could do it, and we still think this would be super helpful and valuable for our surrounding ecosystem.

HaloFour commented 7 years ago

This has been a useful feature in Java. It was their answer to extension methods, and it does overlap them a little from a use case basis. But it also enables a couple of different scenarios. Being properly virtual they allow for type-specific implementations that tolerate casting without having to bake implementation-specific dispatch into the extension method.

I think there is room for both in the language. From a consumer point of view it's no different than working with any other interface. From an implementer's perspective it makes things easier as the number of required members decreases. IEnumerator<T> could finally be reduced to the two members that the vast majority of implementations actually implement (or at least make it so that I don't have to explicitly implement IEnumerator.Current.)

CyrusNajmabadi commented 7 years ago

The fact that @jcouv is on twitter saying he's excited to be working on this feature, yet @gafter is talking about it being a proposal highlights the fact that comms from the team to the community are still broken.

As we've said on our home page:

In many cases it will be necessary to implement and share a prototype of a feature in order to land on the right design, and ultimately decide whether to adopt the feature. Prototypes help discover both implementation and usability issues of a feature. A prototype should be implemented in a fork of the Roslyn repo and meet the following bar:

CyrusNajmabadi commented 7 years ago

This has been a useful feature in Java. It was their answer to extension methods, and it does overlap them a little from a use case basis. But it also enables a couple of different scenarios. Being properly virtual they allow for type-specific implementations that tolerate casting without having to bake implementation-specific dispatch into the extension method.

Yup. And this has been an issue for us in our own APIs. For example, a lot of linq extension methods optimize for the IList case. But they don't work properly on IReadOnlyList. That's unfortunate. Extensions have carried us far. But we see cracks there and we think we have an idea about how we can create a good system that solves another set of issues well.

I think there is room for both in the language. From a consumer point of view it's no different than working with any other interface. From an implementer's perspective it makes things easier as the number of required members decreases. IEnumerator could finally be reduced to the two members that the vast majority of implementations actually implement (or at least make it so that I don't have to explicitly implement IEnumerator.Current.)

Yup. It would also just be great for working with interfaces in APIs today. Right now you have to do the "IFoo1, IFoo2, ... IFooX" route for interfaces in order to add members to them. It's super unpleasant and it would be great if we could safely add members to interfaces without that being a massive breaking change like it is today.

CyrusNajmabadi commented 7 years ago

Why is the team putting effort into this feature now?

Because it's something we've wanted to make better for years. And we think we may be able to. So we'd like to learn more so we can effectively design the best feature.

Why is it suddenly a priority?

Because we looked at the set of work we could do, and we thought this made the cut. That's what we do with every language release.

Does this mean that features that many of us are still waiting for, such as better pattern matching and records, are going to be delayed yet again?

Maybe. Maybe not. We're looking at a lot of different things and a lot of different areas for our next set of releases. I'm sure that every single thing we work on will be in an "i don't care" group for some set of customers :)

AlgorithmsAreCool commented 7 years ago

We could literally make thta argument about any feature :) After all, every feature is one that we made it all this way without doing.

Yeah, as soon as I typed that I regretted it 😆

That being said, if shapes or shapes+records would cover the same conceptual ground as this proposal (which i am fairly sure shapes would), I would take shapes in a heartbeat over this. Half of a heartbeat even.

Eirenarch commented 7 years ago

If this is implemented what would be the use case for extension methods over default methods?

CyrusNajmabadi commented 7 years ago

If this is implemented what would be the use case for extension methods over default methods?

You could still only provide a default-method if you were the author of the interface. They can't be added by someone else. Extension methods don't have that limitation. They can be added externally.

CyrusNajmabadi commented 7 years ago

Extension methods can also do things like specifically extend specific instantiations. i.e. i can have an extension method specifically for the type IList<int>. That's not possible with default interface members.

jamesqo commented 7 years ago

:O I linked to this issue on reddit because I thought it was exciting. Surprised to see people are generally against it rather than in favor of it.

yaakov-h commented 7 years ago

I don't mind making interface methods optional (a la Objective-C, or even something else entirely), but providing a hard implementation - even a default one - seems more like multiple inheritance and less like providing an interface.

bbarry commented 7 years ago

I somewhat like the idea of interfaces being able to implement public and private methods, but I am less enthusiastic about interfaces overriding methods from other interfaces and protected methods (complex inheritance trees quickly become a nasty mess).

Considering the broader picture... I'm assuming since properties are being brought along, you are only going to bring properties which don't have backing state into this? That is to say it would still be a breaking change to add a property to an interface with a default get/set. I think I am in a dislike state here. Thinking that I would really want #133, I lean further into dislike.

That said, it makes sense to implement all of that in a single CLR change. Maybe it is worth implementing everything necessary at the CLR level, but only some of it in C#?

Grauenwolf commented 7 years ago

I really don't like this.

The reason why Java needs default methods is that they don't use interfaces correctly. Rather than keeping them small and to the point, they bloat them with excessive amounts of methods in a pointless attempt to avoid working with "concrete classes".

So far .NET has mostly managed to avoid that. There are a couple problem spots such as the interfaces in System.Data, but by and large we're using abstract interfaces correctly and thus don't need this feature.

I hate to be "that guy", but I really think this is just going to open the door to bad practices. It will eliminate the semantic difference between abstract classes and abstract interfaces and we're just going to see a ton of abstract classes implemented with the interface keyword.

Grauenwolf commented 7 years ago

Backwards compatibility is a huge issue here.

Say you add a new default method to an interface. But a class already has a matching method that does something completely different. What happens?

Does the class implicitly override the default method without have to be recompiled? If no, that means recompiling without changing the code changes the behavior. If yes, that may result in incorrect behavior if the two methods mean different things and just happen to have the same name.

We avoid this for class inheritance by insisting that methods can only override other methods explicitly. Eliminating the "brittle base class" problem that Java has.

But it we do that for C#, then we have some interface methods that require overrides and some that do not. Besides being confusing, it means adding a default implementation later is also a breaking change.

Grauenwolf commented 7 years ago

By considering protected and internal methods, it's pretty clear that we're no longer talking about default methods and have gone 90% of the way to multiple inheritance.

If you really want multiple inheritance, be up front about it. Don't try to sneak it in as part of abstract interfaces; make it a first class feature.

Grauenwolf commented 7 years ago

I don't see why static methods are part of this proposal.

Unlike the rest of it, I think static methods would be great. Especially when combined with extension methods. It is just an organizational convenience so we don't need a separate static class FooExtension to hold them. It doesn't actually change any semantics or introduce any danger in terms of complexity or backwards compatibility.

Grauenwolf commented 7 years ago

For example, a lot of linq extension methods optimize for the IList case. But they don't work properly on IReadOnlyList.

That's because IList doesn't inherit from IReadOnlyList.

But I don't see how this change would fix that problem.

jamesqo commented 7 years ago

I hate to be "that guy", but I really think this is just going to open the door to bad practices. It will eliminate the semantic difference between abstract classes and abstract interfaces and we're just going to see a ton of abstract classes implemented with the interface keyword.

Abstract classes should be used when the base class contains fields, or may contain fields in the future. An interface with default methods would be used when you know that the behavior of the default methods would be determined purely by the behavior of the implemented methods.

jamesqo commented 7 years ago

By considering protected and internal methods, it's pretty clear that we're no longer talking about default methods and have gone 90% of the way to multiple inheritance.

If you really want multiple inheritance, be up front about it. Don't try to sneak it in as part of abstract interfaces; make it a first class feature.

We don't have the diamond inheritance problem due to the most specific override rule, which is the main caveat of multiple inheritance.

jamesqo commented 7 years ago

Unlike the rest of it, I think static methods would be great.

I agree with you here, it would be nice to be able to have static factory methods to create instances of interfaces. Would probably help with decoupling, e.g. you're calling IFoo.Create() which returns an IFoo, as opposed to Foo.Create() which returns a Foo, and you can change IFoo.Create() to return whatever kind of type you want without breaking code so long as it inherits from IFoo.

CyrusNajmabadi commented 7 years ago

But I don't see how this change would fix that problem.

The point would have been that we could just have added members to IEnumerable<T> initially, and then provided specializations per implementation. For example, instead of "Count" being an extension method, it could have been a default interface method:

interface IEnumerable<T>
{
    int Count()
    {
        int count = 0;
        foreach (var x in this)
        {
             count++;
        }

        return count;
    }
}

interface IReadOnlyList<T> ...
{
    int Count { get; }
    override int IEnumerable<T>.Count() => this.Count;
}

interface IList<T> ...
{
    int Count { get; }
    override int IEnumerable<T>.Count() => this.Count;
}

Right now the extension-method based approach can only accomplish the above by doing an explicit type-check against the stream it is passed.

CyrusNajmabadi commented 7 years ago

An interface with default methods would be used when you know that the behavior of the default methods would be determined purely by the behavior of the implemented methods.

Agreed. It also provides a way to give stock implementations of behavior to interfaces, while still enabling implementations to provide specialized behavior. That's not something you can get with extensions. Extensions work for the former, but fail for the latter.

Grauenwolf commented 7 years ago

An interface with default methods would be used when you know that the behavior of the default methods would be determined purely by the behavior of the implemented methods.

That requires being able to predict the future. Maybe you don't need a field today, but you might tomorrow when you add another default method.

There's no "pit of success" here; developers have to make a hard decision with partial information when before the choice was obvious.

CyrusNajmabadi commented 7 years ago

but you might tomorrow when you add another default method.

Then you cannot use a default method for that case. But you can still use a default method for the cases where you don't need state.

It's like saying that we shouldn't have structs because they force developers to have to make a tough decision up front with partial information. After all, in teh future they may want to add a virtual method, and want to have subtyping. But they can't now because they chose a struct isn't of a class.

Developers routinely have to make choices that may not be the best once the future is known. But, by exposing functionality like this, we make it possible to make those future points far less painful than they are today.

Grauenwolf commented 7 years ago

The point would have been that we could just have added members to IEnumerable initially, and then provided specializations per implementation. For example, instead of "Count" being an extension method, it could have been a default interface method:

I see your point, but I strongly disagree.

If you did that, then it wouldn't be IEnumerable anymore, it would be IReadOnlyCollection.

IReadOnlyCollection implies certain things that IEnumerable doesn't, such as the collection actually existing and not waiting for a database or web service call to be completed.

If you threw Count onto IEnumerable, that distinction is lost. And that would be a very bad thing.

This interface bloat is a serious problem in Java. I really don't want to see it happen in .NET.

CyrusNajmabadi commented 7 years ago

If you did that, then it wouldn't be IEnumerable anymore, it would be IReadOnlyCollection.

No it wouldn't. Today, all IEnumerable's effectively have a Count() on them already, thanks to the extension method in System.Linq. User already get a default implementation they can use against any stream. That implementation also tries to be more efficient when it can. But it can only do so in the most adhoc manner (by hard coding in knowledge in the extension method about specific types).

Having "Count()" (not 'Count' btw) be directly provided on IEnumerable, you are in the same world as today (where everyone can get the Count() of a stream), except now

a. all implementations can have the ability to provide an efficient impl. b. derived interfaces even have the ability to provide efficient impls.

If you threw Count onto IEnumerable, that distinction is lost. And that would be a very bad thing.

"Count()" is already there for IEnumerable. It just comes through a weak extension method, instead of being a real member that base types can specialize.

CyrusNajmabadi commented 7 years ago

This interface bloat is a serious problem in Java. I really don't want to see it happen in .NET.

One of the core points is that an interface is not bloated if an implementation can be provided. From a base-type-implementation side, you don't need to do anything here (unless you feel like you can do better than the default impl). And from the consumer side, you have access to the same sort of set of functionality you would normally see today through the interface+inflexible extension methods.

Grauenwolf commented 7 years ago

It's like saying that we shouldn't have structs because they force developers to have to make a tough decision up front with partial information.

We have structs because the performance benefits are worth the extra design complexity. And even then built-in structs are used only in very rare circumstances.

The use case for this is either "partially implemented MI" or "changing the contract of pre-existing interfaces". I don't see either of those as beneficial.

CyrusNajmabadi commented 7 years ago

To be clear @Grauenwolf , you stated "But I don't see how this change would fix that problem."

The problem would be fixed very simply:

  1. We'd have IEnumerable.Count() available for consumers, just like we do today through System.Linq
  2. IEnumerable.Count() would be efficient on IReadOnlyLists, unlike today where it is O(n) instead of O(1)

You don't seem to like the solution to this problem. but i don't like that we have this problem in the first place. I don't like that extensions work fine until you get to the point that you want specialized behavior. I'd like a feature that allows for the benefits of extensions (adding functionality, without having state, and while only using the available interface there), while also allowing specialization.

CyrusNajmabadi commented 7 years ago

We have structs because the performance benefits are worth the extra design complexity.

And we believe that default-interface-methods provide benefits (including major Perf benefits) that are worth the extra design complexity.

CyrusNajmabadi commented 7 years ago

or "changing the contract of pre-existing interfaces". I don't see either of those as beneficial.

And i disagree with this. Pretty much everyone here has to work in a system whereby we provide APIs and we want to be able to evolve those APIs over time. Evolving APIs is basically impossible today with interfaces, even though we've had countless examples in our own codebase where we would have loved to just add a member to the interface, knowing precisely what the default behavior would be.

I see nothing wrong here as the contract has not changed in terms of what i have to actually implement. Those abstract interface members are still the contract that i must supply. The default members are simply a great way to then package functionality with the interface that you don't want to have to force every implementation to provide the same impl for, but which you want them to be able to specialize if they would find that valuable.

I mean heck. The moment we had this, we could simply write:

interface IEnumerable<T> : IEnumerable {
     IEnumerator<T> GetEnumerator();
     override IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

interface IEnumerator { 
    void Reset() { throw new UnsupportedOperationException(); }
    object Current { get; }
    bool MoveNext();
}

interface IEnumerator<T> : IEnumerator
{
    void Dispose() { }
    override object IEnumerator.Current => this.Current;    

    new T Current { get; }
}

Now, any implementation of IEnumerable<T> would only need to provide a single GetEnumerator method. And your IEnumerator<T> implementation would only need to provide implementations of MoveNext and Current. Everything else would be given a perfectly reasonable default implementation for you saving you from having to write this yourself every time you ever provide an IEnumerable implementation.

Grauenwolf commented 7 years ago

Today, all IEnumerable's effectively have a Count() on them already, thanks to the extension method in System.Linq.

Then why do we need this? A simple type check for an interface that overrides Count gives us the same net effect as overridable default methods.

CyrusNajmabadi commented 7 years ago

Then why do we need this?

I already stated why. Because Count() is inefficient in general because the extension method cannot specialize over all potential types it might be called on. Whereas if you have a default interface method, then all those types that implement IEnumerable can provide a specoalized implementation of Count() if they can. I literally wrote exactly that example higher above :)

CyrusNajmabadi commented 7 years ago

A simple type check for an interface that overrides Count gives us the same net effect as overridable default methods.

No.

// I'm not a list, or a read-only-collection.  This is in Cyrus.dll.
class MyType : MyBaseType, IEnumerable<T> {
    private int _length;

    public int Count() => _length;
}

// This is in System.Core.dll
public static class Enumerable
{
    public static int Count<T>(this IEnumerable<T> values)
    {
        int count = 0;
        foreach (var v in values)
        {
              count++;
        }
        return count;
    }
}

How does Enumerable.GetCount know how to optimize Count if i call it on an instance of MyType?

With default-interface-members, this would have been a case where you could use Count() uniformly on all IEnumerables. But every IEnumerable implementation could participate in providing a fast implementation.