Closed gafter closed 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?
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()
.
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.
@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.
@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.
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.
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.
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();
}
}
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".
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.
My thoughts on this idea:
protected
and internal
? What are the use cases for these complicating additions?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.
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.
Personally, I would much prefer an implementation of #164 rather than muddying interface definitions with concrete implementations.
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.
@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?
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.
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
.)
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:
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
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.
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 :)
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.
If this is implemented what would be the use case for extension methods over default methods?
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.
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.
: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.
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.
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#?
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.
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.
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.
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.
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.
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.
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.
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
.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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 :)
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.
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.
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
, andoverride
), as we will describe later, we propose that the default modifierspublic
andabstract
be permitted to be explicit as well. For clarity, we sometimes use these modifiers explicitly in examples of this feature.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.
A class that implements this interface need not implement its concrete method.
The final override for
IA.M
in classC
is the concrete methodM
declared inIA
. Note that a class does not inherit members from its interfaces; that is not changed by this feature: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 interfaceIf 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.
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 onIEnumerable
may have a much more efficient implementation on the interfaceIList
.A method declared in an interface is never treated as an
override
of another method unless it contains heoverride
modifier. This is necessary for compatibility.Reabstraction
A virtual (concrete) method declared in an interface may be overridden to be abstract in a derived interface
The
abstract
modifier is not required in the declaration ofIB.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 overrideM2
ifM1
is declared on typeT1
,M2
is declared on typeT2
, andT1
containsT2
among its direct or indirect interfaces. The most specific override is a unique override that is more specific than every other override.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
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.
static
andprivate
methodsBecause interfaces may now contain executable code, it is useful to abstract common code into private and static methods. We now permit these in interfaces.
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.Effect on existing programs
The rules presented here are intended to have no effect on the meaning of existing programs.
Example 1:
Example 2:
The same rules give similar results to the analogous situation involving default interface methods:
Further areas to be specified
/cc @dotnet/csharplangdesign @dotnet/roslyn-compiler