88 lines
2.3 KiB
Markdown
88 lines
2.3 KiB
Markdown
|
|
||
|
# C# Language Design Notes for Feb 25th, 2019
|
||
|
|
||
|
## Agenda
|
||
|
|
||
|
Semantics of `base()` calls in default interface implementations and classes
|
||
|
|
||
|
## Discussion
|
||
|
|
||
|
The exact semantics of `base()` are still unresolved. Consider
|
||
|
the following legacy `base` call.
|
||
|
|
||
|
```C#
|
||
|
class A
|
||
|
{
|
||
|
virtual void M() {}
|
||
|
}
|
||
|
|
||
|
class B : A
|
||
|
{
|
||
|
// override void M() { }
|
||
|
}
|
||
|
|
||
|
class C : B
|
||
|
{
|
||
|
override void M() => base.M();
|
||
|
}
|
||
|
```
|
||
|
|
||
|
The behavior for `base` is to find the "nearest" implementation and call
|
||
|
that implementation using a direct call, meaning if B.M is uncommented it
|
||
|
will be called, while if it commented out then A.M will be called. Notably,
|
||
|
if `B` is uncommented at compile time, but at runtime the `B.M` override is
|
||
|
not present *the runtime will call A.M.* This is because the runtime will
|
||
|
continue looking through base classes for a matching signature if the target
|
||
|
method is not present.
|
||
|
|
||
|
Most importantly, the runtime *does not* yet have this behavior for `base()`
|
||
|
calls in interface implementations. This is because there could be multiple
|
||
|
paths to search down and the current IL encoding does not provide a root
|
||
|
definition to search towards.
|
||
|
|
||
|
For example,
|
||
|
|
||
|
```C#
|
||
|
interface IA
|
||
|
{
|
||
|
void M();
|
||
|
}
|
||
|
interface IB : IA
|
||
|
{
|
||
|
void IA.M() // If this gets removed, the IC.M call will fail
|
||
|
{
|
||
|
base(IA).M();
|
||
|
}
|
||
|
}
|
||
|
interface IC : IB
|
||
|
{
|
||
|
void IA.M() => base(IB).M();
|
||
|
}
|
||
|
```
|
||
|
|
||
|
At the moment, we do not have the time to implement an entire new IL form
|
||
|
for `base()` calls, so we have to implement a behavior in absence of that
|
||
|
feature.
|
||
|
|
||
|
Choices:
|
||
|
|
||
|
1. No `base()` call
|
||
|
2. `base()` call can only target the original definition of the method
|
||
|
3. `base(T).M()` call is a direct call to `T` and an error if the method
|
||
|
doesn't exist
|
||
|
4. `base(T)` starts searching in `T`, but in the compiler looks at the bases
|
||
|
for a unique, most derived implementation.
|
||
|
|
||
|
For feature evolution, we then have three more choices later.
|
||
|
|
||
|
1. Stay as-is
|
||
|
2. New opcode/behavior for `base()`
|
||
|
3. New opcode/behavior for `base.` and `base()`
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
For the first choice, let's do (3). The emitted code will be a direct call to
|
||
|
that method. It is expected that the runtime will throw an exception if an
|
||
|
implementation is not present in that type. For binding, in classes the
|
||
|
signature used will be the closest override, while for interfaces the
|
||
|
signature will be the member definition.
|