-
Notifications
You must be signed in to change notification settings - Fork 1k
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
A Tour of Default Interface Methods for C# ("traits") #288
Comments
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 |
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. |
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". |
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:
|
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 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? |
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. |
As we've said on our home page:
|
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.
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. |
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.
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.
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 :) |
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? |
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 |
: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'm still not following. Can you show me an an example of what you mean? |
Are you saying that every |
I think I have frequently seen many feature request got negative response with the argument about "encourage bad coder" For example, my request dotnet/roslyn#14143 But now I see that if it for the sake of DIM we stop caring about this reasoning |
I don't know what the CLR implementation of DIM is...if every type's method table adds an entry for every single DIM that applies to it then I can maybe see this being an issue, but even then I doubt we are talking about a significant amount of space. Is that what it does or does it fall back to the interface's method table if the type doesn't contain an entry? |
When you write an IEnumerable a compiler creates a class to use as the enumerator. That class will be instantiated by the JIT the moment that it is first used. If the Linq methods are places in the IEnumerable class, I believe (but am not sure) that this will be the first time you instantiate a class implementing IEnumerable. This means that if you instantiate IEnumerable for 100 types as T, and there are 100 Linq methods, the Jit will instantiate 10000 classes, which may heavily impact performance. I don't know if this is the case, and it could definitely be solved by the JIT. |
I can't see how this will be any different than what it currently does with extension methods. Either way each operation needs an enumerator class for every T. |
The only additional classes generated will be for any DIM that an implementation of |
Joe Duffy discusses all this in more detail here: http://joeduffyblog.com/2011/10/23/on-generics-and-some-of-the-associated-overheads/ It's not quite that simple. In short he moved back to extension methods from instance precisely to improve performance. |
Yes I understand how all that works, but I don't think that it is clear that more types have to be generated with a DIM based LINQ implementation. DIMs are, after all, ultimately compiled down to static methods kind of like extension methods (I believe?). |
Do you mean by the Roslyn compiler? Because DIMs rely on runtime changes, not clever compiling. I am not sure how the JIT works with them. |
I'm almost certain the actual DIM implementations go into a static class in the IL. Yes it requires runtime changes, but ultimately it is a method in a static class. |
I've just checked in ILSpy, and that doesn't appear to be the case. The IL for the method is compiled directly into the interface method declaration |
Either way, the method IL is not "copied" into every single class, it is compiled once per T for the interface, not once per T for every single class that implements the interface (I think), same way a static class is compiled once for every T of the static class. |
I think we'll have to ask someone who works on the JIT TBH. |
Yes agreed, I'm not 100% sure how this works. |
The issue isn't the compilation of the method IL, but the instantiation of all the enumerators. This is indeed once per T, but T is probably in the thousands for large applications, and given the number of Linq methods that could be a lot of classes. |
LINQ extension methods also need an enumerator for every single T and every single method...so what's the difference? |
Also ref type Ts share code so it's only for value types and I highly doubt there are many (if any) applications that use thousands of value type T IEnumerables. |
Data will always win over speculation. There are years worth of evidence as to how a language is used when a feature like DIM is added, none of which bears out this theory that DIM results in encouraging bad code or creating a pit of failure. Developers will have to go out of their way to find and (ab)use the feature. There are decades of evidence with programming languages that lack structure or allow optional structure as to what happens with all but the most trivial of applications. Because it is the easier path the developer will automatically gravitate towards it, thus that needs to be considered significantly more carefully. There is a championed proposal to explore this, though: #2765 |
Generic extension methods are only instantiated on first use of that method. Non generic nested classes are instantiated on first use of the parent class.
RTTI and method tables are duplicated for every class, even if method bodies aren't. It's described in the link above. |
Yeah, I see what you're saying. I don't know if the method table for all DIM for all interfaces a type implements but doesn't override is duplicated for each type as well but I imagine they probably are...but I imagine it could also just fall back to interface implementation if the entry isn't found so I'm not sure. That would be ideal for a case like this. I still think that internally from a JIT perspective DIM implementations are treated like static methods and will only be JIT on first call like a static method but I could be entirely wrong about that. That's kind of why they decided to allow static interface members if I recall correctly...it's already essentially a static class so might as well allow it. That's also why instance fields can't be added but static fields can. |
It's worth noting that only logically optimizable LINQ methods that can't sensibly be optimized by implementing a collection interface for all collection types need to be added as DIMs. From the list of LINQ methods it seems that good candidates are: Reverse, ElementAt, Last, TakeLast and Skip. The rest are already well optimized by just using IEnumerable or checking for a couple other interface implementations. That's not a very long list of DIMs. We only need DIMs for the methods that potentially iterate through an entire collection needlessly when a highly optimized alternative can be provided. I think that takes care of the issue @YairHalberstadt raised above. |
It's too bad .NET didn't plan better and implement a collection design like C5 Collections. Such a beautifully designed collection library. |
yes, it's such a shame that .NET in 2002 and 2005 didn't implement a design similar to a library from 2011. |
@yaakov-h The date of the library isn't the issue, it's that it follows good interface design principles in the way the hierarchy is setup, in particular following the Interface Segregation Principle. That design principle predates 2002 by a very long time. |
It doesn't take a time traveler to know that people might want to design a collection that can count and index items but not add or remove items and have a suitable interface to represent that ;) |
@mikernet Microsoft figured that one out eventually with |
Look at this magnificent interface hierarchy: Pretty much every collection capability is easily and accurately represented in there. The "read only indexed countable" collection was a bad example since C5 doesn't really have a full read-only interface hierarchy...it's been many years since I used C5...but the way the interfaces are split makes it very easy to demand or determine collection capabilities and figure out the time complexity of an operation on a given collection to make optimization decisions when doing stuff with them. There's an interface to represent sorted collections...or enumerables that can efficiently enumerate backwards...or efficiently search for an item...etc, and you can combine them many ways to properly express the capabilities of a collection. Unfortunately there were too many interop issues with BCL and third party libraries when using C5 as they weren't designed for those collections so we ended up giving up on that route. The read only interfaces in the BCL are annoying because the read-write versions of the interfaces don't inherit from them, so you end up casting everywhere if you are programming against interfaces. ObservableCollection is indexable and implements IList...so why isn't it ObservableList? |
Even the BCL doesn't properly support read-only collections. For example, List and Dictionary constructors don't optimize for source collections of IReadOnlyCollection or IReadOnlyDictionary......they only check for ICollection and IDictionary implementations. And that often makes very large performance differences so it just isn't even worth using read only collections. I just end up having to implement the full IList / IDictionary interface anyway to optimize these scenarios. WPF bindings also only support IList and not IReadOnlyList. Even LINQ doesn't support A well thought out interface hierarchy from the start which follows the interface segregation principle would have allowed you to program against the lowest common denominator of interface functionality that you need from a collection for a given task, but now every single collection has to implement IList with boat loads of |
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
The text was updated successfully, but these errors were encountered: