csharplang/meetings/2020/LDM-2020-04-08.md

5.1 KiB

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

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

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.

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.