Skip to content

Commit

Permalink
Specify generic constraints added to support nullable reference types…
Browse files Browse the repository at this point in the history
… in C# 8 (#1178)

* Incorporate some text from #700

Bring in the normative text from #700.

Some text is removed because of the decision on normative language in our September meeting.

* fix build issues

* Edit pass

* Respond to feedback.

* port grammar, part 1

* Update standard/types.md

Co-authored-by: Nigel-Ecma <[email protected]>

* fix merge / rebase mishap

* Edits based on meeting feedback.

* Apply suggestions from code review

Co-authored-by: Nigel-Ecma <[email protected]>

* Apply suggestions from code review

Co-authored-by: Nigel-Ecma <[email protected]>

* grammar fixes

* Apply suggestions from code review

Co-authored-by: Nigel-Ecma <[email protected]>

* one last minor fix

* Rework description based on last meeting

Rework the description of nullable annotations on generic type parameters and generic type arguments.

We decided that these annotations should be specified in terms of only generating warnings, but never changing the semantics of a program.

* Use `nullable_type_attribute` in all type grammar

We'd used `'?'` and `nullable_type_attribute` in different places for the `?` annotation. Define `nullable_type_attribute` at first use, and use that consistently.

* Apply suggestions from code review

Co-authored-by: Jon Skeet <[email protected]>

* small grammar fix

Offline comment from @Nigel-Ecma

* updates from 10/30 meeting

This covers part 1, the comments in the files tab

* address comments in converstation tab

This commit addresses the comments in the conversation tab from the 10/30 meeting.

* additional feedback

This commit incorporates the comments on the conversation tab.

* Apply suggestions from code review

Co-authored-by: Nigel-Ecma <[email protected]>

* Replace nullable_type_attribute

with nullable_type_annotation

* typos

---------

Co-authored-by: Nigel-Ecma <[email protected]>
Co-authored-by: Jon Skeet <[email protected]>
  • Loading branch information
3 people authored Nov 20, 2024
1 parent c10ff18 commit a7525ff
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 28 deletions.
97 changes: 75 additions & 22 deletions standard/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,33 +396,32 @@ type_parameter_constraints_clauses
: type_parameter_constraints_clause
| type_parameter_constraints_clauses type_parameter_constraints_clause
;
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: primary_constraint
| secondary_constraints
: primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
| secondary_constraints (',' constructor_constraint)?
| constructor_constraint
| primary_constraint ',' secondary_constraints
| primary_constraint ',' constructor_constraint
| secondary_constraints ',' constructor_constraint
| primary_constraint ',' secondary_constraints ',' constructor_constraint
;
primary_constraint
: class_type
| 'class'
: class_type nullable_type_annotation?
| 'class' nullable_type_annotation?
| 'struct'
| 'notnull'
| 'unmanaged'
;
secondary_constraint
: interface_type nullable_type_annotation?
| type_parameter nullable_type_annotation?
;
secondary_constraints
: interface_type
| type_parameter
| secondary_constraints ',' interface_type
| secondary_constraints ',' type_parameter
: secondary_constraint (',' secondary_constraint)*
;
constructor_constraint
Expand All @@ -434,12 +433,66 @@ Each *type_parameter_constraints_clause* consists of the token `where`, followed

The list of constraints given in a `where` clause can include any of the following components, in this order: a single primary constraint, one or more secondary constraints, and the constructor constraint, `new()`.

A primary constraint can be a class type, the ***reference type constraint*** `class`, the ***value type constraint*** `struct`, or the ***unmanaged type constraint*** `unmanaged`.
A primary constraint can be a class type, the ***reference type constraint*** `class`, the ***value type constraint*** `struct`, the ***not null constraint*** `notnull` or the ***unmanaged type constraint*** `unmanaged`. The class type and the reference type constraint can include the *nullable_type_annotation*.

A secondary constraint can be a *type_parameter* or *interface_type*.
A secondary constraint can be an *interface_type* or *type_parameter*, optionally followed by a *nullable_type_annotation*. The presence of the nullable_type_annotatione* indicates that the type argument is allowed to be the nullable reference type that corresponds to a non-nullable reference type that satisfies the constraint.

The reference type constraint specifies that a type argument used for the type parameter shall be a reference type. All class types, interface types, delegate types, array types, and type parameters known to be a reference type (as defined below) satisfy this constraint.

The class type, reference type constraint, and secondary constraints can include the nullable type annotation. The presence or absence of this annotation on the type parameter indicates the nullability expectations for the type argument:

- If the constraint does not include the nullable type annotation, the type argument is expected to be a non-nullable reference type. A compiler may issue a warning if the type argument is a nullable reference type.
- If the constraint includes the nullable type annotation, the constraint is satisfied by both a non-nullable reference type and a nullable reference type.

