- Base call syntax for default interface implementations
- Switch exhaustiveness and null
Example of base calls:
interface I1 { void M(); }
interface I2 { void M(); }
interface I3 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I4 : I1, I2 { void I1.M() { } void I2.M() { } }
interface I5 : I3, I4
{
void I1.M()
{
base<I3>(I1).M();
base<I4>(I1).M();
}
void I2.M()
{
base<I3>(I2).M();
base<I4>(I2).M();
}
}
Q: Should we even allow the programmer to call explicit implementations?
In existing C# code this is not allowed since all explicit implementations are private. You can always call by casting to the interface if the interface is not re-implemented, but there's no way to do this via a base call.
However, there's no way to use overriding default interface implementation in C# 8.0 except for explicit implementation, so not allowing it would basically not allow the scenario.
Java does allow this scenario, so full compat would require us to add the feature.
Q: What type of dispatch would we use in the runtime?
One way is to constrain to the interface, then do a virtual dispatch for a particular method. This probably would require additional (fairly expensive) runtime work.
Q: What does the syntax need to express?
One problem is for I3
, where by signature M
is ambiguous because it's not
clear whether you mean I1.M
and I2.M
. The syntax would need to specify both
that the I3
implementation is requested and whether or not to choose I1.M
or
I2.M
.
One other option is to just not provide a way of disambiguating M
. If there's an
ambiguity you can't call any particular M
.
Conclusion
Let's do the simplified form. If the M
being called is ambiguous, it's illegal to
call such a method. The only configuration we're considering is which base to look
for the method.
namespace N {
interface I1<T> { int M(string s) => s.Length; }
}
class C : I1<T>
{
int I1.M(string s)
{
// Options:
base<N.I1<T>>.M(s);
base(N.I1<T>).M(s);
N.I1<T>.base.M(2);
base{N.I1<T>}
base:N.I1<T>.M(s);
base!N.I1<T>.M(s);
base@N.I1<T>.M(s);
base::global::N.I1<T>.M(s);
base.(N.I1<T>).M(s);
(base as N.I1).M(s); // base isn't an expression, so this isn't legal today
((N.I1)base).M(s); // same here
base.N.I1.M(s); // clash with a member on base, doesn't allow for qualified name
N.I1.M(s); // this is currently a static call on I1, can't work
}
}
We think the forms that take advantage of base
not being an expression are
too clever and would just be confusing. N.I1.base.M(2)
is possibly tricky
for human readability since base
is in the middle and is harder to notice
without coloration.
One problem with base(N.I1).M(s)
is if we want to add "invocation" operators
to the language, that would disallow invoking your base.
base::N.I1<T>.M(s)
has some history in the language.
Conclusion
Decided on base(N.I1<T>).M(s)
, conceding that if we have an invocation
binding there may be problem here later on.
Current design: warning about not handling all inputs except null.
Q: What happens when the switch actually doesn't match what was given at compile time?
Proposal:
A new MatchFailureException
that extends ArgumentException
. This allows
the compiler to use an existing exception for down-level support. There's
no data, just a message.
Conclusion
The base should be InvalidOperationException
, not ArgumentException
. If
the argument being switched on can be trivially boxed into object, we'll add
it as an argument to the MatchFailureException
. Otherwise, just fill in
null
.