Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify generic constraints added to support nullable reference types in C# 8 #1178

Open
wants to merge 6 commits into
base: draft-v8
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 22 additions & 15 deletions standard/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,17 +412,18 @@ type_parameter_constraints
;
primary_constraint
: class_type
| 'class'
: class_type '?'?
| 'class' '?'?
| 'struct'
| 'notnull'
| 'unmanaged'
;
secondary_constraints
: interface_type
| type_parameter
| secondary_constraints ',' interface_type
| secondary_constraints ',' type_parameter
: interface_type '?'?
| type_parameter '?'?
| secondary_constraints ',' interface_type '?'?
| secondary_constraints ',' type_parameter '?'?
Comment on lines +423 to +426
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should review the grammar here. I can't add this as a suggested changed as it covers non-altered lines, so just code. The rules here pre-date the use of ANTLR, if we ANTLRize them and remove unneeded left-recursion (as we do elsewhere) we get:

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type '?'?
    | 'class' '?'?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type '?'?
    | type_parameter '?'?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

Optionally to improve readability we could replace the '?'? by adding a rule:

nullable_type_attribute
    : '?'
    ;

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type nullable_type_attribute?
    | 'class' nullable_type_attribute?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type nullable_type_attribute?
    | type_parameter nullable_type_attribute?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

The introduced * nullable_type_attribute* would then also need to be used in other grammar rules where the literal '?' is used as a type attribute and not in places where it is not, e.g. for the null conditional operations.

Copy link
Member Author

@BillWagner BillWagner Sep 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the second one. When I was working through the grammar, I found '?'? hard to mentally parse and read through the grammar.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for a rule for nullable_type_attribute.

;
constructor_constraint
Expand All @@ -434,16 +435,20 @@ 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 ***nullable reference type constraint*** `class?`, the ***not null*** constraint `notnull`, the ***value type constraint*** `struct` or the ***unmanaged type constraint*** `unmanaged`.

A secondary constraint can be a *type_parameter* or *interface_type*.
A secondary constraint can be a *type_parameter* or *interface_type*, either optionally followed by `?`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn’t read well to me in this context, given the comment on 426 above I’ll offer two alternatives:

Suggested change
A secondary constraint can be a *type_parameter* or *interface_type*, either optionally followed by `?`.
A secondary constraint can be a *type_parameter* or *interface_type*, either optionally nullable.

Or if the line 426 added rule is adopted:

Suggested change
A secondary constraint can be a *type_parameter* or *interface_type*, either optionally followed by `?`.
A secondary constraint can be a *type_parameter* or *interface_type*, optionally followed by a *nullable_type_attribute*.


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 reference type constraint specifies that a type argument used for the type parameter shall be a non-nullable reference type. All non-nullable class types, non-nullable interface types, non-nullable delegate types, non-nullable array types, and type parameters known to be a non-nullable reference type (as defined below) satisfy this constraint.

The nullable reference type constraint specifies that a type argument shall be either a non-nullable reference type or a nullable 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 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*
BillWagner marked this conversation as resolved.
Show resolved Hide resolved
The ***not null*** constraint specifies that a type argument used for the type parameter shall be a non-nullable value type or a non-nullable reference type. All non-nullable class types, interface types, delegate types, array types, struct types, enum types, and type parameters known to be a non-nullable value type or non-nullable reference type satisfy this constraint.

