Skip to content

Latest commit

 

History

History
61 lines (39 loc) · 4.81 KB

LDM-2018-10-01.md

File metadata and controls

61 lines (39 loc) · 4.81 KB

C# Language Design Notes for Oct 1, 2018

Agenda

  1. Nullable type inference
  2. Type parameters and nullability context

Nullable type inference

Generic type inference is also used to determine "best common type", e.g. in anonymously typed arrays, finding the inferred return type of lambdas with multiple returns, etc.

Type inference roughly proceeds as follows:

  1. Gather up all the types of the constituent expressions (some, e.g. null, may not have a type to contribute)
  2. Among those, gather the ones to which all the constituent expressions have an appropriate implicit conversion
  3. If these are all identity convertible to each other, construct the result from them

This third step is a little cryptic. Most of the time, when types are identity convertible to each other, they are the same type. But there are two exceptions in the language today:

  • object and dynamic are identity convertible to each other
  • Tuple types with pairwise identity convertible element types are identity convertible, regardless of element names

In step 3 above, in places where the identity convertible types differ by object vs dynamic, choose dynamic. Where they differ by tuple element names, have the tuple element be unnamed.

This is all relevant to nullable reference types, because we are about to introduce a third way in which non-identical types can be identity convertible to each other:

  • Identity conversion between reference types is determined without regard to their nullability (though if the conversion is performed, it may lead to a warning)

This means we need to say how to construct the result of type inference with regard to nullability of reference types. Where the identity convertible types differ by nullability, we'll determine the nullability based on the variance of the type's position:

  • Covariant: A type is in a covariant position if it is
    • a top level type,
    • a type argument to a covariant type parameter of a generic type in a covariant position
    • a type argument to a contravariant type parameter if a generic type in a contravariant position
  • Contravariant: A type is in a contravariant position if it is:
    • a type argument to a contravariant type parameter of a generic type in a covariant position
    • a type argument to a covariant type parameter if a generic type in a contravariant position
  • Invariant: A type is in an invariant position if it is:
    • a type argument to an invariant type parameter
    • a type argument to a type parameter of a generic type in an invariant position

For a given type position in the result type, we'll always pick among the nullabilities present in that position, with one exception.

  • In a covariant position pick nullable if present, otherwise oblivious if present, otherwise nonnullable
  • In a contravariant position pick nonnullable if present, otherwise oblivious if present, otherwise nullable
  • In an invariant position:
    • if both nullable and nonnullable are present, then yield a warning and pick oblivious (this is the one exception)
    • otherwise if either nullable or nonnullable is present, pick that one
    • otherwise pick oblivious

This leads to nice and symmetric rules, where nullable and nonnullable are treated equally, and oblivious isn't too infectious. As far as we can tell, the rules are associative and can be expressed in a pairwise manner, without causing order dependence. If oblivious had dominated nullable and nonnullable in the invariant case, that would have thwarted associativity.

The one thing that's a little odd is where nullable and nonnullable clash in an invariant position. Ideally this would lead to an error, but we only want to allow nullability to lead to warnings, not errors, so we need to have some answer for what comes out. Oblivious seems the right choice, in that we've already warned that something is wrong, and it will lead to suppression of further warnings caused by that. What's odd about it is that oblivious normally comes from legacy code that's explicitly opted out of the nullability feature. This is the only place where it can occur in new code that is all "opted in".

Type parameters declared and used in different nullability contexts

Allowing a more granular in-file choice between nullability contexts (whether nullable annotations are on or off) leads to new kinds of situations. For instance, a type parameter can be declared in an "off" context (oblivious to nullability) but used in an "on" context. This is the topic of Roslyn issue 30214.

The context where the type parameter is declared determines whether it should be sensitive to the nullability implications of its constraints. In the example in the issue, the type parameters are oblivious, and should not lead to nullability diagnostics, because they are declared in a "legacy" context.