e is dynamic
pure null check- Target typing
?:
- Inferred type of an
or
pattern - Module initializers
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.
The simplest example where we have a breaking change is
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.
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
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
if (o is (Derived1 or Derived2 or Base { ... }) and var x)
looks like this today
((Derived1 or Derived2) or Base)
-> ((object) or Base)
-> (object)
On the other hand, if the example were parameterized in the opposite way,
(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
if (o is (Derived1 or Derived2 or Base { ... }))
should produce Base
, and the second question is if parenthesizing affects this, e.g. explicitly saying
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.
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.