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

Issues in Default Interface Methods #406

Open
gafter opened this Issue Apr 4, 2017 · 67 comments

Comments

Projects
None yet
@gafter
Member

gafter commented Apr 4, 2017

The following notes are intended to prime the pump for LDM discussion as soon as the LDM has time to take up these issues.

Issues in Default Interface Methods

Event accessors

Closed Issue: Can an event be overridden "piecewise"?

Consider this case:

public interface I1
{
    event T e1;
}
public interface I2 : I1
{
    override event T
    {
        add { }
        // error: "remove" accessor missing
    } 
}

This "partial" implementation of the event is not permitted because, as in a class, the syntax for an event declaration does not permit only one accessor; both (or neither) must be provided. You could accomplish the same thing by permitting the abstract remove accessor in the syntax to be implicitly abstract by the absence of a body:

public interface I1
{
    event T e1;
}
public interface I2 : I1
{
    override event T
    {
        add { }
        remove; // implicitly abstract
    } 
}

Note that this is a new (proposed) syntax. In the current grammar, event accessors have a mandatory body.

Closed Issue: Can an event accessor be (implicitly) abstract by the omission of a body, similarly to the way that methods in interfaces and property accessors are (implicitly) abstract by the omission of a body?

Decision: (2017-04-18) No, event declarations require both concrete accessors (or neither).

Reabstraction in a Class

Closed Issue: We should confirm that this is permitted (otherwise adding a default implementation would be a breaking change):

interface I1
{
    void M() { }
}
abstract class C : I1
{
    public abstract void M(); // implement I1.M with an abstract method in C
}

Decision: (2017-04-18) Yes, adding a body to an interface member declaration shouldn't break C.

Sealed Override

The previous question implicitly assumes that the sealed modifier can be applied to an override in an interface. This contradicts the draft specification. Do we want to permit sealing an override? Source and binary compatibility effects of sealing should be considered.

Closed Issue: Should we permit sealing an override?

Decision: (2017-04-18) Let's not allowed sealed on overrides in interfaces. The only use of sealed on interface members is to make them non-virtual in their initial declaration.

Diamond inheritance and classes

The draft of the proposal prefers class overrides to interface overrides in diamond inheritance scenarios:

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. The most specific override is a unique override that is more specific than every other override. 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 either

  1. T1 contains T2 among its direct or indirect interfaces, or
  2. T2 is an interface type but T1 is not an interface type.

The scenario is this

interface IA
{
    void M();
}
interface IB : IA
{
    override void M() { WriteLine("IB"); }
}
class Base : IA
{
    void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
    static void Main()
    {
        Ia a = new Derived();
        a.M();           // what does it do?
    }
}

We should confirm this behavior (or decide otherwise)

Closed Issue: Confirm the draft spec, above, for most specific override as it applies to mixed classes and interfaces (a class takes priority over an interface). See https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#diamonds-with-classes

Interface methods vs structs

There are some unfortunate interactions between default interface methods and structs.

interface IA
{
    public void M() { }
}
struct S : IA
{
}

Note that interface members are not inherited:

var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'

Consequently, the client must box the struct to invoke interface methods

IA s = default(S); // an S, boxed
s.M(); // ok

Boxing in this way defeats the principal benefits of a struct type. Moreover, any mutation methods will have no apparent effect, because they are operating on a boxed copy of the struct:

interface IB
{
    public void Increment() { P += 1; }
    public int P { get; set; }
}
struct T : IB
{
    public int P { get; set; } // auto-property
}

T t = default(T);
Console.WriteLine(t.P); // prints 0
(t as IB).Increment();
Console.WriteLine(t.P); // prints 0

Closed Issue: What can we do about this:

  1. Forbid a struct from inheriting a default implementation. All interface methods would be treated as abstract in a struct. Then we may take time later to decide how to make it work better.
  2. Come up with some kind of code generation strategy that avoids boxing. Inside a method like IB.Increment, the type of this would perhaps be akin to a type parameter constrained to IB. In conjunction with that, to avoid boxing in the caller, non-abstract methods would be inherited from interfaces. This may increase compiler and CLR implementation work substantially.
  3. Not worry about it and just leave it as a wart.
  4. Other ideas?

Decision: Not worry about it and just leave it as a wart. See https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#structs-and-default-implementations

Base interface invocations

The draft spec suggests a syntax for base interface invocations inspired by Java: Interface.base.M(). We need to select a syntax, at least for the initial prototype. My favorite is base<Interface>.M().

Closed Issue: What is the syntax for a base member invocation?

Decision: The syntax is base(Interface).M(). See https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-19.md#base-invocation. The interface so named must be a base interface, but does not need to be a direct base interface.

Open Issue: Should base interface invocations be permitted in class members?

Overriding non-public interface members

In an interface, non-public members from base interfaces are overridden using the override modifier. If it is an "explicit" override that names the interface containing the member, the access modifier is omitted.

Closed Issue: If it is an "implicit" override that does not name the interface, does the access modifier have to match?

Decision: Only public members may be implicitly overridden, and the access must match. See https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list

Open Issue: Is the access modifier required, optional, or omitted on an explicit override such as override void IB.M() {}?

Open Issue: Is override required, optional, or omitted on an explicit override such as void IB.M() {}?

How does one implement a non-public interface member in a class? Perhaps it must be done explicitly?

interface IA
{
    internal void MI();
    protected void MP();
}
class C : IA
{
    // are these implementations?
    internal void MI() {}
    protected void MP() {}
}

Closed Issue: How does one implement a non-public interface member in a class?

Decision: You can only implement non-public interface members explicitly. See https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-04-18.md#dim-implementing-a-non-public-interface-member-not-in-list

Binary Compatibility 2

Consider the following code in which each type is in a separate assembly

interface I1
{
    void M() { Impl1 }
}
interface I2 : I1
{
    override void M() { Impl2 }
}
interface I3 : I1
{
}
class C : I2, I3
{
}

We understand that the implementation of I1.M in C is I2.M. What if the assembly containing I3 is changed as follows and recompiled

interface I3 : I1
{
    override void M() { Impl3 }
}

but C is not recompiled. What happens when the program is run? An invocation of (C as I1).M()

  1. Runs I1.M
  2. Runs I2.M
  3. Runs I3.M
  4. Either 2 or 3, deterministically
  5. Throws some kind of runtime exception

Permit partial in interface?

Given that interfaces may be used in ways analogous to the way abstract classes are used, it may be useful to declare them partial. This would be particularly useful in the face of generators.

Proposal: Remove the language restriction that interfaces and members of interfaces may not be declared partial.

Main in an interface?

Open Issue: Is a static Main method in an interface a candidate to be the program's entry point?

Confirm intent to support public non-virtual methods

Can we please confirm (or reverse) our decision to permit non-virtual public methods in an interface?

interface IA
{
    public sealed void M() { }
}

Semi-Closed Issue: (2017-04-18) We think it is going to be useful, but will come back to it. This is a mental model tripping block.

Does an override in an interface introduce a new member?

There are a few ways to observe whether an override declaration introduces a new member or not.

interface IA
{
    void M(int x) { }
}
interface IB
{
    override void M(int y) { }
}
interface IC
{
    static void M2()
    {
        M(y: 3); // permitted?
    }
    override void IB.M(int z) { } // permitted? What does it override?
}