The nullability of the type argument need not match the nullability of the type parameter. The compiler may issue a warning if the nullability of the type parameter doesn't match the nullability of the type argument.

> *Note*: To specify that a type argument is a nullable reference type, don't add the nullable type annotation as a constraint (use `T : class` or `T : BaseClass`), but use `T?` throughout the generic declaration to indicate the corresponding nullable reference type for the type argument. *end note*
<!-- Remove in C# 9, when this is allowed -->
The nullable type annotation, `?`, can't be used on an unconstrained type argument.

For a type parameter `T` when the type argument is a nullable reference type `C?`, instances of `T?` are interpreted as `C?`, not `C??`.

> *Example*: The following examples show how the nullability of a type argument impacts the nullability of a declaration of its type parameter:
>
> <!-- Example: {template:"standalone-lib-without-using", name:"RepeatedNullable"} -->
> ```csharp
> public class C
> {
> }
>
> public static class Extensions
> {
> public static void M<T>(this T? arg) where T : notnull
> {
>
> }
> }
>
> public class Test
> {
> public void M()
> {
> C? mightBeNull = new C();
> C notNull = new C();
>
> int number = 5;
> int? missing = null;
>
> mightBeNull.M(); // arg is C?
> notNull.M(); // arg is C?
> number.M(); // arg is int?
> missing.M(); // arg is int?
> }
> }
> ```
>
> When the type argument is a non-nullable type, the `?` type annotation indicates that the parameter is the corresponding nullable type. When the type argument is already a nullable reference type, the parameter is that same nullable type.
>
> *end example*
The ***not null*** constraint specifies that a type argument used for the type parameter should be a non-nullable value type or a non-nullable reference type. A type argument that isn't a non-nullable value type or a non-nullable reference type is allowed, but the compiler may produce a diagnostic warning.
The value type constraint specifies that a type argument used for the type parameter shall be a non-nullable value type. All non-nullable struct types, enum types, and type parameters having the value type constraint satisfy this constraint. Note that although classified as a value type, a nullable value type ([§8.3.12](types.md#8312-nullable-value-types)) does not satisfy the value type constraint. A type parameter having the value type constraint shall not also have the *constructor_constraint*, although it may be used as a type argument for another type parameter with a *constructor_constraint*.
> *Note*: The `System.Nullable<T>` type specifies the non-nullable value type constraint for `T`. Thus, recursively constructed types of the forms `T??` and `Nullable<Nullable<T>>` are prohibited. *end note*
Expand Down Expand Up @@ -604,7 +657,7 @@ The ***effective interface set*** of a type parameter `T` is defined as follows
- If `T` has no *interface_type* constraints but has *type_parameter* constraints, its effective interface set is the union of the effective interface sets of its *type_parameter* constraints.
- If `T` has both *interface_type* constraints and *type_parameter* constraints, its effective interface set is the union of the set of dynamic erasures of its *interface_type* constraints and the effective interface sets of its *type_parameter* constraints.
A type parameter is *known to be a reference type* if it has the reference type constraint or its effective base class is not `object` or `System.ValueType`.
A type parameter is *known to be a reference type* if it has the reference type constraint or its effective base class is not `object` or `System.ValueType`. A type parameter is *known to be a non-nullable reference type* if it is known to be a reference type and has the non-nullable reference type constraint.
Values of a constrained type parameter type can be used to access the instance members implied by the constraints.
Expand Down Expand Up @@ -671,7 +724,7 @@ class_body

The modifier `partial` is used when defining a class, struct, or interface type in multiple parts. The `partial` modifier is a contextual keyword ([§6.4.4](lexical-structure.md#644-keywords)) and only has special meaning immediately before one of the keywords `class`, `struct`, or `interface`.

Each part of a ***partial type*** declaration shall include a `partial` modifier and shall be declared in the same namespace or containing type as the other parts. The `partial` modifier indicates that additional parts of the type declaration might exist elsewhere, but the existence of such additional parts is not a requirement; it is valid for the only declaration of a type to include the `partial` modifier.
Each part of a ***partial type*** declaration shall include a `partial` modifier and shall be declared in the same namespace or containing type as the other parts. The `partial` modifier indicates that additional parts of the type declaration might exist elsewhere, but the existence of such additional parts is not a requirement; it is valid for the only declaration of a type to include the `partial` modifier. It is valid for only one declaration of a partial type to include the base class or implemented interfaces. However, all declarations of a base class or implemented interfaces must match, including the nullability of any specified type arguments.

All parts of a partial type shall be compiled together such that the parts can be merged at compile-time. Partial types specifically do not allow already compiled types to be extended.

Expand Down Expand Up @@ -878,7 +931,7 @@ All members of a generic class can use type parameters from any enclosing class,
> class C<V>
> {
> public V f1;
> public C<V> f2 = null;
> public C<V> f2;
>
> public C(V x)
> {
Expand Down Expand Up @@ -1055,17 +1108,17 @@ Non-nested types can have `public` or `internal` declared accessibility and have
> private class Node
> {
> public object Data;
> public Node Next;
> public Node? Next;
>
> public Node(object data, Node next)
> public Node(object data, Node? next)
> {
> this.Data = data;
> this.Next = next;
> }
> }
>
> private Node first = null;
> private Node last = null;
> private Node? first = null;
> private Node? last = null;
>
> // Public interface
> public void AddToFront(object o) {...}
Expand Down
4 changes: 3 additions & 1 deletion standard/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2929,13 +2929,15 @@ When the operand of a *typeof_expression* is a sequence of tokens that satisfies
- Evaluate the resulting *type_name*, while ignoring all type parameter constraints.
- The *unbound_type_name* resolves to the unbound generic type associated with the resulting constructed type ([§8.4](types.md#84-constructed-types)).

It is an error for the type name to be a nullable reference type.

The result of the *typeof_expression* is the `System.Type` object for the resulting unbound generic type.

The third form of *typeof_expression* consists of a `typeof` keyword followed by a parenthesized `void` keyword. The result of an expression of this form is the `System.Type` object that represents the absence of a type. The type object returned by `typeof(void)` is distinct from the type object returned for any type.

> *Note*: This special `System.Type` object is useful in class libraries that allow reflection onto methods in the language, where those methods wish to have a way to represent the return type of any method, including `void` methods, with an instance of `System.Type`. *end note*
The `typeof` operator can be used on a type parameter. The result is the `System.Type` object for the run-time type that was bound to the type parameter. The `typeof` operator can also be used on a constructed type or an unbound generic type ([§8.4.4](types.md#844-bound-and-unbound-types)). The `System.Type` object for an unbound generic type is not the same as the `System.Type` object of the instance type ([§15.3.2](classes.md#1532-the-instance-type)). The instance type is always a closed constructed type at run-time so its `System.Type` object depends on the run-time type arguments in use. The unbound generic type, on the other hand, has no type arguments, and yields the same `System.Type` object regardless of runtime type arguments.
The `typeof` operator can be used on a type parameter. It is a compile time error if the type name is known to be a nullable reference type. The result is the `System.Type` object for the run-time type that was bound to the type parameter. If the run-time type is a nullable reference type, the result is the corresponding non-nullable reference type. The `typeof` operator can also be used on a constructed type or an unbound generic type ([§8.4.4](types.md#844-bound-and-unbound-types)). The `System.Type` object for an unbound generic type is not the same as the `System.Type` object of the instance type ([§15.3.2](classes.md#1532-the-instance-type)). The instance type is always a closed constructed type at run-time so its `System.Type` object depends on the run-time type arguments in use. The unbound generic type, on the other hand, has no type arguments, and yields the same `System.Type` object regardless of runtime type arguments.

> *Example*: The example
>
Expand Down
22 changes: 17 additions & 5 deletions standard/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,13 @@ delegate_type
;
nullable_reference_type
: non_nullable_reference_type '?'
: non_nullable_reference_type nullable_type_annotation
;
nullable_type_annotation
: '?'
;
```

*pointer_type* is available only in unsafe code ([§23.3](unsafe-code.md#233-pointer-types)). *nullable_reference_type* is discussed further in [§8.9](types.md#89-reference-types-and-nullability).
Expand Down Expand Up @@ -206,7 +211,7 @@ enum_type
;
nullable_value_type
: non_nullable_value_type '?'
: non_nullable_value_type nullable_type_annotation
;
```

Expand Down Expand Up @@ -538,10 +543,11 @@ type_arguments
type_argument
: type
| type_parameter nullable_type_annotation?
;
```
Each type argument shall satisfy any constraints on the corresponding type parameter ([§15.2.5](classes.md#1525-type-parameter-constraints)).
Each type argument shall satisfy any constraints on the corresponding type parameter ([§15.2.5](classes.md#1525-type-parameter-constraints)). A reference type argument whose nullability doesnt match the nullability of the type parameter satisfies the constraint; however a warning may be issued.
### 8.4.3 Open and closed types
Expand Down Expand Up @@ -720,7 +726,7 @@ An *unmanaged_type* is any type that isn’t a *reference_type*, a *type_paramet

### 8.9.1 General

A *nullable reference type* is denoted by appending a `?` to a valid non-nullable reference type name. There is no semantic difference between a non-nullable reference type and its corresponding nullable type. Both a nullable reference and a non-nullable reference can contain either a reference to an object or `null`. The presence or absence of the `?` annotation declares whether an expression is intended to permit null values or not. A compiler can provide diagnostics when an expression is not used according to that intent. The null state of an expression is defined in [§8.9.5](types.md#895-nullabilities-and-null-states). An identity conversion exists among a nullable reference type and its corresponding non-nullable reference type ([§10.2.2](conversions.md#1022-identity-conversion)).
A *nullable reference type* is denoted by appending a *nullable_type_annotation* (`?`) to a non-nullable reference type. There is no semantic difference between a non-nullable reference type and its corresponding nullable type, both can either be a reference to an object or `null`. The presence or absence of the *nullable_type_annotation* declares whether an expression is intended to permit null values or not. A compiler may provide diagnostics when an expression is not used according to that intent. The null state of an expression is defined in [§8.9.5](types.md#895-nullabilities-and-null-states). An identity conversion exists among a nullable reference type and its corresponding non-nullable reference type ([§10.2.2](conversions.md#1022-identity-conversion)).

There are two forms of nullability for reference types:

Expand All @@ -729,7 +735,7 @@ There are two forms of nullability for reference types:

> *Note:* The types `R` and `R?` are represented by the same underlying type, `R`. A variable of that underlying type can either contain a reference to an object or be the value `null`, which indicates “no reference.” *end note*
The syntactic distinction between a *nullable reference type* and its corresponding *non-nullable reference type* enables a compiler to generate diagnostics. A compiler shall allow the `?` annotation as defined in [§8.2.1](types.md#821-general). The diagnostics shall be limited to warnings. Neither the presence or absence of nullable annotations, nor the state of the nullable context can change the compile time or runtime behavior of a program except for changes in any diagnostic messages generated at compile time.
The syntactic distinction between a *nullable reference type* and its corresponding *non-nullable reference type* enables a compiler to generate diagnostics. A compiler must allow the *nullable_type_annotation* as defined in [§8.2.1](types.md#821-general). The diagnostics must be limited to warnings. Neither the presence or absence of nullable annotations, nor the state of the nullable context can change the compile time or runtime behavior of a program except for changes in any diagnostic messages generated at compile time.

### 8.9.2 Non-nullable reference types

Expand Down Expand Up @@ -762,6 +768,9 @@ When the nullable context is ***disabled***:
- No warning shall be generated when a variable of an unannotated reference type is initialized with, or assigned a value of, `null`.
- No warning shall be generated when a variable of a reference type that possibly has the null value.
- For any reference type `T`, the annotation `?` in `T?` generates a message and the type `T?` is the same as `T`.
- For any type parameter constraint `where T : C?`, the annotation `?` in `C?` generates a message and the type `C?` is the same as `C`.
- For any type parameter constraint `where T : U?`, the annotation `?` in `U?` generates a message and the type `U?` is the same as `U`.
- The generic constraint `class?` generates a warning message. The type parameter must be a reference type.
> *Note*: This message is characterized as “informational” rather than “warning,” so as not to confuse it with the state of the nullable warning setting, which is unrelated. *end note*
- The null-forgiving operator `!` ([§12.8.9](expressions.md#1289-null-forgiving-expressions)) has no effect.

Expand Down Expand Up @@ -839,6 +848,7 @@ When the nullable context is ***enabled***:
- For any reference type `T`, the annotation `?` in `T?` makes `T?` a nullable type, whereas the unannotated `T` is non-nullable.
- The compiler can use static flow analysis to determine the null state of any reference variable. When nullable warnings are enabled, a reference variables null state ([§8.9.5](types.md#895-nullabilities-and-null-states)) is either *not null*, *maybe null*, or *maybe default* and
- The null-forgiving operator `!` ([§12.8.9](expressions.md#1289-null-forgiving-expressions)) sets the null state of its operand to *not null*.
- The compiler can issue a warning if the nullability of a type parameter doesn't match the nullability of its corresponding type argument.
### 8.9.5 Nullabilities and null states
Expand All @@ -861,6 +871,8 @@ The ***default null state*** of an expression is determined by its type, and the
- Not null when its declaration is in text where the annotations flag is disabled.
- The default null state of a non-nullable reference type is not null.
> *Note:* The *maybe default* state is used with unconstrained type parameters when the type is a non-nullable type, such as `string` and the expression `default(T)` is the null value. Because null is not in the domain for the non-nullable type, the state is maybe default. *end note*
A diagnostic can be produced when a variable ([§9.2.1](variables.md#921-general)) of a non-nullable reference type is initialized or assigned to an expression that is maybe null when that variable is declared in text where the annotation flag is enabled.
> *Example*: Consider the following method where a parameter is nullable and that value is assigned to a non-nullable type:
Expand Down

0 comments on commit a7525ff

Please sign in to comment.