191 lines
6.2 KiB
Markdown
191 lines
6.2 KiB
Markdown
|
|
# 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,
|
|
|
|
```C#
|
|
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:
|
|
|
|
```C#
|
|
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:
|
|
|
|
```C#
|
|
|
|
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
|
|
|
|
```C#
|
|
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
|
|
|
|
```C#
|
|
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?
|
|
2. 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. |