Add design notes

This commit is contained in:
Mads Torgersen 2017-06-19 17:05:12 -07:00
parent b6cd4f2f59
commit ad9e5bca2e
6 changed files with 434 additions and 106 deletions

View file

@ -1,15 +1,24 @@
# C# Language Design Notes for May 17, 2017
***Raw notes, yet to be cleaned up - read at your own peril***
## Agenda
# Conflicting override
More questions about default interface member implementations
If at runtimes you have ambiguous overrides
1. Conflicting override of default implementations
2. Can the Main entry point method be in an interface?
3. Static constructors in interfaces?
4. Virtual properties with private accessors
5. Does an override introduce a member?
6. Parameter names
# Conflicting override of default implementations
If at runtimes you have ambiguous overrides:
``` c#
interface I1 { void M1(); }
interface I2 : I1 { override void M1() { ... }}
interface I2 : I1 { override void M1() { ... } }
interface I3 : I1 { override void M1() { ... } }
@ -19,42 +28,37 @@ C x;
x.M1(); // what happens when you call?
```
This can happen through certain orders of compilation.
This can happen without compiler complaint through certain orders of compilation.
Options:
1. throw on load
2. throw on call (like Java)
3. pick arbitrarily (but consistently)
4. bake in what you meant at compile time
1. always use the baked in, error if it's gone
2. pick the unique one, fallback to baked in if unsuccessful
1. always use the baked in one, error if it's gone
2. pick the unique one, fallback to baked-in if unsuccessful
Baking in causes a lot to hinge on recompilation. We should leave to the runtime to resolve the virtual call. So not 4. Also, 1 seems too harsh. If you don't call it, why complain? If you call an arbitrary method, that's a little dangerous: you can't reason about what the program does.
## Conclusion
Option 2 it is. We can throw. If we realize we were wrong about this later, we can move from there. No bake in.
# Can the Main entry point method be in an interface?
Yes. No good reason why not. Also, that's the least work.
## Is the overriding logic handled at compile time or runtime
Yes
# Does the compiler encode what the most specific override is
No
## interaction with variance
# Can Main be in an interface?
Who cares? Aleksey does. Yes. Because that's the least work.
# Static constructors in interfaces?
Probably, but let the runtime folks think about this. Reason is static fields. We allow initializers for static fields, but not an explicit static constructor. Can think about adding later.
# In classes you can define virtual prop with private accessor. Allowed in interfaces? What does it mean?
Probably, but let the runtime folks think about this. Reason is static fields. We could allow initializers for static fields, but not an explicit static constructor. Can think about adding later.
# Does an override introduce a member
- Does it introduce parameter names
# Virtual properties with private accessors
In classes you can define a virtual property with private accessor. Allowed in interfaces? What does it mean? Probably disallow.
# Does an override introduce a member?
- Does it introduce parameter names?
- Does it introduce a new virtual method that could be implemented independently
If the call site sees two separately declared methods, that's already an ambiguity.
@ -62,7 +66,7 @@ If the call site sees two separately declared methods, that's already an ambigui
``` c#
interface I1 { void M1(); }
interface I2 : { void M1() { ... } }
interface I2 { void M1() { ... } }
interface I3 : I1, I2 { override void M1() { ... } }
@ -70,18 +74,19 @@ I3 x;
x.M1();
```
Should I3 be allowed?
Should x.M1();
Should `I3` be allowed? Does it override both?
Should `x.M1()`;
Explicit override syntax removes the problem, but do we want to force people to always put the interface in front.
Explicit override syntax removes the problem, but do we want to force people to always put the interface in front?
Java's solution does not help us, because independently declared interface members unify.
Java's solution does not help us, because independently declared interface members unify in Java.
Xamarin does not expect to need override. They'll just need to know whether there is a default implementation or not. They will need reflection support to query the default methods.
It's not an option to depend on order.
Resolution: I3 is fine, but does not introduce a new member. So the call to M1 is ambiguous. The work-around is to cast to one of the interfaces. However, oddly, you *can* call it through base(I3).
## Conclusion
`I3` is fine, but does not introduce a new member. So the call to `M1` is ambiguous. The work-around is to cast to one of the interfaces. However, oddly, you *can* call it through `base(I3)`.
# Parameter names
@ -100,10 +105,9 @@ The class behavior is to pick the most specific names, but that does mean that i
From experience, people do intentionally change names on override, but others ask for features to prevent them from doing it!
Common scneario is implementing a generic instance. Going from "T" to Dog.
Common scenario is implementing a generic instance. Going from `T` to `Dog`.
## Conclusion
Let's take the names from the original declaration, deliberately being inconsistent with the class rule for simplicity's sake.
# New open question:
what about base(I2) and parameter names?
New open question: what about `base(I2)` and parameter names?

