csharplang/meetings/2017/LDM-2017-03-08.md
2017-05-30 15:56:12 -07:00

5 KiB

C# Language Design Notes for Mar 8, 2017

Agenda

We looked at default interface member implementations.

  1. Xamarin interop scenario
  2. Proposal
  3. Inheritance from interface to class
  4. Overriding and base calls
  5. The diamond problem
  6. Binary compatibility
  7. Other semantic challenges

Xamarin interop scenario

Android interfaces are written in Java, and can therefore now have default implementations on members. Xamarin won't be able to seamlessly project those interfaces into .NET.

On iOS, Objective-C and Swift have protocols, of the general shape:

protocol Foo
{
    void Hello();
@optional
	int Color { get; set; }
	int Bye();
}

Again, the best we can do is to project the non-optional parts into C# interfaces, whereas the optional parts must be handled in less appetizing ways:

interface IFoo
{
	void Hello();
}

Proposal

The current proposal is to allow the following in interfaces:

  • member bodies
  • nonvirtual static and private members
  • overrides of members

Putting a concrete body on your method (etc), means you don't have to implement it on the class.

Not proposing (but may want to look at):

  • non-virtual public instance members
  • protected and internal members
  • nested types
  • operator declarations
  • etc...

Don't want:

  • state!
  • conversions

This gives parity with where Java is: they made tweaks over time based on feedback, and this is where they landed (private and static members were added).

The more you go to the side of adding things, the more this is a philosophy change for interfaces.

Inheritance from interface to class

Important question: is the member inherited into the class, or is it "explicitly implemented"? Our assumption is that it's explicitly implemented:

interface I { int M() => 0; }
class C : I { } // C does not have a public member "M"

That means you can't "just" refactor similar implementations into an interface the way you do into a base class.

Overriding and base calls

Should overrides be able to call base? Yes, but we would need new syntax to deal with ambiguity arising from the fact that you can have more than one base interface:

interface I1 { void M() { WriteLine("1"); } }
interface I2 : I1 { override void M() { WriteLine("2"); } }
interface I3 : I1 { override void M() { WriteLine("3"); } } 
interface I4 : I2, I3 { override void M() { base(I2).M(); } }

The exact syntax for disambiguating base calls is TBD. Some ideas:

I.base.M()  // what Java has
base(I).M() // similar to default(I)
base.I.M()  // ambiguous with existing meaning
base<I>.M() // looks like generics

The diamond problem

I4 above inherits two default implementations, one each from I2 and I3, but explicitly provides a new implementation, so that there's never a doubt as to which one applies. However, imagine:

interface I5: I2, I3 { }
class C : I2, I 3 { }

The class declaration of C above must certainly be an error, since there is no good (symmetric) unambiguous way of determining a default implementation. Also, the interface declaration of I5 should be an error, or at least a warning.

This means that adding an override to an interface can break existing code. This can happen in source, but depending on compilation order, it may also be possible to successfully compile such code.

Binary compatibility

What should the runtime do when an interface adds a default implementation and that causes an ambiguity in code that isn't recompiled?

Should this be a runtime error? It would lead to some hard-to-understand failures. Should it "just pick one"?

"Why did you not let my program run?" vs "why did you not prevent this hole"?

We need to decide what is less harmful. We should look at what Java does and why.

Other semantic challenges

The feature reveals some "seams" between C# and the CLR in how they understand interface overriding. In the following, imagine I, C and the consuming code are in three different assemblies:

// step 1

interface I { }

class C : I { public void M() { /* C.M */ } }

// step 2

interface I { void M() { /* I.M */} }

I i = new C();
i.M(); // calls I.M default implementation

// step 3: recompile C

I i = new C();
i.M(); // calls C.M non-virtual method

The problem here is that the runtime doesn't consider non-virtual members able to implement interface members, but C# does. To bridge the gap, C# generates a hidden virtual stub method to implement the interface member. However, during step 1 there is no interface member to implement, and during step 2 the class declaration isn't recompiled. The runtime doesn't consider C.M an implementation of I.M, so if you call M on a C through the I interface, you get the default behavior. As soon as C is recompiled, however, the compiler inserts its stub, and the behavior changes.

We have to decide it there is something we can and will do about this. We may just accept it as a wart.