Open Issue: Does an override declaration in an interface introduce a new member?

In a class, an overriding method is "visible" in some senses. For example, the names of its parameters take precedence over the names of parameters in the overridden method. It may be possible to duplicate that behavior in interfaces, as there is always a most specific override. But do we want to duplicate that behavior?

Also, it is possible to "override" an override method?

Properties with a private accessor

We say that private members are not virtual, and the combination of virtual and private is disallowed. But what about a property with a private accessor?

interface IA
{
    public virtual int P
    {
        get => 3;
        private set => { } 
    }
}

Is this allowed? Is the set accessor here virtual or not? Can it be overridden where it is accessible? Does the following implicitly implement only the get accessor?

class C : IA
{
    public int P
    {
        get => 4;
        set { }
    }
}

Is the following presumably an error because IA.P.set isn't virtual and also because it isn't accessible?

class C : IA
{
    int IA.P
    {
        get => 4;
        set { }
    }
}

Base Interface Invocations, round 2

Our previous "resolution" to how to handle base invocations doesn't actually provide sufficient expressiveness. It turns out that in C# and the CLR, unlike Java, you need to specify both the interface containing the method declaration and the location of the implementation you want to invoke.

I propose the following syntax for base calls in interfaces. I’m not in love with it, but it illustrates what any syntax must be able to express:

interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I4 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3, I4
{
    void I1.M()
    {
        base<I3>(I1).M(); // calls I3's implementation of I1.M
        base<I4>(I1).M(); // calls I4's implementation of I1.M
    }
    void I2.M()
    {
        base<I3>(I2).M(); // calls I3's implementation of I2.M
        base<I4>(I2).M(); // calls I4's implementation of I2.M
    }
}

If there is no ambiguity, you can write it more simply

interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I4 : I1 { void I1.M() { } }
interface I5 : I3, I4
{
    void I1.M()
    {
        base<I3>.M(); // calls I3's implementation of I1.M
        base<I4>.M(); // calls I4's implementation of I1.M
    }
}

Or

interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3
{
    void I1.M()
    {
        base(I1).M(); // calls I3's implementation of I1.M
    }
    void I2.M()
    {
        base(I2).M(); // calls I3's implementation of I2.M
    }
}

Or

interface I1 { void M(); }
interface I3 : I1 { void I1.M() { } }
interface I5 : I3
{
    void I1.M()
    {
        base.M(); // calls I3's implementation of I1.M
    }
}
@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli Apr 5, 2017

For the virtual issue: It is essentially to choose between being consistent with interface style or class style. If we were to use this feature lightly, where we would typically only add a few default implementations, then the interface style makes more sense. But if we were to put a lot of logic (including private methods) into interfaces, the class style makes more sense. IMO, the later case will happen more as time goes.

Another idea (maybe already raised by others) is to actually put the class stuff into a class, e.g.:

interface I
{
  void M();

  default class C : I
  {
    public virtual void M() { ... }
  }
}

So no need for a lot of new rules to learn and remember.

qrli commented Apr 5, 2017

For the virtual issue: It is essentially to choose between being consistent with interface style or class style. If we were to use this feature lightly, where we would typically only add a few default implementations, then the interface style makes more sense. But if we were to put a lot of logic (including private methods) into interfaces, the class style makes more sense. IMO, the later case will happen more as time goes.

Another idea (maybe already raised by others) is to actually put the class stuff into a class, e.g.:

interface I
{
  void M();

  default class C : I
  {
    public virtual void M() { ... }
  }
}

So no need for a lot of new rules to learn and remember.

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli Apr 5, 2017

For struct: I don't think the boxing need to be solved here. It is a more general issue with struct, and we have lived with it til now. The only issue is I expect

var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'

to work. But if it is hard, I can also live with 1. Forbid a struct from inheriting a default implementation

qrli commented Apr 5, 2017

For struct: I don't think the boxing need to be solved here. It is a more general issue with struct, and we have lived with it til now. The only issue is I expect

var s = default(S);
s.M(); // error: 'S' does not contain a member 'M'

to work. But if it is hard, I can also live with 1. Forbid a struct from inheriting a default implementation

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Apr 5, 2017

Member

I've added more open issues to the OP, starting with "Base interface invocations"

Member

gafter commented Apr 5, 2017

I've added more open issues to the OP, starting with "Base interface invocations"

@orthoxerox

This comment has been minimized.

Show comment
Hide comment
@orthoxerox

orthoxerox Apr 5, 2017

Open Issue: What is the syntax for a base member invocation?

IFoo.base.M() works, but makes it look like you're calling the base of IFoo, not IFoo as the base type. I propose to use base<IFoo>.M().

orthoxerox commented Apr 5, 2017

Open Issue: What is the syntax for a base member invocation?

IFoo.base.M() works, but makes it look like you're calling the base of IFoo, not IFoo as the base type. I propose to use base<IFoo>.M().

@orthoxerox

This comment has been minimized.

Show comment
Hide comment
@orthoxerox

orthoxerox Apr 5, 2017

Open Issue: Should a concrete method without implementation be implicitly virtual?

The question is confusing (or I am too tired), but I think it's supposed to say "with an implementation". I think requiring an explicit modifier on all default methods is the better option. A "plain old method" would then be sealed without an override. "Implicitly virtual" makes default methods work like regular interface methods, but unlike class methods. "Implicitly plain old" makes them work like class methods, but unlike regular interface methods. Both options are potentially confusing.

Requiring a modifier forces the programmer to make an explicit choice.

orthoxerox commented Apr 5, 2017

Open Issue: Should a concrete method without implementation be implicitly virtual?

The question is confusing (or I am too tired), but I think it's supposed to say "with an implementation". I think requiring an explicit modifier on all default methods is the better option. A "plain old method" would then be sealed without an override. "Implicitly virtual" makes default methods work like regular interface methods, but unlike class methods. "Implicitly plain old" makes them work like class methods, but unlike regular interface methods. Both options are potentially confusing.

Requiring a modifier forces the programmer to make an explicit choice.

@gafter gafter referenced this issue Apr 5, 2017

Open

Champion "default interface methods" #52

3 of 5 tasks complete
@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour Apr 5, 2017

Contributor

@orthoxerox

That syntax feels very Java. Maybe base(IFoo).M ()?

Contributor

HaloFour commented Apr 5, 2017

@orthoxerox

That syntax feels very Java. Maybe base(IFoo).M ()?

@gafter gafter changed the title from 2017-04-05 Issues in Default Interface Methods to Issues in Default Interface Methods Apr 5, 2017

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Apr 6, 2017

Member

Added some resolutions from today's LDM, including

Closed Issue: Should a concrete method (with implementation) be implicitly virtual? [YES]

Member

gafter commented Apr 6, 2017

Added some resolutions from today's LDM, including

Closed Issue: Should a concrete method (with implementation) be implicitly virtual? [YES]

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Apr 6, 2017

Member

I like the syntax base<IFoo>.M()

Member

gafter commented Apr 6, 2017

I like the syntax base<IFoo>.M()

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Apr 6, 2017

Contributor

