csharplang/meetings/2017/LDM-2017-04-05.md
2017-06-01 14:53:15 -07:00

5.5 KiB
Raw Blame History

C# Language Design Notes for Apr 5, 2017

Agenda

  1. Non-virtual members in interfaces
  2. Inferred tuple element names
  3. Tuple element names in generic constraints

Non-virtual members in interfaces

Do we want to allow non-virtual declarations in interfaces? In practice you probably at least want private methods. But it's likely you'll want internal, and maybe public. What's the syntactic distinction from normal "virtual/abstract" interface members?:

interface I1
{
    virtual void M1() { ... }
    void M2() { ... } // non-virtual
}
interface I2
{
    void M1() { ... } 
    sealed void M2() { ... } // non-virtual
}
interface I3
{
    virtual void M1() { ... } 
    sealed void M2() { ... } // non-virtual
}

If you want this for traits based programming, you'd want to have the same things available, when you "port things over" to a trait.

Intuitively, though, it feels against the spirit of interfaces.

Should an interface be required to put abstract in order to allow derived interfaces to override them? No. So it probably also shouldn't require virtual. So it's the "virtualness" that's implicit, not specifically the "abstractness".

The core motivating scenario is default-ness of implementations. So that is the thing we should design around. Is the copy-paste scenario between interfaces and classes important? It already is very far from possible. For instance, property declarations in interfaces today are syntactically identical to auto-properties in classes today!

Requiring a modifier (e.g. virtual) on a virtual interface member with a default member, as in I1 and I3 above, causes an inconsistency between interface members with and without default implementations.

Also, taking unmodified members with bodies to be non-virtual, as in I1 above, is a bit dangerous: adding a body changes the member from virtual to non-virtual. While an implementing class may still think it is implicitly implementing the member, it is in fact just declaring an unrelated member:

class C : I1
{
    public void M2() { ... } // Doesn't implement I1.M2, which isn't virtual!
}

It is better to require a modifier (e.g. sealed) on non-virtual members in interfaces, to clearly distinguish them, as in I2 and I3 above.

The best combination, then, is illustrated by I2, where non-virtual members must be marked, and virtual ones must not (and maybe cannot).

Tentatively we'll go forward with the following decisions:

  • Non-virtualness should be explicitly expressed through sealed
  • We want to allow all modifiers in interfaces, in analogy with classes, but with different defaults
  • Default accessibility for interface members is public, including for nested types
  • private function members in interfaces are implicitly sealed, and sealed is not permitted on them. private nested classes can be sealed, and that means sealed in the class sense.

Absent a good proposal, partial is still not allowed on interfaces or their members.

Inferred tuple element names

Anonymous objects allow "projection initializers", where the name of a member can be automatically inferred from an expression ending in a name:

new { X = X, Y }; // infers Y as name of second member

Proposal is to do the same for tuple literals, picking up any names in element expressions if the element doesn't explicitly have a name:

int a = 1;
...
var t = (a, b: 2, 3); // (int a, int b, int)

Just as with anonymous types we pick up names from expressions that are simple names (x), dotted names (e.x) and ?.'ed names (e?.x).

This is technically a breaking change from C# 7.0, but it is quite esoteric. It is hard to construct an example where you depend on a tuple element not having a particular name, but here's one:

Action y = () => {}
var t = (x: x, y) // (int x, Action y) or (int x, Action)?
t.y(); // a call of the second component or of an extension method y on the type (int, int)?

We think that this is low enough risk that we can introduce the feature, especially if we do it quickly (C# 7.1).

A subtlety of the feature is that inferred names should not trigger the warnings around unused names:

(int, int) t = (x: x, y); // warning for x. No warning for y, because inferred

Tuple names in constraints

There are some gnarly issues questions around tuple element names in generic constraints. In general we try to avoid use of different tuple element names across related declarations, but with constraints it is hard.

A simple example is

class C<U> where U : I<(int a, int b)>, I<(int notA, int notB)> // this is currently allowed, and would become an error

But more complex examples involve indirect differences.

class C<U> where U : I<(int a, int b)>, I2 // where I2 implements I<(int notA, int notB)>

In the case of type implementations, we prevent those and even check for possible type unifications. But in the case of constraints, we currently dont check as much (no type unification check).

The third level of difficulty is illustrated by:

class C<U, T> where U : I<(int a, int b)>, I2<T> // where I2 implements I<(T notA, T notB)>

We think you could never declare a class (in C#) that satisfies the constraints (in C#) in these examples. You could construct things in IL to circumvent, but it's ok for us not to deal with that. So it's probably ok for C# not emit an error here.

We do try to emit an error when constraints conflict. But the value is low and it is hard and esoteric and makes the compiler slower.