csharplang/meetings/2019/LDM-2019-03-13.md
2019-04-03 22:36:00 -07:00

6.2 KiB

C# Language Design Meeting for March 13, 2019

Agenda

  1. Interface "reabstraction" with default interface methods
  2. Precedence of the switch expression
  3. or keyword in patterns
  4. "Pure" null tests and the switch statement/expression

Discussion

Interface reabstraction

We previously specified our intent to permit reabstraction, where you could mark a derived implementation as "abstract" and force further implementors to provide an implementation.

We allow this in interfaces, so one argument for allowing is simply maintaining feature parity with classes, as we do in many other areas.

Another argument is that it would be very useful in interfaces because you would be able to provide an interface with a stronger contract than the base implementation. For instance, if you were implementing an interface that has a method which sorts a list, and you would like to provide an implementation which requires a sort with a stronger contract, like a stable sort, then you would want to mark the sort implementation as abstract. It may not be possible to write an implementation yourself and the implementation in the base may not be suitable because it doesn't implement the stronger contract (e.g., it implements an unstable sort).

There's an open question of how this would be implemented in the runtime and how expensive it would be.

Conclusion

The feature seems reasonable and if there were an easy, cheap implementation we would probably take it. However, we don't know if that's possible. We'll take a work item to investigate and see if it's feasible. We'd also like this to work for classes, but don't see it as strictly required if only interfaces are possible.

Precedence of the switch expression

It's currently at the same precedence of the relational operators (like <, or is and as). There are some problems with this precedence, though. For example,

x = e switch { .. } + 1 // syntax error

This is because the + binds tighter than the switch, so this switch ({ ... } + 1).

Relational precedence is also weird because of the following:

x = a + b switch { ... } // this is `(a + b) switch { ... }`
x = a & b switch { ... } // this is `a & (b switch { ... })`

If we were to change to primary expression precedence, it would be (b switch {...}) for both examples.

One question is what happens with await: what does await e switch { ... } do?

If we look at the switch expression as similar to the conditional expression, this would produce (await e) switch { ... }. More broadly, the switch looks a like a more complex conditional expression, so allowing expressions that are allowed on the left hand side of the conditional could be desirable for the switch as well. So, a + b switch { ... } is (a + b) switch { ... }.

However, one major difference between the conditional and the switch is that the switch can have expressions with arbitrary type, but conditional requires a bool, which allows for more complex expressions.

Here are some examples that may motivate the decision:


x + e switch {} * y

((x + e) switch {}) * y

_ = await e switch { ... };

_ = a ?? (b switch {
            ...
          });

_ = (a ?? b) switch {
            ...
            };

_ = a ?? b switch {
        ...
        };

_ = 1 + o switch { int i => i, _ => 0 };

_ = a && b ? c : d;

_ = a ? b : d ? e : f;

_ = (string)s switch { ... };

_ = -x switch { ... };

_ = await x switch { ... };

Looking at the examples, it's seeming like the space makes a big difference in readability. When there's a space between the operands, it's less obvious what the switch applies to, so binary operators are more ambiguous between a ?? (b switch { ... }) and (a ?? b) switch {...}. However, the parenthesis for the first example look a bit stranger than the second, and some people feel that it looks more natural to the switch to bind to only b.

There's also a fairly strong feeling that the unary operators, like - and cast, should bind more strongly.

Conclusion

It seems like a new precedence level between unary and multiplicative is the right fit. For places where either interpretation could be possible (await), this doesn't feel like an unreasonable decision. For places where one interpretation seems preferred (other unary operators and binary operators) this choice fits for both.

Patterns with or

Should we make

switch (e)
{
    case X or:
}

a warning on or as an illegal variable name? This would assist parsing because we would not have to do any parsing lookahead to figure out if this is a designation, vs an or pattern (that doesn't exist yet).

Conclusion

Unfortunately, we already shipped this, so it would be a breaking change. Since we can resolve this through lookahead, we don't think this example is severe enough to warrant a new warning. If we see something more complicated to resolve, we may reconsider.

Where to produce warnings for "null test" in switch

switch (s)
{
    case null when s.Length == 1:
        break;
    case null when M(s = "foo"):
        break;
    case _ when s.Length == 2:
        break;
}

We have decided null is a "pure" null test and we will update the null state for s. The question is where we update that state:

  1. To the entry of the switch and all previous cases?
  2. To the branch of the switch only?
  3. Just to the forward paths of the flow analysis?

One problem with relying just on the flow analysis is that disjoint checks could be reordered and affect the diagnostics produced, even though the bug would be present regardless of the ordering, since the check would actually be verifying the nullability of the input, which is immutable across the switch arms.

The other problem is what variable state to update. If we copy values from the switch and test against the copy, that would be a separate state that would need to be tracked, since the original expression could be modified within the switch.

Conclusion

Let's look at only using forward flow propagation, but define the ordering as looking at the patterns of each arm in order, then use the following flow graph. Notably, if there was no declared variable for the switch expression, we will synthesize one.