154 lines
5.1 KiB
Markdown
154 lines
5.1 KiB
Markdown
|
|
||
|
# C# Language Design for April 8, 2020
|
||
|
|
||
|
## Agenda
|
||
|
|
||
|
1. `e is dynamic` pure null check
|
||
|
2. Target typing `?:`
|
||
|
3. Inferred type of an `or` pattern
|
||
|
4. Module initializers
|
||
|
|
||
|
## Discussion
|
||
|
|
||
|
### `e is dynamic` pure null check
|
||
|
|
||
|
We warn that doing `e is dynamic` is equivalent to `e is object`, but `e is object` is a pure
|
||
|
null check, while `e is dynamic` is not. Should we make `is dynamic` a pure null check for
|
||
|
consistency?
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
Yes.
|
||
|
|
||
|
### Target-typing `?:`
|
||
|
|
||
|
The simplest example where we have a breaking change is
|
||
|
|
||
|
```C#
|
||
|
void M(short)
|
||
|
void M(long)
|
||
|
M(b ? 1 : 2)
|
||
|
```
|
||
|
|
||
|
Previously this would choose `long`, because the expressions are effectively typed separately,
|
||
|
and when inferring `1 : 2` we would choose `int`, and then rule out `short` during overload
|
||
|
resolution as invalid.
|
||
|
|
||
|
Target-typing converts each arm in turn to find the best possible type, selecting `short` instead
|
||
|
of `long`. That means the first overload is selected instead with target-typing.
|
||
|
|
||
|
It's hard to easily avoid this breaking change. The obvious mechanism, running overload
|
||
|
resolution twice, is problematic because having multiple arguments with `conditional` expressions
|
||
|
produces exponential growth in the number of passes of overload resolution.
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
Let's talk about this again in a separate meeting. Since whatever we choose here will probably
|
||
|
be the final decision (any further changes will be breaking), we want to be sure we're making the
|
||
|
best choice.
|
||
|
|
||
|
### Inferred type of an `or` pattern is the common type of two inferred types
|
||
|
|
||
|
```C#
|
||
|
object o = 1;
|
||
|
if (o is (1 or 3L) and var x)
|
||
|
// what is the type of `x`?
|
||
|
```
|
||
|
|
||
|
The problem here is that the existing common type algorithm allows conversion that are forbidden
|
||
|
in pattern matching. In the above case we would choose `long`, because `3L` is a long, and `1`
|
||
|
can be converted to `long`. However, in pattern matching the widening conversion from `int` to
|
||
|
`long` is illegal.
|
||
|
|
||
|
The proposal is to narrow the set of conversions only to implicit reference conversions or
|
||
|
boxing conversions. Why this could be useful is the example
|
||
|
|
||
|
```C#
|
||
|
class B { }
|
||
|
class C : B { }
|
||
|
if (o is (B or C) and var x)
|
||
|
// x is `B`
|
||
|
```
|
||
|
|
||
|
A follow-up question is about ordering. The proposed rules only infer types mentioned in
|
||
|
the checks, meaning that the following
|
||
|
|
||
|
```C#
|
||
|
if (o is (Derived1 or Derived2 or Base { ... }) and var x)
|
||
|
```
|
||
|
|
||
|
looks like this today
|
||
|
|
||
|
```C#
|
||
|
((Derived1 or Derived2) or Base)
|
||
|
|
||
|
-> ((object) or Base)
|
||
|
|
||
|
-> (object)
|
||
|
```
|
||
|
|
||
|
On the other hand, if the example were parameterized in the opposite way,
|
||
|
|
||
|
```C#
|
||
|
(Derived1 or (Derived2 or Base))
|
||
|
|
||
|
-> (Derived1 or (Base))
|
||
|
|
||
|
-> (Base)
|
||
|
```
|
||
|
|
||
|
So the ordering seemingly matters if the `or` pattern is a simple binary expression.
|
||
|
|
||
|
The first question is if
|
||
|
|
||
|
```C#
|
||
|
if (o is (Derived1 or Derived2 or Base { ... }))
|
||
|
```
|
||
|
|
||
|
should produce `Base`, and the second question is if parenthesizing affects this, e.g. explicitly saying
|
||
|
|
||
|
```C#
|
||
|
if (o is ((Derived1 or Derived2) or Base { ... }))
|
||
|
```
|
||
|
|
||
|
produces `object`.
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
We're not seeing a lot of scenarios that depend on these features, but not doing it feels like
|
||
|
leaving information on the table. Functionally, the compiler can infer a stronger type, so why
|
||
|
not do so? The proposed modified common type algorithm is accepted.
|
||
|
|
||
|
We also think that type narrowing for the `or` operation should use all the `or` operations
|
||
|
in a series as the arguments to the common type algorithm.
|
||
|
|
||
|
### Module Initializers
|
||
|
|
||
|
Proposal: https://github.com/RikkiGibson/csharplang/blob/module-initializers/proposals/module-initializers.md
|
||
|
|
||
|
There are a few places where module initializers today. The most common is a shared resource
|
||
|
that is used by multiple types, but the program would like to initialize the shared resource
|
||
|
before the static constructors of the types are run.
|
||
|
|
||
|
The question for the implementation is to how to indicate where the code for the module
|
||
|
initializer will go, and how the compiler will recognize it.
|
||
|
|
||
|
The proposal provides a mechanism to identify the module initializer via an attribute on the module
|
||
|
that points to a type, and type contains a static constructor that acts as the module initializer.
|
||
|
From a conceptual level, this seems more complicated than necessary. Can we put the attribute on the
|
||
|
type or, even the method, that holds the code for the module initializer?
|
||
|
|
||
|
If we go that route, why require the code be in the static constructor at all? Can any static method
|
||
|
be a target? One reason why we may prefer a static constructor is that if the emitted module initializer
|
||
|
calls the target static constructor method, it's easy to insert code before that method is invoked
|
||
|
by writing a static constructor for the containing type. On the other hand, even static constructor
|
||
|
can have code inserted before them, for example with static field initializers.
|
||
|
|
||
|
The last question is whether to allow only one module initializer method, or allow multiple and specify
|
||
|
that the compiler will call them in a stable, but implementation-defined order.
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
Let's let any static method be a module initializer, and mark that method using a well-known attribute.
|
||
|
We'll also allow multiple module initializer methods, and they will each be called in a reserved, but
|
||
|
deterministic order.
|