Why this ((IFoo)this).M(); can't work though? It worked for classes so far.

Contributor

alrz commented Apr 6, 2017

Why this ((IFoo)this).M(); can't work though? It worked for classes so far.

@yaakov-h

This comment has been minimized.

Show comment
Hide comment
@yaakov-h

yaakov-h Apr 6, 2017

Contributor

Wouldn't that just cause a stack overflow, if it's virtual?

Contributor

yaakov-h commented Apr 6, 2017

Wouldn't that just cause a stack overflow, if it's virtual?

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli Apr 7, 2017

@alrz Because you want to invoke the base interface's default implementation rather than the current override implementation.

qrli commented Apr 7, 2017

@alrz Because you want to invoke the base interface's default implementation rather than the current override implementation.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz Apr 7, 2017

Contributor

Nevermind, apparently it doesn't work that way.

I see base(T) has been chosen cause it works like default(T). That's great.

Contributor

alrz commented Apr 7, 2017

Nevermind, apparently it doesn't work that way.

I see base(T) has been chosen cause it works like default(T). That's great.

@zippec

This comment has been minimized.

Show comment
Hide comment
@zippec

zippec Apr 9, 2017

Regarding base interface invocation, I'm not a fan base<IFoo>.M() syntax. There is no generic involved so why make it look like one? Its especially weird with generic interface base<IEnumerable<int>>.Count().

base(IFoo).M() looks much more in-home in C# next to typeof(), default() and nameof()

Regarding diamond inheritance

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 either

  1. T1 contains T2 among its direct or indirect interfaces, or
  2. T2 is an interface type but T1 is not an interface type.

I don't think that the second rule is useful. It's better to require derived class to implement explicit override.

Regarding Binary Compatibility issues, they are very similar to concerns raised in #409. Following existing C# logic for base invocation, Binary Compatibility 1 should (although probably unintuitively) run I1.M() and Binary Compatibility 2 should run I2.M().

zippec commented Apr 9, 2017

Regarding base interface invocation, I'm not a fan base<IFoo>.M() syntax. There is no generic involved so why make it look like one? Its especially weird with generic interface base<IEnumerable<int>>.Count().

base(IFoo).M() looks much more in-home in C# next to typeof(), default() and nameof()

Regarding diamond inheritance

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 either

  1. T1 contains T2 among its direct or indirect interfaces, or
  2. T2 is an interface type but T1 is not an interface type.

I don't think that the second rule is useful. It's better to require derived class to implement explicit override.

Regarding Binary Compatibility issues, they are very similar to concerns raised in #409. Following existing C# logic for base invocation, Binary Compatibility 1 should (although probably unintuitively) run I1.M() and Binary Compatibility 2 should run I2.M().

@jnm2

This comment has been minimized.

Show comment
Hide comment
@jnm2

jnm2 Apr 9, 2017

Contributor

Can I put a request in here for calling this(IInterface).ExplicitGetterOnlyAutoProperty = value (non-virtual) and base(IInterface).ReimplementedMethod(), in C# 7 classes implementing C# 7 interfaces? These were always pain points.

Oops, wrong thread. Moved to #52 (comment)

Contributor

jnm2 commented Apr 9, 2017

Can I put a request in here for calling this(IInterface).ExplicitGetterOnlyAutoProperty = value (non-virtual) and base(IInterface).ReimplementedMethod(), in C# 7 classes implementing C# 7 interfaces? These were always pain points.

Oops, wrong thread. Moved to #52 (comment)

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Apr 9, 2017

Member

@zippec

I don't think that the second rule is useful. It's better to require derived class to implement explicit override.

That would make it an incompatible change to add default methods to interfaces, because it would change the behavior of this program

interface IA
{
    void M();
}
interface IB : IA
{
}
class Base : IA
{
    void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
    static void Main()
    {
        Ia a = new Derived();
        a.M();
    }
}

when a default method implementation is added like this

interface IA
{
    void M();
}
interface IB : IA
{
    override void M() { WriteLine("IB"); }
}
class Base : IA
{
    void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
    static void Main()
    {
        Ia a = new Derived();
        a.M();           // what does it do?
    }
}

Since we don't want the addition of default methods to an interface to affect the meaning of existing correct programs that do not use default methods, we need the second rule.

See the open question "Diamond inheritance and classes".

Member

gafter commented Apr 9, 2017

@zippec

I don't think that the second rule is useful. It's better to require derived class to implement explicit override.

That would make it an incompatible change to add default methods to interfaces, because it would change the behavior of this program

interface IA
{
    void M();
}
interface IB : IA
{
}
class Base : IA
{
    void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
    static void Main()
    {
        Ia a = new Derived();
        a.M();
    }
}

when a default method implementation is added like this

interface IA
{
    void M();
}
interface IB : IA
{
    override void M() { WriteLine("IB"); }
}
class Base : IA
{
    void IA.M() { WriteLine("Base"); }
}
class Derived : Base, IB // allowed?
{
    static void Main()
    {
        Ia a = new Derived();
        a.M();           // what does it do?
    }
}

Since we don't want the addition of default methods to an interface to affect the meaning of existing correct programs that do not use default methods, we need the second rule.

See the open question "Diamond inheritance and classes".

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Apr 19, 2017

Member

The following draft notes from 2017-04-19 LDM have been partially reflected in the open issue list and specification:


DIM Event accessors

Today, syntactically, either both or neither accessor can have an implementation.

Should we allow just one to be specified? Overridden?

Conclusion

If you have only one, you probably have a bug. Let's not allow it. Not blocked for future idiots to ...

DIM Reabstraction

Yes, adding a body to an interface member declaration shouldn't break C.

DIM Sealed override

Should it be allowed? would it prevent overrides in classes or only in interfaces?

It seems odd to prevent either. Also, it is weird in connection with diamond inheritance: what if one branch is sealed?

Conclusion

Let's not allowed sealed on overrides in interfaces. The only use of sealed on interface members is to make them non-virtual in their initial declaration.

DIM sealed members (not on list)

Some folks in the community find it weird, and that they look too much like things that can be implemented in classes.

Conclusion

We think it is going to be useful, but will come back to it. This is a mental model tripping block.

DIM not quite implementing a member (not int list)

You have a member and implement an interface. The interface adds a new member with a default implementation, that looks like your method but doesn't quite make it an implementation. Bug? Intentional? We can't provide a warning, because it would assume it was a bug.

Conclusion

Can't do anything about this.

DIM implementing inaccessible interface members (not in list)

The way the runtime works today, a class member can happily implement an interface member that isn't accessible! That's not likely to be dependent on today (no language will generate that interface), but we need to decide what semantics to have here.

We could continue to only have public members in interfaces be virtual. But if we want protected, internal and private, we should probably have it so they can only be implemented by classes that can see them. But this means that interfaces can prevent other assemblies from implementing them! This may be a nice feature - it allows closed sets of implementations.

Conclusion

This is still open, but our current stake in the ground is we should allow non-public virtual interface members, but disallow overriding or implementing them in places where they are not accessible.

DIM Implementing a non-public interface member (not in list)

Would we allow them to be implemented implicitly? If so, what is required of the accessibility of the implementing method?:

  • Must be public
  • Must be the same accessibility
  • Must be at least as accessible

