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

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

Closed
2 tasks
gafter opened this issue Mar 19, 2017 · 466 comments
Closed
2 tasks

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

gafter opened this issue Mar 19, 2017 · 466 comments

Comments

@gafter
Copy link
Contributor

@gafter gafter commented Mar 19, 2017

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

  • It would be useful to catalog the kinds of source and binary compatibility effects caused by adding default interface methods and overrides to existing interfaces.
  • We need to specify the runtime method resolution rules.

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

@gafter gafter self-assigned this Mar 19, 2017
@yaakov-h
Copy link
Member

@yaakov-h yaakov-h commented Mar 19, 2017

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?

Loading

@orthoxerox
Copy link

@orthoxerox orthoxerox commented Mar 19, 2017

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().

Loading

@AlgorithmsAreCool
Copy link

@AlgorithmsAreCool AlgorithmsAreCool commented Mar 19, 2017

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.

Loading

@gafter
Copy link
Contributor Author

@gafter gafter commented Mar 19, 2017

@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.

Loading

@gafter
Copy link
Contributor Author

@gafter gafter commented Mar 19, 2017

@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.

Loading

@HaloFour
Copy link
Contributor

@HaloFour HaloFour commented Mar 19, 2017

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.

Loading

@louthy
Copy link

@louthy louthy commented Mar 19, 2017

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.

Loading

@smoothdeveloper
Copy link

@smoothdeveloper smoothdeveloper commented Mar 19, 2017

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();
  }
}

Loading

@lloydjatkinson
Copy link

@lloydjatkinson lloydjatkinson commented Mar 19, 2017

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".

Loading

@Jorenkv
Copy link

@Jorenkv Jorenkv commented Mar 19, 2017

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.

Loading

@DavidArno
Copy link

@DavidArno DavidArno commented Mar 19, 2017

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?

Loading

@lloydjatkinson
Copy link

@lloydjatkinson lloydjatkinson commented Mar 19, 2017

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.

Loading

@ilexp
Copy link

@ilexp ilexp commented Mar 19, 2017

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.

Loading

@jmazouri
Copy link

@jmazouri jmazouri commented Mar 19, 2017

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

Loading

@mgarrettm
Copy link

@mgarrettm mgarrettm commented Mar 19, 2017

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.

Loading

@AlgorithmsAreCool
Copy link

@AlgorithmsAreCool AlgorithmsAreCool commented Mar 19, 2017

@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?

Loading

@CyrusNajmabadi
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Mar 19, 2017

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.

Loading

@HaloFour
Copy link
Contributor

@HaloFour HaloFour commented Mar 19, 2017

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.)

Loading

@CyrusNajmabadi
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Mar 19, 2017

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:

Loading

@CyrusNajmabadi
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Mar 19, 2017

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.

Loading

@CyrusNajmabadi
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Mar 19, 2017

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 :)

Loading

@AlgorithmsAreCool
Copy link

@AlgorithmsAreCool AlgorithmsAreCool commented Mar 19, 2017

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.

Loading

@Eirenarch
Copy link

@Eirenarch Eirenarch commented Mar 19, 2017

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

Loading

@CyrusNajmabadi
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Mar 19, 2017

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.

Loading

@CyrusNajmabadi
Copy link
Contributor

@CyrusNajmabadi CyrusNajmabadi commented Mar 19, 2017

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.

Loading

@jamesqo
Copy link
Contributor

@jamesqo jamesqo commented Mar 19, 2017

: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.

Loading

@yaakov-h
Copy link
Member

@yaakov-h yaakov-h commented Mar 19, 2017

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.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

I'm still not following. Can you show me an an example of what you mean?

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

Are you saying that every IEnumerable<T> type will need to contain a method table for every LINQ method defined as a DIM and that will be a problem or ?

Loading

@Thaina
Copy link

@Thaina Thaina commented Sep 3, 2019

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

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

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?

Loading

@YairHalberstadt
Copy link
Contributor

@YairHalberstadt YairHalberstadt commented Sep 3, 2019

@mikernet

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.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

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.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

The only additional classes generated will be for any DIM that an implementation of IEnumerable<T> decides to override...which will only happen if the type can optimize that operation, and that doesn't happen that often.

Loading

@YairHalberstadt
Copy link
Contributor

@YairHalberstadt YairHalberstadt commented Sep 3, 2019

@mikernet

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.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

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?).

Loading

@YairHalberstadt
Copy link
Contributor

@YairHalberstadt YairHalberstadt commented Sep 3, 2019

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.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

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.

Loading

@YairHalberstadt
Copy link
Contributor

@YairHalberstadt YairHalberstadt commented Sep 3, 2019

@mikernet

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

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

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.

Loading

@YairHalberstadt
Copy link
Contributor

@YairHalberstadt YairHalberstadt commented Sep 3, 2019

I think we'll have to ask someone who works on the JIT TBH.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

Yes agreed, I'm not 100% sure how this works.

Loading

@YairHalberstadt
Copy link
Contributor

@YairHalberstadt YairHalberstadt commented Sep 3, 2019

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).

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.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

LINQ extension methods also need an enumerator for every single T and every single method...so what's the difference?

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

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.

Loading

@HaloFour
Copy link
Contributor

@HaloFour HaloFour commented Sep 3, 2019

@Thaina

But now I see that if it for the sake of DIM we stop caring about this reasoning

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

Loading

@YairHalberstadt
Copy link
Contributor

@YairHalberstadt YairHalberstadt commented Sep 3, 2019

LINQ extension methods also need an enumerator for every single T and every single method...so what's the difference?

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.

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.

RTTI and method tables are duplicated for every class, even if method bodies aren't. It's described in the link above.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 3, 2019

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.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 4, 2019

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.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 4, 2019

It's too bad .NET didn't plan better and implement a collection design like C5 Collections. Such a beautifully designed collection library.

Loading

@yaakov-h
Copy link
Member

@yaakov-h yaakov-h commented Sep 4, 2019

yes, it's such a shame that .NET in 2002 and 2005 didn't implement a design similar to a library from 2011.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 4, 2019

@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.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 4, 2019

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 ;)

Loading

@yaakov-h
Copy link
Member

@yaakov-h yaakov-h commented Sep 4, 2019

@mikernet Microsoft figured that one out eventually with IReadOnlyList<T> in 2012, no?

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 4, 2019

Look at this magnificent interface hierarchy:

image

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? IList<T> implements ICollection<T> but Collection<T> implements IList<T>, so it's kinda a list, not a collection. It's just a jumbled mess.

Loading

@mikernet
Copy link

@mikernet mikernet commented Sep 4, 2019

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 IReadOnlyXXX<T> interfaces for optimizations (i.e. Last() only checks for IList<T> - why? I have no idea). The list of problems goes on and on.

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 NotSupportedException throws just to work sensibly anywhere indexing is required. Even ReadOnlyCollection<T> implements IList<T> 😆 😆 😆

Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet