131 lines
5.5 KiB
Markdown
131 lines
5.5 KiB
Markdown
# 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?:
|
||
|
||
``` c#
|
||
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:
|
||
|
||
``` c#
|
||
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:
|
||
|
||
``` c#
|
||
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:
|
||
|
||
``` c#
|
||
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:
|
||
|
||
``` c#
|
||
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:
|
||
|
||
``` c#
|
||
(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
|
||
|
||
``` c#
|
||
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.
|
||
|
||
``` c#
|
||
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 don’t check as much (no type unification check).
|
||
|
||
The third level of difficulty is illustrated by:
|
||
|
||
``` c#
|
||
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.
|
||
|
||
|
||
|
||
|