View file

@ -1,27 +1,25 @@
# C# Language Design Notes for May 26, 2017
***Raw notes, yet to be cleaned up - read at your own peril***
## Agenda
- Native ints
1. Native ints
# Native ints
We would like to supply high-quality native-size integers. The best we have today is `IntPtr`, which lacks most operators and have a few behavioral weaknesses, including a suboptimal `ToString` implementation. Xamarin has introduced user-defined `nint` and `nuint` types for interop, but those can't completely do the trick; for instance, user-defined operators cannot distinguish between checked and unchecked contexts.
Options:
1. Improve `IntPtr` with operators
2. Add `nint` and `nuint` as a user defined struct (the Xamarin approach)
3. Add `nint` and `nuint` to the language
1. compile down to native ints and add an attribute to persist in metadata (doesn't solve `ToString`)
2. project language `nint` etc to user defined structs
1. compile down to `IntPtr` and add an attribute to persist the additional type info in metadata
2. project language-level types to new user defined structs
4. A combo of 1 and 3
State of the world: We have `IntPtr` which has crappy arithmetics, and enough use that it would be breaking to change anything.
`IntPtr` has a few operators today; for instance `+` with `int` (which is checked on 32 bit and unchecked on 64 bit!), and some conversions.
Xamarin need to do interop, so they have a user defined struct.
`IntPtr` has a few operators: `+` with `int`, which is checked on 32 bit and unchecked on 64 bit! And some conversions. Plus a very unfortunate `ToString` implementation.
3.2 looks like this:
The new structs in 3.2 look something like this:
``` c#
struct NativeInt
@ -32,42 +30,31 @@ struct NativeInt
/// etc
```
But operators are implemented by the language, not as UD operators.
But operators are implemented by the language, not as user-defined operators.
Diff between 3.1 and 3.2 is that with 3.2 at runtime we have different types, so we can have differentiated runtime behavior: `ToString` and reflection.
The difference between 3.1 and 3.2 is that with 3.2 at runtime we have different types, so we can have differentiated runtime behavior: `ToString` and reflection can tell them apart.
A downside is that operations that take `ref IntPtr` (like Interlocked...) wouldn't automatically take `ref nint`. Having the public mutable field would let things still work for people, and we could go through and add `nint` overloads over time to make it better.
A downside is that operations that take `ref IntPtr` (like `Interlocked.CompareExchange`) wouldn't automatically take `ref nint`. Having the public mutable field would let things still work for people, and we could go through and add `nint` overloads over time to make it better.
This should be in corlib. Is there anything we can do to mitigate in the meantime? We could ship a nuget package etc, but there's some cost to that, including indefinite maintenance. But some of the people who would benefit from this will be in a terrible spot if we don't provide something.
This should be in mscorlib, but that takes time. Is there anything we can do to mitigate in the meantime? We could ship a nuget package etc, but there's some cost to that, including indefinite maintenance. But some of the people who would benefit from this will be in a terrible spot if we don't provide something.
We also need to deal with native float. There is no option to do 3.1 for floats; there is no `IntPtr` equivalent. So that one would need a framework type. However, we could probably live with that nfloat struct moving into the frameworks - other than Xamarin, which would add it faster for its interop scenarios.
We also need to deal with native float. There is no option to do 3.1 for floats; there is no `IntPtr` equivalent. So that one would need a framework type. However, we could probably live with that `nfloat` struct moving into the frameworks over time - other than Xamarin, which would add it faster for its interop scenarios.
With 3.1, if you consume a `nint`-attributed `IntPtr` with an old compiler, would it treat it as an `intPtr`? If that's the case then the code would subtly change behavior on compiler upgrade. Unfortunate! We could poison `nint` with `ModReq` so that they cannot be consumed by existing compilers, but now `nint` really *is* a different type, and requires separate overloads of methods that take it as a parameter.
With 3.1, if you consume a `nint`-attributed `IntPtr` with an old compiler, would it treat it as an `intPtr`? If that's the case then the code would subtly change behavior on compiler upgrade. Unfortunate! We could perhaps poison `nint` with `ModReq` so that they cannot be consumed by existing compilers, but now `nint` really *is* a different type, and requires separate overloads of methods that take it as a parameter.
Another option is to obsolete the UD operators on `IntPtr`, to drive people to use `nint` instead.
Another option is to obsolete the user-defined operators on `IntPtr`, to drive people to use `nint` instead.
Objections to 3.2:
## Objections to 3.2:
- Adoption, where a separate struct would take a while to propagate (we feel we've mostly mitigated this)
- We'll emit slightly less efficient and more verbose IL in a couple of cases
- Needing new overloads for `nint` where there are `IntPtr` overloads today
- Needing new overloads for `nint` where there are `IntPtr` overloads today (or at least a conversion, and new overloads where there are `ref IntPtr` parameters).
Objection to 3.1:
- No runtime distinction (reflection and `ToString`)
- `ToString` happens all the time
Who has the benefit
- Identity conversion: 3.1
- ToString: 3.2
- IL efficiency: 3.1
- Preserve the type on boxing: 3.2
- Adoption cost: 3.1, but only slightly
- Can overload: 3.2
- Does not need to overload: 3.1
We're balanced on preferring 3.1 vs 3.2. Could maybe be convinced to 3.2 if we can solve how do existing users of `IntPtr` migrate.
## Conclusion
We're torn, and evenly balanced on preferring 3.1 vs 3.2. Could maybe be convinced to 3.2 if we can solve how do existing users of `IntPtr` migrate.

View file

@ -1,14 +1,16 @@
# C# Language Design Notes for May 31, 2017
## Agenda
***Raw notes, yet to be cleaned up - read at your own peril***
1. Default interface members: overriding or implementing?
2. Downlevel poisoning of ref readonly in signatures
3. Extension methods with ref this and generics
4. Default in operators
# Default interface members
# Default interface members: overriding or implementing?
We are mixing two different concepts: Overriding and implementing.
Current design:
So far, when a derived interface wants to provide a default implementation for a member declared by a base interface, we have been thinking of it by analogy with overriding of virtual methods in classes, even using the `override` keyword:
``` c#
interface I1
@ -22,9 +24,9 @@ interface I2 : I1
}
```
This design (thinking of it as overriding, in analogy with classes), forces us to grab a lot of stuff from classes that may not be very useful.
This design philosophy forces us to grapple with a lot of stuff from classes that may not be very useful.
Implementing on the other hand is different. You don't have to match accessibility, etc. You can implement multiple with one method. A base class can highjack an implementation from an interface:
Implementing on the other hand is different. You don't have to match accessibility, etc. You can implement multiple interface members with one class member. A base class can highjack an implementation from an interface:
``` c#
class C1
@ -38,7 +40,7 @@ class C2 : C1, I2
}
```
Maybe we should try to stay true to the notion of *implementing* when it comes to default interface methods. So "overriding" should instead be expressed as an explicit interface member implementation:
Maybe we should try to stay true to the notion of *implementing* when it comes to default interface members. So "overriding" should instead be expressed as an explicit interface member implementation:
``` c#
interface I2 : I1
@ -47,26 +49,30 @@ interface I2 : I1
}
```
This seems to have more of an "interface camp" feel to it.
This seems to have more of an "interface camp" feel to it: it is more similar to what you can do with interface members today, only you can now do it in interfaces.
How do you call the implementation? Well you have that problem today: you *cannot* call an explicit implementation today.
We like this approach! Let's investigate some of its consequences.
However, that's a significant reduction in expressiveness. You'd have to do so via a non-virtual helper method. So the implementation needs to anticipate needing to be reused.
## "Base calls" to default interface implementations
For instance, when you do an "implement interface" refactoring, what does it do? Leave out already implemented ones? implement them again with a call to `base` (if that's possible)? with a `throw`?
With the "explicit implementation" approach above, how do you *call* the implementation from another one? Well you have that problem today: you *cannot* directly call an explicit implementation today; not even from within the implementing class itself.
Won't it be common to want to reuse a default implementation, adding "just a little bit more", i.e., calling `base`? Well, that gets into an accessibility game: the accessibility for ultimate use, versus the accessibility for being non-virtually called as `base`.
However, for default interface implementations that's a significant reduction in expressiveness. You'd have to do so via a non-virtual helper method. So the implementation has to anticipate needing to be reused and factor out to a helper method.
## Problem with base
For instance, when you do an "implement interface" refactoring, what does it do? Leave out already implemented ones? implement them again with a call to `base` (if that's possible)? With a `throw`?
There are two ambiguities.
Won't it be common to want to reuse a default implementation, adding "just a little bit more", i.e., calling `base`? Well, that gets into an accessibility game: the accessibility for ultimate use, versus the accessibility for being non-virtually called as `base`.
## Ambiguity of base calls
If we allow base calls to default implementations, there are two different kinds of ambiguities.
Ambiguity of declaration:
``` c#
interface I1 { void M() { ... } }
interface I2 { void M() { ... } }
interface I3 : I1, I2 { void N() { base.M(); } } // which M()?
interface I3 : I1, I2 { void N() { base.M(); } } // which declaration of M()?
```
Ambiguity of implementation:
@ -75,16 +81,16 @@ Ambiguity of implementation:
interface I1 { void M(); }
interface I2: I1 { void I1.M() { ... } }
interface I3: I1 { void I1.M() { ... } }
interface I4: I2, I3 { void N() { base.M(); } } // which M()?
interface I4: I2, I3 { void N() { base.M(); } } // which implementation of M()?
```
We could say, just no base calls to default implementations, for now.
You need to get into weird-ish stuff like this:
Alternatively you need to get into stuff like this:
``` c#
base(I3).(I1.M1)<string>(1, 2, 3); // Call I3's implementation of I1's M1
```
`base(I3).(I1.M1)<string>(1, 2, 3);`
If we were to come back and do something about this, we could also consider allowing *classes* to pick which one they grab the implementation for.
If we want to do something like this, we would also consider allowing *classes* to pick which one they grab the implementation from: even though classes have no ambiguity about which is the the most *specific* override, you could potentially allow "reaching around" and grabbing an older one.
## Deimplementation
@ -94,41 +100,53 @@ In this new scheme, should we allow "deimplementation" - the ability for an inte
The meaning would be "I declare that the default implementation is not useful (or is harmful) to classes implementing me."
Not worth it. This would be rare. People can implement to throw an exception instead.
This would probably be rare. People can implement to throw an exception instead.
## Conclusion
Strangely this moves the design closer to its intended purpose. It keeps it about implementation, without mixing inheritance into it.
Strangely this approach moves the design closer to its intended purpose. It keeps it about implementation, without mixing inheritance into it.
Shorthand for when the interface name is obvious? No. We haven't needed it for explicit implementation in classes, we don't need it now.
It does mean you have to type the interface name every time you want to provide a new implementation for a base interface member. Should we have a shorthand for when the interface name is unambiguous and obvious? No. We haven't needed it for explicit implementation in classes, we probably don't need it now.
base calls? No.
Let's think about whether we can live entirely without base calls to default implementations, and come back to this when we've mulled it a bit.
# Ref stuff
# Downlevel poisoning of ref readonly in signatures
## Poisoning
We currently poison with a `modreq` all the places in signatures where unaware compilers could do something unsafe (write into read-only memory) by using a `ref readonly` as if it was a mutable `ref`.
We currently poison with a modreq all the places in signatures where unaware compilers could do something unsafe (write into readonly memory).
`Modreq`s aren't very discerning, so a virtual method cannot even be called by unaware compilers, even when only overriding is unsafe. We accept this degree of granularity as the best possible state of affairs.
Modreqs aren't very discerning, so a virtual method cannot even be called by unaware compilers, even when only overriding is unsafe.
We accept this as the best state of affairs.
Should we reject methods that have the attributes but not the modreq? It makes it harder to relax it later. But it protects the contract from manual finagling. So yes, refuse to consume.
The only role of the `modreq` is to poison unaware compilers. The actual information about read-only-ness of refs is carried in attributes. Should we reject methods that have the attributes but not the `modreq`? It makes it harder to relax it later. But it protects the contract from manual finagling. So yes, refuse to consume such methods.
It's hard for us to add a new modifier. We may have to reuse one. If we had our own, we could avoid using attributes altogether, just make it modopt or modreq depending on whether it is required for safety. Let's keep this idea around, in case we do get to have our own.
## extension methods with `ref`, and generics
Extension methods will be allowed to take value types by `ref` or `ref readonly`. It doesn't make sense for reference types. However, what about unconstrained generics.
# Extension methods with ref this and generics
Extension methods will be allowed to take value types by `ref` or `ref readonly`. It doesn't make sense for reference types, so those are disallowed. However, what about unconstrained (or interface constrained) type parameters?
Should we allow so as to get the benefit when a type argument happens to be a value type? Probably not.
- For mutable ref it seems a little dangerous and surprising that an extension method can modify a variable of reference type
- For readonly ref, chances are it would lead to much unintended copying, as the readonly-ness would cause the value to get copied whenever you tried to do something useful with it in the body (e.g. based on an interface constraint).
## Conclusion
Don't allow.
Let's allow for `ref readonly`, disallow for `ref`.
# Default in operators
Don't allow `default` as an operand to a unary or binary operator. We need to protect the ability to add new overloads in the future.
`default` as an operand to unary or binary operators would sometimes work, and sometimes not, depending on whether there happens to be a best operator across all available predefined or user-defined ones for all types:
<paste in examples>
``` c#
var a = default + default; // error
var b = default - default; // ok
var c = default * default; // ok
var d = default / default; // error
```
# Non-trailing named arguments
This feels arbitrary. But worse, it is actually a recipe for future compat disasters. Imagine we added a `-` operator to, say, arrays in the future. Now the second line above would break, because the `int` overload of the pre-defined `-` operator would no longer happen to be best.
## Conclusion
Don't allow `default` as an operand to a unary or binary operator. We need to protect the ability to add new operator overloads in the future.

View file

@ -0,0 +1,51 @@
# C# Language Design Notes for Jun 13, 2017
## Agenda
1. Native-size ints
2. Native-size floats
# Native-size ints
We want `nint` and `nuint` to be part of the language. They will be used in contexts where we care about respecting checked vs unchecked in operator behavior, for example.
We have two options for how to represent at runtime:
1. Project to `IntPtr`, track the `nint` or `nuint` "overlay" in metadata for compile-time consumption only
2. Project to new struct types that wrap an `IntPtr` and potentially exhibit different behavior at runtime too
Option 2 had some objections attached. Let's go through them:
- Adoption/roll out of new types would take time
- We could embed these struct types in the generated assembly and use the NoPIA machinery to unify them across assemblies.
- Would have a slight cost on IL size.
- Probably not a decisive objection
- Need new overloads for a few things, such as `Interlocked.CompareExchange`
- We could probably live with those rolling out gradually, especially with access to the `IntPtr` of a `nint` or `nuint` as a public field (so it can be passed by `ref` to existing overloads)
- There is a bifurcation because the types are really distinct at runtime. There's interop pain between them.
- This may be a good thing. Arguably they represent different concepts: `IntPtr` is more of an opaque handle, whereas `nint` and `nuint` are real integers with number semantics.
Option 1 has some objections as well:
- `ToString` doesn't work right
- This could probably be fixed. It is unlikely that the current deficiencies are depended upon
- you lose context on box/unbox
- That is already the case today with other common types, such as tuples, nullable value types and `dynamic`. Is it really so bad?
- Imprecise reflection info
- Again, we live with this in many places
`String.Format` would exhibit the combination of the first and second problem: passing a `nint` as an argument, it would box and lose context, so `IntPtr.ToString` would get called.
Things to ponder regardless of implementation strategy:
- **Conversions:** Should there be conversions between `nint`/`nuint` and `IntPtr`? If so, which way (if any) should be implicit?
- **Upgrade:** Someone using `IntPtr` today might want to switch to `nint`. This would mostly just work, but would have very subtle changes of behavior here and there.
## Conclusion
We are still hung on what to do. We want to more deeply understand objections against Option 1, and understand if mitigations considered would address e.g. Xamarin's concerns.
# Native-size floats
Float really is a different discussion. Language embedding is less significant: we don't have a notion of checked/unchecked. The main objective would just be to have an alias.

View file

@ -0,0 +1,226 @@
# C# Language Design Notes for Jun 14, 2017
## Agenda
Several issues related to default implementations of interface members
1. Virtual properties with private accessors
2. Requiring interfaces to have a most specific implementation of all members
3. Member declaration syntax revisited
4. Base calls
# Virtual properties with private accessors
Classes (strangely) allow virtual properties with one accessor being private. Should interfaces allow something similar?
## Conclusion
No. It is not obvious what it means, or whether it's in any way useful.
# Requiring interfaces to have a most specific implementation of all members
Should it be an error for an interface to not have a single most specific implementation if each member (even inherited) that has default implementations?
## Conclusion
This might be desirable if we were designing the language from scratch, but it would be breaking, since existing compilers wouldn't know to check that. Also, there'd be no way to fix it if they could.
So we cannot disallow this. The buck stops with the implementing class, which is the one that has to implement any interface member that doesn't have a unique most specific default implementation.
This does allow you to "reabstract" a default-implemented member by injecting a diamond situation and forcing a class level error, but so be it.
# Member declaration syntax revisited
In a [previous meeting](https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-05-31.md) we changed the syntax around "overriding" of default implementations to match that of explicit implementation in classes today.
This topic explores whether we have the right syntax when declaring an interface member and giving it a default implementation *at the same time*.
Today the syntax for this and an overriding implementation is simply:
``` c#
interface I1
{
int M(int i, bool b, string s) { return s?.Length ?? i; }
}
interface I2 : I1
{
int I1.M(int i, bool b, string s) { return b ? i : s?.Length ?? 0; }
}
```
Or, if we use expression bodies:
``` c#
interface I1
{
int M(int i, bool b, string s) => s?.Length ?? i;
}
interface I2 : I1
{
int I1.M(int i, bool b, string s) => b ? i : s?.Length ?? 0;
}
```
The declaration syntax in `I1` has both pros and cons. One the one hand:
- It is simply what you get if you take today's interface member declaration syntax and add a body
- It looks similar to a class member that might implicitly implement an interface member
On the other:
- It looks exactly like a non-virtual class member, which cannot be overridden/reimplemented by anything else
- It's odd why the implementation in `I2` needs to look like an explicit implementation (`I1.M`) when one that's immediately declared does not
One could argue that the current syntax should be reserved for the same meaning that it has in classes: a non-virtual member. But either way leads to confusion and surprise for some:
- I "simply" add a body to an interface member, and all of a sudden it is no longer implementable by classes or derived interfaces!
- I "simply" copy a nonvirtual member from a class to an interface, and all of a sudden it is re-implementable by classes and derived interfaces!
We need to decide which side of this conundrum to land on. If we *were* to change the meaning of
``` c#
int M(int i, bool b, string s) => s?.Length ?? i;
```
to be "nonvirtual" member what *would* an implementable interface member with a default implementation look like instead? Let's explore some options:
## Proposal 0
Status quo. Non-virtual members are explicitly annotated with `sealed`, which isn't great, but we can live with it.
## Proposal 1
Make a separate declaration that uses the same "explicit implementation" syntax as in the derived classes:
``` c#
interface I1
{
int M(int i, bool b, string s); // declares the member
int I1.M(int i, bool b, string s) => s?.Length ?? i; // provides a default implementation
}
interface I2 : I1
{
int I1.M(int i, bool b, string s) => b ? i : s?.Length ?? 0; // no change
}
```
This is probably the most hideous and verbose option we can come up with. But points for consistency!
## Proposal 2
Use a `default` keyword as a modifier to signal that a default implementation is provided:
``` c#
interface I1
{
default int M(int i, bool b, string s) => s?.Length ?? i;
}
```
`default` may or may not be allowed and/or required on a derived implementation as well:
``` c#
interface I2 : I1
{
default int I1.M(int i, bool b, string s) => b ? i : s?.Length ?? 0;
}
```
We don't hate this option, but it may be a bit too much syntax "just for the benefit of first timers".
It signals: "I am not really implementing it here; I'm providing a default!" That may allay the sense that interfaces are becoming too much like classes.
# Proposal 3
Same as 2, but with the `virtual` keyword instead:
``` c#
interface I1
{
virtual int M(int i, bool b, string s) => s?.Length ?? i;
}
```
`virtual` may or may not be allowed and/or required on a derived implementation as well:
``` c#
interface I2 : I1
{
virtual int I1.M(int i, bool b, string s) => b ? i : s?.Length ?? 0;
}
```
This feels like we're going back towards virtual methods in classes in a way we don't want to. Also, it anything it should probably then be `override` and not `virtual` on the derived implementation, but now we're just going in circles.
## Additional consideration
If we ever want to allow implementable static members in interfaces (that are required to be provided by implementing classes), then the syntactic choices we make here will potentially come back to bite us.
## Conclusion
We don't like Option 3, and we hate Option 1. We're not totally averse to Option 2, but for now we'll keep the status quo, which seems to have the least syntax for the most common tasks.
# Base calls
We pondered briefly whether we could live without the ability to call default implementations directly, just as you cannot call an explicit implementation today. However, we think that we do need to allow it. A particularly strong example is when you have ambiguous default implementations from different base interfaces. How do you pick one if you cannot call it?
## Disambiguation
There are two ways in which a base call to a default implementation can be ambiguous:
- **Implementation ambiguity:** For a given original declaration, there may be two base interfaces both inheriting that declaration and providing competing implementations for it. Which one did you want to call?
- **Declaration ambiguity:** There may be two base interfaces that happen to declare members of the same signature. Which one did you mean?
We can think of disambiguating syntax for both:
- `base(I1).M`: I want `I1`'s implementation of `M`
- `base.(I1.M)`: I want my `base`'s implementation of the `M` that was declared in `I1`
- `base(I3).(I1.M)`: I want `I3`'s implementation of the `M` that was declared in `I1`
However we think that implementation ambiguity is going to be common, and declaration ambiguity is more esoteric. We are happy to add disambiguation syntax only for implementations for now.
## Aside: Name disambiguation
There's an interesting and more general feature waiting for another time in the ability to disambiguate a member *name* by where it's declared, as in `e.(I1.M)`. This could be used to get at hidden members or implemented interface members without casting, etc. But let's save that for another day.
## Syntax
As for the implementation disambiguation, we have three proposals on the table:
``` c#
base(T) // 1
base<T> // 2
T.base // 3
```
We don't like 3 at all (even though that's essentially Java's syntax for it). We have some sympathy for 2, but it looks like there's generics involved, and there's not. We like 1 the best. It is reminiscent of other places in the language where a keyword is followed by a type in parentheses:
``` c#
typeof(T)
default(T)
sizeof(T)
```
## Semantics
When you access a base implementation with the syntax `base(T)`, the meaning is "get the implementation of the member from `T`". This should at least work when `T` is an interface (but we are also interested in making it work for classes, letting you skip to an earlier base).
It is a compile time error if there is no default implementation of the member in `T`. If there is, that implementation is called directly. At runtime, if the implementation is no longer there, it is probably not worth it to try to walk the graph and find another one: we should just throw at this point.
These semantics mean that you can get in trouble if you rearrange your default implementations and don't recompile things that depend on you. So be it.
## Unqualified base
`base` *without* a type should also be allowed for default implementations, as long as it is unambiguous. It means the same as `base(T)` where `T` is the interface that has the unique most specific default implementation at the time where the `base` access is compiled.
## Conclusion
`base` and `base(T)` will be allowed to access default implementations in interfaces, both from derived interfaces and implementing classes. They are compiled to directly call the implementation in the interface they (implicitly or explicitly) designate.
It is worth pondering whether this is all too "static". It is not enlisting the runtime as much as perhaps it could, meaning that more specific default implementations could be ignored if they weren't known when the calling code was compiled. It is, however, equivalent to what we do with base calls in classes today. And it avoids a whole class of problems and error modes, because the compiler hardwires its choice of implementation into the code.

View file

@ -177,3 +177,45 @@ Design some remaining 7.1 features
2. Look at C# 7.2 features
3. GitHub procedure around new design notes and proposals
4. Triage of championed features
## May 17, 2017
[C# Language Design Notes for May 17, 2017](LDM-2017-05-17.md)
More questions about default interface member implementations
1. Conflicting override of default implementations
2. Can the Main entry point method be in an interface?
3. Static constructors in interfaces?
4. Virtual properties with private accessors
5. Does an override introduce a member?
6. Parameter names
## May 26, 2017
[C# Language Design Notes for May 26, 2017](LDM-2017-05-26.md)
1. Native ints
## May 31, 2017
[C# Language Design Notes for May 31, 2017](LDM-2017-05-31.md)
1. Default interface members: overriding or implementing?
2. Downlevel poisoning of ref readonly in signatures
3. Extension methods with ref this and generics
4. Default in operators
## Jun 13, 2017
[C# Language Design Notes for Jun 13, 2017](LDM-2017-06-13.md)
1. Native-size ints
2. Native-size floats
## Jun 14, 2017
[C# Language Design Notes for Jun 14, 2017](LDM-2017-06-14.md)
Several issues related to default implementations of interface members
1. Virtual properties with private accessors
2. Requiring interfaces to have a most specific implementation of all members
3. Member declaration syntax revisited
4. Base calls