154 lines
No EOL
5.1 KiB
Markdown
154 lines
No EOL
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. |