Conclusion

For now, let's not allow any of these. We can relax as we think through it.

Member

gafter commented Apr 19, 2017

The following draft notes from 2017-04-19 LDM have been partially reflected in the open issue list and specification:


DIM Event accessors

Today, syntactically, either both or neither accessor can have an implementation.

Should we allow just one to be specified? Overridden?

Conclusion

If you have only one, you probably have a bug. Let's not allow it. Not blocked for future idiots to ...

DIM Reabstraction

Yes, adding a body to an interface member declaration shouldn't break C.

DIM Sealed override

Should it be allowed? would it prevent overrides in classes or only in interfaces?

It seems odd to prevent either. Also, it is weird in connection with diamond inheritance: what if one branch is sealed?

Conclusion

Let's not allowed sealed on overrides in interfaces. The only use of sealed on interface members is to make them non-virtual in their initial declaration.

DIM sealed members (not on list)

Some folks in the community find it weird, and that they look too much like things that can be implemented in classes.

Conclusion

We think it is going to be useful, but will come back to it. This is a mental model tripping block.

DIM not quite implementing a member (not int list)

You have a member and implement an interface. The interface adds a new member with a default implementation, that looks like your method but doesn't quite make it an implementation. Bug? Intentional? We can't provide a warning, because it would assume it was a bug.

Conclusion

Can't do anything about this.

DIM implementing inaccessible interface members (not in list)

The way the runtime works today, a class member can happily implement an interface member that isn't accessible! That's not likely to be dependent on today (no language will generate that interface), but we need to decide what semantics to have here.

We could continue to only have public members in interfaces be virtual. But if we want protected, internal and private, we should probably have it so they can only be implemented by classes that can see them. But this means that interfaces can prevent other assemblies from implementing them! This may be a nice feature - it allows closed sets of implementations.

Conclusion

This is still open, but our current stake in the ground is we should allow non-public virtual interface members, but disallow overriding or implementing them in places where they are not accessible.

DIM Implementing a non-public interface member (not in list)

Would we allow them to be implemented implicitly? If so, what is required of the accessibility of the implementing method?:

  • Must be public
  • Must be the same accessibility
  • Must be at least as accessible

Conclusion

For now, let's not allow any of these. We can relax as we think through it.

@BreyerW

This comment has been minimized.

Show comment
Hide comment
@BreyerW

BreyerW Apr 19, 2017

I have a question about unfortunate interaction with default members vs structs: How this (will) work with generics constrained to interface with default members? Lets me show example:

interface IDumb{

public virtual void Talk(){
Console.WriteLine("I'm here!");
}

}
struct DumbStruct : IDumb{

}
....
public void ConsumeDumb<T>(T item) where T : IDumb
{
item.Talk(); 
}
....
ConsumeDumb(new DumbStruct()); //print "I'm here!" or throw error?

The thing is, as far as i understand, generic constraints avoid boxing on value types and because of that default members should work flawlessly. But i would like to be sure.

If thats true then i vote for 2->3 ------------------------------>1 in that order (from best to worst) because default members will still be useful on structs even if you decide to leave it as-is (without forbidding nor providing code generator).

Otherwise i still vote on 2 but 3 and 1 become effectively same.

BreyerW commented Apr 19, 2017

I have a question about unfortunate interaction with default members vs structs: How this (will) work with generics constrained to interface with default members? Lets me show example:

interface IDumb{

public virtual void Talk(){
Console.WriteLine("I'm here!");
}

}
struct DumbStruct : IDumb{

}
....
public void ConsumeDumb<T>(T item) where T : IDumb
{
item.Talk(); 
}
....
ConsumeDumb(new DumbStruct()); //print "I'm here!" or throw error?

The thing is, as far as i understand, generic constraints avoid boxing on value types and because of that default members should work flawlessly. But i would like to be sure.

If thats true then i vote for 2->3 ------------------------------>1 in that order (from best to worst) because default members will still be useful on structs even if you decide to leave it as-is (without forbidding nor providing code generator).

Otherwise i still vote on 2 but 3 and 1 become effectively same.

@MI3Guy

This comment has been minimized.

Show comment
Hide comment
@MI3Guy

MI3Guy Apr 19, 2017

@gafter

With regard to non-public overrides, isn't this what the strict modifier in IL is supposed to control? I was under the impression that without that any inaccessible method could be overridden at the IL level.

MI3Guy commented Apr 19, 2017

@gafter

With regard to non-public overrides, isn't this what the strict modifier in IL is supposed to control? I was under the impression that without that any inaccessible method could be overridden at the IL level.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Apr 19, 2017

Member

How this (will) work with generics constrained to interface with default members?

That is the essence of the open question. Inside the interface's default method, the this parameter is of an interface type. How could that be unless the receiver was boxed? If it was boxed, then the caller had no way to avoid boxing.

@MI3Guy I don't know anything about that. If your point is that our choice on how this works may affect the IL we elect to emit, I believe that.

Member

gafter commented Apr 19, 2017

How this (will) work with generics constrained to interface with default members?

That is the essence of the open question. Inside the interface's default method, the this parameter is of an interface type. How could that be unless the receiver was boxed? If it was boxed, then the caller had no way to avoid boxing.

@MI3Guy I don't know anything about that. If your point is that our choice on how this works may affect the IL we elect to emit, I believe that.

@MI3Guy

This comment has been minimized.

Show comment
Hide comment
@MI3Guy

MI3Guy Apr 19, 2017

