csharplang/meetings/2018/LDM-2018-11-14.md
2018-11-28 14:52:39 -08:00

3.8 KiB

C# Language Design Notes for Nov 14, 2018

Agenda

  1. Base call syntax for default interface implementations
  2. Switch exhaustiveness and null

Discussion

Default interface implementations

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.

Syntax

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.

Switch exhaustiveness and null

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.