Because `unmanaged` is not a keyword, in *primary_constraint* the unmanaged constraint is always syntactically ambiguous with *class_type*. For compatibility reasons, if a name lookup ([§12.8.4](expressions.md#1284-simple-names)) of the name `unmanaged` succeeds it is treated as a `class_type`. Otherwise it is treated as the unmanaged constraint.

The unmanaged type constraint specifies that a type argument used for the type parameter shall be a non-nullable unmanaged type ([§8.8](types.md#88-unmanaged-types)).
Expand Down Expand Up @@ -604,7 +609,9 @@ 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 non-nullable reference type* if it has the non-nullable 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 non-nullable reference type constraint, reference type constraint or its effective base class is not `object` or `System.ValueType`.
Comment on lines +612 to +614
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be simplified to:

Suggested change
A type parameter is *known to be a non-nullable reference type* if it has the non-nullable 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 non-nullable reference type constraint, 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 has the non-nullable 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 known to be a non-nullable reference type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its => it's in the suggestion (line 614) - but then I like it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're not to use contractions, so "it is".

Values of a constrained type parameter type can be used to access the instance members implied by the constraints.
Expand Down Expand Up @@ -878,7 +885,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 = null!;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unclear to me why in the original f2 was initialised, and f1 not, when both are set in the constructor. Now a suppression has been added…

I suggest a comment explaining the suppression, or the use of default (which leaves the explanation for the section on default to handle), or just drop the initialisation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to drop the initialization.

>
> public C(V x)
> {
Expand Down Expand Up @@ -1055,17 +1062,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
28 changes: 27 additions & 1 deletion standard/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ type_argument
;
```

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 type argument whose nullability doesn't match the nullability of the type parameter satisfies the constraint with the exception that a nullable value type does not satisfy the value type constraint. A warning may be issued when the nullability of a type argument does not satisfy the nullability requirements of the constraint.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think this needs to be phrased in terms of value type exceptions, the point being that reference type nullability is a “soft” constraint. Maybe:

Suggested change
Each type argument shall satisfy any constraints on the corresponding type parameter ([§15.2.5](classes.md#1525-type-parameter-constraints)). A type argument whose nullability doesn't match the nullability of the type parameter satisfies the constraint with the exception that a nullable value type does not satisfy the value type constraint. A warning may be issued when the nullability of a type argument does not satisfy the nullability requirements of the constraint.
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.

I see that @KalleOlaviNiemitalo has commented on the use of “shall” here, I'll defer that to @RexJaeschke.


### 8.4.3 Open and closed types

Expand Down Expand Up @@ -601,8 +601,20 @@ A type parameter is an identifier designating a value type or reference type tha
type_parameter
: identifier
;

nullable_type_parameter
: non_nullable_non_value_type_parameter '?'
;

non_nullable_non_value_type_parameter
: type_parameter
;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This grammar is wrong/pointless, as @KalleOlaviNiemitalo has commented the added rules are not used anywhere.

Unfortunately I’m not sure what the intended semantics are here.

Guessing: is the addition of the nullable type attribute meant to require the type argument to be a nullable reference and/or value type, i.e. it’s an additional kind of primary_constraint or secondary_constraint?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my mistake. I added the additional rules in the latest commit.

Note that the grammar additions for non_nullable_reference_type and nullable_reference_type are in #1089

```

The *non_nullable_non_value_type_parameter* in *nullable_type_parameter* shall be a type parameter that isn’t constrained to be a value type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"shall" can give the wrong idea that the compiler would report an error otherwise. Could clarify with a note saying that the same syntax can be used with a type parameter that is constrained to be a value type, but it means Nullable<T> in that case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also waiting for more eyes here. I see your point, but I don't have the right suggestion yet.


In *nullable_type_parameter*, the annotation `?` indicates the intent that nullable type corresponding to the type arguments of this type are nullable. The absence of the annotation `?` indicates the intent that type arguments of this type are non-nullable.

Since a type parameter can be instantiated with many different type arguments, type parameters have slightly different operations and restrictions than other types.

> *Note*: These include:
Expand Down Expand Up @@ -705,3 +717,17 @@ An *unmanaged_type* is any type that isn’t a *reference_type*, a *type_paramet
- `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `char`, `float`, `double`, `decimal`, or `bool`.
- Any *enum_type*.
- Any user-defined *struct_type* that is not a constructed type and contains instance fields of *unmanaged_type*s only.

## §Generics-and-nullable-placeholder More nullable context text

> This is a placeholder for text that will be added in the clause on "nullable context" described in `#1124`. I'll rebase and edit once that's done.

- Add the note for *maybe default*:

> *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*

Add to rules on types and nullable context flags:

When the *annotations* flag is disabled:

- The `class?` constraint generates a warning.
Loading