I can't say that I've ever actually heard anyone talk about the strict Method Attribute, but it shows up in the CLR spec (II.15.4.2.2, not sure if there's a better reference) and that's how I understood what it is supposed to do.

MI3Guy commented Apr 19, 2017

I can't say that I've ever actually heard anyone talk about the strict Method Attribute, but it shows up in the CLR spec (II.15.4.2.2, not sure if there's a better reference) and that's how I understood what it is supposed to do.

@Thaina

This comment has been minimized.

Show comment
Hide comment
@Thaina

Thaina Apr 26, 2017

There are many things extension method would not have a problem with

For instance. default method for struct could make it as generic constraint interface

public static class Ext
{
    public static void M<T>(this T obj) where T : ISomeThing
    {}
}

struct S : ISomeThing {}

S s;
s.M(); // not boxed

Thaina commented Apr 26, 2017

There are many things extension method would not have a problem with

For instance. default method for struct could make it as generic constraint interface

public static class Ext
{
    public static void M<T>(this T obj) where T : ISomeThing
    {}
}

struct S : ISomeThing {}

S s;
s.M(); // not boxed
@paulomorgado

This comment has been minimized.

Show comment
Hide comment
@paulomorgado

paulomorgado May 7, 2017

Regarding base interface member access, what's wrong with this?

interface I0
{
   void M() { Console.WriteLine("I0"); }
}
interface I1 : I0
{
   override void M() { Console.WriteLine("I1"); }
}
interface I2 : I0
{
   override void M() { Console.WriteLine("I2"); }
}
interface I3 : I1, I2
{
   // an explicit override that invoke's a base interface's default method
   void I0.M() { I2.M(); }
}

I2.base looks like accessing the base of I2 and the use of <> without generics being involved seems awkward.

paulomorgado commented May 7, 2017

Regarding base interface member access, what's wrong with this?

interface I0
{
   void M() { Console.WriteLine("I0"); }
}
interface I1 : I0
{
   override void M() { Console.WriteLine("I1"); }
}
interface I2 : I0
{
   override void M() { Console.WriteLine("I2"); }
}
interface I3 : I1, I2
{
   // an explicit override that invoke's a base interface's default method
   void I0.M() { I2.M(); }
}

I2.base looks like accessing the base of I2 and the use of <> without generics being involved seems awkward.

@alrz

This comment has been minimized.

Show comment
Hide comment
@alrz

alrz May 7, 2017

Contributor

@paulomorgado I read in a design note that base(T).M() is the winner as it works like default(T).

Contributor

alrz commented May 7, 2017

@paulomorgado I read in a design note that base(T).M() is the winner as it works like default(T).

@gafter gafter self-assigned this May 21, 2018

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour May 22, 2018

Contributor

So:

  1. base(I1).M() refers to the interface in which the member slot is initially declared, and
  2. base<I3>.M() refers to the interface on which that member slot has a default implementation

Do I have that right?

The syntax feels a little kludgy, but I would hope that the situation requiring both would be relatively infrequent. Probably infrequent enough that I'd have to poke into the docs (or trial&error) any time it came up.

How about some other options?

  1. base<I3>(I1).M(); - what you have above
  2. base(I1)<I3>.M(); - reverse the two
  3. base<I1>(I3).M(); - same syntax as above but reverse what <> and () mean
  4. base(I1<I3>).M(); - confused with a generic base interface?
Contributor

HaloFour commented May 22, 2018

So:

  1. base(I1).M() refers to the interface in which the member slot is initially declared, and
  2. base<I3>.M() refers to the interface on which that member slot has a default implementation

Do I have that right?

The syntax feels a little kludgy, but I would hope that the situation requiring both would be relatively infrequent. Probably infrequent enough that I'd have to poke into the docs (or trial&error) any time it came up.

How about some other options?

  1. base<I3>(I1).M(); - what you have above
  2. base(I1)<I3>.M(); - reverse the two
  3. base<I1>(I3).M(); - same syntax as above but reverse what <> and () mean
  4. base(I1<I3>).M(); - confused with a generic base interface?
@jnm2

This comment has been minimized.

Show comment
Hide comment
@jnm2

jnm2 May 22, 2018

Contributor

Reminiscent of the LanguageUtils.ExplicitImplementation<TheBaseClass, ISomeInterface>(this).Foo(…); for the rare occasion when a third-party component should have delegated the interface implementation to a protected virtual method but didn't.

Contributor

jnm2 commented May 22, 2018

Reminiscent of the LanguageUtils.ExplicitImplementation<TheBaseClass, ISomeInterface>(this).Foo(…); for the rare occasion when a third-party component should have delegated the interface implementation to a protected virtual method but didn't.

@Thaina

This comment has been minimized.

Show comment
Hide comment
@Thaina

Thaina May 22, 2018

This is what I hate most about. Virtual overriding and base call in interface

Thaina commented May 22, 2018

This is what I hate most about. Virtual overriding and base call in interface

@orthoxerox

This comment has been minimized.

Show comment
Hide comment
@orthoxerox

orthoxerox May 22, 2018

@HaloFour, @gafter Why not (I1)base<I3>.M() for I3's implementation of I1.M()? The cast should make it explicit that you're accessing the implementation of a method from I1. Or have I forgotten the extra parentheses again?

orthoxerox commented May 22, 2018

@HaloFour, @gafter Why not (I1)base<I3>.M() for I3's implementation of I1.M()? The cast should make it explicit that you're accessing the implementation of a method from I1. Or have I forgotten the extra parentheses again?

@svick

This comment has been minimized.

Show comment
Hide comment
@svick

svick May 22, 2018

Contributor

I don't like the proposed base<I3>(I1).M() syntax much, so I thought I would come up with my own syntax. Here's my though process:

When you write base.M(), what you're saying is "invoke the method M on the base type". What we want is syntax that says "invoke the method I1.M on the base type I3".

Let's start with "invoke the method I1.M on the base type". The obvious syntax would be base.I1.M(), but that's already valid syntax with different meaning. What would be useful is some way to mark I1.M as a single name. I think parentheses could be used for this purpose: base.(I1.M)().

As an extension, it would make sense to use this syntax to invoke interface methods on actual expressions, not just the base pseudo-expression, e.g. x.(I1.M)(). This would allow calling implicit interface methods without casting or generic constraints. Not sure if that's desirable or not.

Next up, "invoke the method M on the base type I3". The above proposal says to use generic-like syntax: base<I3>.M(). I don't like this, because it behaves quite unlike generics and because reading it as generics are commonly read, "base of I3", doesn't make much sense. Instead I think parentheses could be used here: base(I3).M(). Compare this will all the other keyword operators that take a type as a "parameter": typeof, sizeof and nameof.

Putting the two together, "invoke the method I1.M on the base type I3" would be: base(I3).(I1.M)(). I think this makes it clearer what the expression actually means, even if it looks a bit awkward.

Contributor

svick commented May 22, 2018

I don't like the proposed base<I3>(I1).M() syntax much, so I thought I would come up with my own syntax. Here's my though process:

When you write base.M(), what you're saying is "invoke the method M on the base type". What we want is syntax that says "invoke the method I1.M on the base type I3".

Let's start with "invoke the method I1.M on the base type". The obvious syntax would be base.I1.M(), but that's already valid syntax with different meaning. What would be useful is some way to mark I1.M as a single name. I think parentheses could be used for this purpose: base.(I1.M)().

As an extension, it would make sense to use this syntax to invoke interface methods on actual expressions, not just the base pseudo-expression, e.g. x.(I1.M)(). This would allow calling implicit interface methods without casting or generic constraints. Not sure if that's desirable or not.

Next up, "invoke the method M on the base type I3". The above proposal says to use generic-like syntax: base<I3>.M(). I don't like this, because it behaves quite unlike generics and because reading it as generics are commonly read, "base of I3", doesn't make much sense. Instead I think parentheses could be used here: base(I3).M(). Compare this will all the other keyword operators that take a type as a "parameter": typeof, sizeof and nameof.

Putting the two together, "invoke the method I1.M on the base type I3" would be: base(I3).(I1.M)(). I think this makes it clearer what the expression actually means, even if it looks a bit awkward.

@BreyerW

This comment has been minimized.

Show comment
Hide comment
@BreyerW

BreyerW May 22, 2018

What about (base<I3> as I1).M(); ? Similar idea to that of @orthoxerox but using as. base<> is special syntax anyway so i dont think it will clash with usual meaning of as?

BreyerW commented May 22, 2018

What about (base<I3> as I1).M(); ? Similar idea to that of @orthoxerox but using as. base<> is special syntax anyway so i dont think it will clash with usual meaning of as?

@svick

This comment has been minimized.

Show comment
Hide comment
@svick

svick May 22, 2018

Contributor

@BreyerW The meaning of as is "try to cast to the given type and return null if the cast fails". Since base<I3> as I1 would never be null, I don't think it makes sense to use as for that.

Contributor

svick commented May 22, 2018

@BreyerW The meaning of as is "try to cast to the given type and return null if the cast fails". Since base<I3> as I1 would never be null, I don't think it makes sense to use as for that.

@BreyerW

This comment has been minimized.

Show comment
Hide comment
@BreyerW

BreyerW May 22, 2018

Well with this logic my idea and @orthoxerox s are both insufficient since both use casting syntax while the issue is all about pointing out which implementation should be called.

But folks are already familiar with idea of casting and it vaguely resemble the problem of pointing out which implementation to call so i think using casting syntax is, at least, intriguing and then there is choice which syntax reads better. I just proposed alternative syntax which already exist too and i wouldnt focus too much on nullability aspect, more on expressivness and readability since it isnt exactly about casting.

BreyerW commented May 22, 2018

Well with this logic my idea and @orthoxerox s are both insufficient since both use casting syntax while the issue is all about pointing out which implementation should be called.

But folks are already familiar with idea of casting and it vaguely resemble the problem of pointing out which implementation to call so i think using casting syntax is, at least, intriguing and then there is choice which syntax reads better. I just proposed alternative syntax which already exist too and i wouldnt focus too much on nullability aspect, more on expressivness and readability since it isnt exactly about casting.

@OJacot-Descombes

This comment has been minimized.

Show comment
Hide comment
@OJacot-Descombes

OJacot-Descombes May 22, 2018

Why not simply write I3.I1.M(); instead of base<I3>(I1).M(); by walking the path of the implemented interfaces backwards?

OJacot-Descombes commented May 22, 2018

Why not simply write I3.I1.M(); instead of base<I3>(I1).M(); by walking the path of the implemented interfaces backwards?

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour May 22, 2018

Contributor

@OJacot-Descombes

I3.I1.M() is already legal C# syntax.

Contributor

HaloFour commented May 22, 2018

@OJacot-Descombes

I3.I1.M() is already legal C# syntax.

@OJacot-Descombes

This comment has been minimized.

Show comment
Hide comment
@OJacot-Descombes

OJacot-Descombes May 22, 2018

I don't like the mix of <> and (). Maybe base(I3.I1).M(); as a short for base(I3).base(I1).M();. Both variants would be allowed. If a base, let's say I3, is not ambiguous: base.base(I1).M();. It is a bit verbose but will be used rarely anyway.

OJacot-Descombes commented May 22, 2018

I don't like the mix of <> and (). Maybe base(I3.I1).M(); as a short for base(I3).base(I1).M();. Both variants would be allowed. If a base, let's say I3, is not ambiguous: base.base(I1).M();. It is a bit verbose but will be used rarely anyway.

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli May 22, 2018

My vote:
base(I3, I1).M() instead of base<I3>(I1).M(), but the best is to get rid of it.
base(I3).M() instead of base<I3>.M()
base(I1).M() instead of base(I1).M()
base.M()

qrli commented May 22, 2018

My vote:
base(I3, I1).M() instead of base<I3>(I1).M(), but the best is to get rid of it.
base(I3).M() instead of base<I3>.M()
base(I1).M() instead of base(I1).M()
base.M()

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour May 22, 2018

Contributor

@qrli

base(I3).M() instead of base<I3>.M()
base(I1).M() instead of base(I1).M()

The <> and () do two different things. I don't think you can encode them using the same syntax. That's why you can't get rid of it of the first syntax (or a variant thereof), you need a syntax which let's you describe both the interface on which the member is first defined as well as an interface which can provide a default implementation for that member.

Contributor

HaloFour commented May 22, 2018

@qrli

base(I3).M() instead of base<I3>.M()
base(I1).M() instead of base(I1).M()

The <> and () do two different things. I don't think you can encode them using the same syntax. That's why you can't get rid of it of the first syntax (or a variant thereof), you need a syntax which let's you describe both the interface on which the member is first defined as well as an interface which can provide a default implementation for that member.

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli May 22, 2018

@HaloFour I know. But I just wonder whether they are significant enough. The purpose is only to invoke a base. Do we really care whether it is an implementation or declaration? As long as the compiler can find one, in most cases, I'd think. When it is really necessary, we can still specify both.

qrli commented May 22, 2018

@HaloFour I know. But I just wonder whether they are significant enough. The purpose is only to invoke a base. Do we really care whether it is an implementation or declaration? As long as the compiler can find one, in most cases, I'd think. When it is really necessary, we can still specify both.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour May 22, 2018

Contributor

@qrli

Do we really care whether it is an implementation or declaration?

You could have multiple interfaces that declare the same signature but they each represent a different slot. Then those interfaces (or other interfaces) could provide their own implementations. You could even have a single interface that provides default implementation for both. As such, for completeness, you'd need a syntax that can describe both unambiguously. It would probably be exceptionally rare to actually require to specify both unambiguously.

Given the first example that @gafter posted I don't know of another way to explicitly specify that you want I3's default implementation of I1.M. Using base(I3).M would be ambiguous because I3 has two members by that name, and base(I1).M would resolve to I4.M, if at all depending on whether the compiler treats these interfaces as being a hierarchy.

Personally I think I'm warming up to base<>() as it feels like a generic method call. It's a little wonky, and I could probably swap what <> and () are supposed to contain back and forth all day long, but I think that once we get used to it it'll be fine. And then we'll likely never have to actually use it.

Contributor

HaloFour commented May 22, 2018

@qrli

Do we really care whether it is an implementation or declaration?

You could have multiple interfaces that declare the same signature but they each represent a different slot. Then those interfaces (or other interfaces) could provide their own implementations. You could even have a single interface that provides default implementation for both. As such, for completeness, you'd need a syntax that can describe both unambiguously. It would probably be exceptionally rare to actually require to specify both unambiguously.

Given the first example that @gafter posted I don't know of another way to explicitly specify that you want I3's default implementation of I1.M. Using base(I3).M would be ambiguous because I3 has two members by that name, and base(I1).M would resolve to I4.M, if at all depending on whether the compiler treats these interfaces as being a hierarchy.

Personally I think I'm warming up to base<>() as it feels like a generic method call. It's a little wonky, and I could probably swap what <> and () are supposed to contain back and forth all day long, but I think that once we get used to it it'll be fine. And then we'll likely never have to actually use it.

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli May 23, 2018

@HaloFour
When I said Do we really care whether it is an implementation or declaration?, base(I3).M() instead of base<I3>.M() and base(I1).M() instead of base(I1).M(), I'm referring to the easy cases.

If we talk about the 1st example, which is the extreme example: yes, we can or we must specify both.

However, even with C++'s multi-inheritance, a base invocation only specifies the implementation.

I think the core issue is with explicit interface implementation, where the implementation method has no callable name. Clearly I3 has two methods I1.M and I2.M, and we want to call one of them. I1 and I2 do not really matter here. If we could name them, we could name them as M1 and M2 respectively, then we can do base(I3).M1().

Since we cannot name them, some way has to be invented. Then it is natural to think I1.M and I2.M as their names, with an invalid character dot in the middle. Then the name has to be quoted, e.g. base(I3)."I1.M"(), or escaped, e.g. base(I3).I1@M(). I don't mean this is better syntactically. But this is more natural conceptually to understand, IMO.

qrli commented May 23, 2018

@HaloFour
When I said Do we really care whether it is an implementation or declaration?, base(I3).M() instead of base<I3>.M() and base(I1).M() instead of base(I1).M(), I'm referring to the easy cases.

If we talk about the 1st example, which is the extreme example: yes, we can or we must specify both.

However, even with C++'s multi-inheritance, a base invocation only specifies the implementation.

I think the core issue is with explicit interface implementation, where the implementation method has no callable name. Clearly I3 has two methods I1.M and I2.M, and we want to call one of them. I1 and I2 do not really matter here. If we could name them, we could name them as M1 and M2 respectively, then we can do base(I3).M1().

Since we cannot name them, some way has to be invented. Then it is natural to think I1.M and I2.M as their names, with an invalid character dot in the middle. Then the name has to be quoted, e.g. base(I3)."I1.M"(), or escaped, e.g. base(I3).I1@M(). I don't mean this is better syntactically. But this is more natural conceptually to understand, IMO.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour May 23, 2018

Contributor

@qrli

Clearly I3 has two methods I1.M and I2.M, and we want to call one of them.

Not necessarily. That naming convention is only specific to how explicit implementation works in C#. The CLR imposes quite literally no limitation as long as the signature matches, and other languages like VB.NET allow the developer to rename the method whatever they want with whatever accessibility that they desire, as long as the rest of the signature matches.

That said, it'd probably be safe to refer to those methods using their C# explicit implementation syntax of I1.M and the compiler can use that name to resolve the slot regardless of the actual name of the implementing member. But that name is problematic and I think looks pretty alien embedded in some kind of expression. If C# had a general scope resolution operator base(I3).I1::M() (C++ or Java) or a way to escape arbitrary identifiers base(I3).``I3::M()`` (F#) then at least we'd have something that we could reach to that would somewhat fit into the language already.

We could probably throw lots of really ugly spaghetti at the wall all day long.

Contributor

HaloFour commented May 23, 2018

@qrli

Clearly I3 has two methods I1.M and I2.M, and we want to call one of them.

Not necessarily. That naming convention is only specific to how explicit implementation works in C#. The CLR imposes quite literally no limitation as long as the signature matches, and other languages like VB.NET allow the developer to rename the method whatever they want with whatever accessibility that they desire, as long as the rest of the signature matches.

That said, it'd probably be safe to refer to those methods using their C# explicit implementation syntax of I1.M and the compiler can use that name to resolve the slot regardless of the actual name of the implementing member. But that name is problematic and I think looks pretty alien embedded in some kind of expression. If C# had a general scope resolution operator base(I3).I1::M() (C++ or Java) or a way to escape arbitrary identifiers base(I3).``I3::M()`` (F#) then at least we'd have something that we could reach to that would somewhat fit into the language already.

We could probably throw lots of really ugly spaghetti at the wall all day long.

@orthoxerox

This comment has been minimized.

Show comment
Hide comment
@orthoxerox

orthoxerox May 23, 2018

What do you do today when you have to call an explicitly implemented method on the base class?

interface IFoo { void Foo(); }

class Base : IFoo { void IFoo.Foo() { Console.WriteLine("Base IFoo.Foo"); } }

class Derived : Base { public void M() { ((IFoo)this).Foo(); } }

I think calling an explicitly implemented method on the base interface should look similar to that, except instead of this some base interface selector should be used. Can be base(I3) or base<I3> or anything else.

orthoxerox commented May 23, 2018

What do you do today when you have to call an explicitly implemented method on the base class?

interface IFoo { void Foo(); }

class Base : IFoo { void IFoo.Foo() { Console.WriteLine("Base IFoo.Foo"); } }

class Derived : Base { public void M() { ((IFoo)this).Foo(); } }

I think calling an explicitly implemented method on the base interface should look similar to that, except instead of this some base interface selector should be used. Can be base(I3) or base<I3> or anything else.

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli May 23, 2018

@HaloFour Agree. But that's for the extreme case, so ugliness is not that bad.
On the other hand, we will be using the easy case most of the time. And I don't like to think whether I should use <> or (), and whether the code I read means implementation or declaration.

qrli commented May 23, 2018

@HaloFour Agree. But that's for the extreme case, so ugliness is not that bad.
On the other hand, we will be using the easy case most of the time. And I don't like to think whether I should use <> or (), and whether the code I read means implementation or declaration.

@HaloFour

This comment has been minimized.

Show comment
Hide comment
@HaloFour

HaloFour May 23, 2018

Contributor

@qrli

And I don't like to think whether I should use <> or (), and whether the code I read means implementation or declaration.

I can understand that. I think that the problem with trying to codify that into a single syntax, e.g. base(I1).M() is that the behavior could change based on which interfaces have default implementations.

Let's say you start with:

public interface IFoo {
    void M();
}

public interface IBar : IFoo {
    void IFoo.M() => Console.WriteLine("Bar!");
}

public class Baz : IBar {
    public void M() {
        base(IFoo).M();
    }
}

In this case since base(IFoo).M() is pointing at a declaration the compiler could resolve the default implementation at IBar.M. But, if someone were to change IFoo:

public interface IFoo {
    void M() => Console.WriteLine("Foo!");
}

Since IFoo.M is now an implementation the use of base(IFoo).M() would call the default implementation at IFoo.M and skip the implementation at IBar.M. I don't think that this would be desirable, so I think that the declaration vs. implementation versions do have to be different.

As @gafter described, base(IFoo).M() would call the most derived implementation, so it would still invoke IBar.M, but without a second form of the syntax to explicitly take an implementing interface you wouldn't have the ability to disambiguate or jump the hierarchy.

Contributor

HaloFour commented May 23, 2018

@qrli

And I don't like to think whether I should use <> or (), and whether the code I read means implementation or declaration.

I can understand that. I think that the problem with trying to codify that into a single syntax, e.g. base(I1).M() is that the behavior could change based on which interfaces have default implementations.

Let's say you start with:

public interface IFoo {
    void M();
}

public interface IBar : IFoo {
    void IFoo.M() => Console.WriteLine("Bar!");
}

public class Baz : IBar {
    public void M() {
        base(IFoo).M();
    }
}

In this case since base(IFoo).M() is pointing at a declaration the compiler could resolve the default implementation at IBar.M. But, if someone were to change IFoo:

public interface IFoo {
    void M() => Console.WriteLine("Foo!");
}

Since IFoo.M is now an implementation the use of base(IFoo).M() would call the default implementation at IFoo.M and skip the implementation at IBar.M. I don't think that this would be desirable, so I think that the declaration vs. implementation versions do have to be different.

As @gafter described, base(IFoo).M() would call the most derived implementation, so it would still invoke IBar.M, but without a second form of the syntax to explicitly take an implementing interface you wouldn't have the ability to disambiguate or jump the hierarchy.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter May 23, 2018

Member

@orthoxerox

What do you do today when you have to call an explicitly implemented method on the base class?

interface IFoo { void Foo(); }

class Base : IFoo { void IFoo.Foo() { Console.WriteLine("Base IFoo.Foo"); } }

class Derived : Base { public void M() { ((IFoo)this).Foo(); } }

That is not correct. The point of a base.M() call is that it invokes the method non-virtually. You're invoking the base interface implementation virtually, which defeats the purpose:

interface IFoo { void Foo(); }

class Base : IFoo { void IFoo.Foo() { Console.WriteLine("Base IFoo.Foo"); } }

class Derived : Base, IFoo { void IFoo.Foo() { ((IFoo)this).Foo(); } } // OOPS - self-recursive call
Member

gafter commented May 23, 2018

@orthoxerox

What do you do today when you have to call an explicitly implemented method on the base class?

interface IFoo { void Foo(); }

class Base : IFoo { void IFoo.Foo() { Console.WriteLine("Base IFoo.Foo"); } }

class Derived : Base { public void M() { ((IFoo)this).Foo(); } }

That is not correct. The point of a base.M() call is that it invokes the method non-virtually. You're invoking the base interface implementation virtually, which defeats the purpose:

interface IFoo { void Foo(); }

class Base : IFoo { void IFoo.Foo() { Console.WriteLine("Base IFoo.Foo"); } }

class Derived : Base, IFoo { void IFoo.Foo() { ((IFoo)this).Foo(); } } // OOPS - self-recursive call
@orthoxerox

This comment has been minimized.

Show comment
Hide comment
@orthoxerox

orthoxerox May 23, 2018

@gafter oops, didn't think that one 100% through. So there's no way to invoke it non-virtually, is it?

orthoxerox commented May 23, 2018

@gafter oops, didn't think that one 100% through. So there's no way to invoke it non-virtually, is it?

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter May 23, 2018

Member

@orthoxerox Correct, no way to do it. That's not a big deal today because you don't get into that situation without an uncommon coding pattern. But it would be a problem in the presence of default interface implementations, because you'd be using explicit implementations more often (because that is how you implement things).

Member

gafter commented May 23, 2018

@orthoxerox Correct, no way to do it. That's not a big deal today because you don't get into that situation without an uncommon coding pattern. But it would be a problem in the presence of default interface implementations, because you'd be using explicit implementations more often (because that is how you implement things).

@bbarry

This comment has been minimized.

Show comment
Hide comment
@bbarry

bbarry May 23, 2018

Contributor

Will these calling conventions apply to classes as well as interfaces? Would you for example be able to call a base.base.Method()?

Contributor

bbarry commented May 23, 2018

Will these calling conventions apply to classes as well as interfaces? Would you for example be able to call a base.base.Method()?

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter May 23, 2018

Member

@bbarry Undecided, but possibly yes.

Member

gafter commented May 23, 2018

@bbarry Undecided, but possibly yes.

@qrli

This comment has been minimized.

Show comment
Hide comment
@qrli

qrli May 24, 2018

@HaloFour

Since IFoo.M is now an implementation the use of base(IFoo).M() would call the default implementation at IFoo.M and skip the implementation at IBar.M. I don't think that this would be desirable, so I think that the declaration vs. implementation versions do have to be different.

The same issue applies to the base(IFoo).M() as @gafter given, as long as the base is parameterized by declaration interface, the implementation would have to be picked by compiler, which may change if base interface implementation changes.

Since the CRL requires to specify the implementation interface, I think the C++ way could be taken, so that base(TInterface) only specifies the implementation interface. Then at least for non-extreme cases, the implementation method is always fixed, no need to care about what has been changed in base interfaces.

qrli commented May 24, 2018

@HaloFour

Since IFoo.M is now an implementation the use of base(IFoo).M() would call the default implementation at IFoo.M and skip the implementation at IBar.M. I don't think that this would be desirable, so I think that the declaration vs. implementation versions do have to be different.

The same issue applies to the base(IFoo).M() as @gafter given, as long as the base is parameterized by declaration interface, the implementation would have to be picked by compiler, which may change if base interface implementation changes.

Since the CRL requires to specify the implementation interface, I think the C++ way could be taken, so that base(TInterface) only specifies the implementation interface. Then at least for non-extreme cases, the implementation method is always fixed, no need to care about what has been changed in base interfaces.

@Joe4evr

This comment has been minimized.

Show comment
Hide comment
@Joe4evr

Joe4evr May 24, 2018

but without a second form of the syntax to explicitly take an implementing interface you wouldn't have the ability to disambiguate or jump the hierarchy.

And why is this a bad thing? You can't jump the hierarchy in class inheritance (outside of a specific edge-case), I would work on the assumption that if you make a type that implements IBar, you are satisfied with the default implementation that is defined in that interface and less interested in IFoo's implementation.

Joe4evr commented May 24, 2018

but without a second form of the syntax to explicitly take an implementing interface you wouldn't have the ability to disambiguate or jump the hierarchy.

And why is this a bad thing? You can't jump the hierarchy in class inheritance (outside of a specific edge-case), I would work on the assumption that if you make a type that implements IBar, you are satisfied with the default implementation that is defined in that interface and less interested in IFoo's implementation.

@gafter

This comment has been minimized.

Show comment
Hide comment
@gafter

gafter Jun 1, 2018

Member

I agree with @Joe4evr that jumping the hierarchy is not an important use case. But disambiguating among implementations provided by different base interfaces is important.

Member

gafter commented Jun 1, 2018

I agree with @Joe4evr that jumping the hierarchy is not an important use case. But disambiguating among implementations provided by different base interfaces is important.

@Joe4evr

This comment has been minimized.

Show comment
Hide comment
@Joe4evr

Joe4evr Jun 13, 2018

Here's a thought, illustrated in sample code:

//The example interfaces we used earlier
public interface IFoo
{
    void M();
}
public interface IBar : IFoo
{
    void IFoo.M() => Console.WriteLine("Bar!");
}

public class ImplicitFoo : IBar
{
    public void M()
    {
        //Not allowed, can't "jump the hierarchy"
        //when not stating IFoo in the inheritance list
        //even if the type is implicitly an IFoo via IBar
        base(IFoo).M();

        //Potential "find unambiguous signature match
        //among directly stated interfaces" syntax
        //(let me dream, but some inference where possible would be nice)
        base().M();
    }
}

public class ExplicitFoo : IBar, IFoo
{
    public void M()
    {
        //Now this is allowed, even required to disambiguate
        base(IFoo).M();

        //Not allowed because there's an ambiguous match
        //between definitions in directly stated interfaces
        base().M();
    }
}

Joe4evr commented Jun 13, 2018

Here's a thought, illustrated in sample code:

//The example interfaces we used earlier
public interface IFoo
{
    void M();
}
public interface IBar : IFoo
{
    void IFoo.M() => Console.WriteLine("Bar!");
}

public class ImplicitFoo : IBar
{
    public void M()
    {
        //Not allowed, can't "jump the hierarchy"
        //when not stating IFoo in the inheritance list
        //even if the type is implicitly an IFoo via IBar
        base(IFoo).M();

        //Potential "find unambiguous signature match
        //among directly stated interfaces" syntax
        //(let me dream, but some inference where possible would be nice)
        base().M();
    }
}

public class ExplicitFoo : IBar, IFoo
{
    public void M()
    {
        //Now this is allowed, even required to disambiguate
        base(IFoo).M();

        //Not allowed because there's an ambiguous match
        //between definitions in directly stated interfaces
        base().M();
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment