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

C# 8 Nullable reference types: Define different reference types: oblivious, nullable, and non-nullable #1105

Closed
wants to merge 9 commits into from

Conversation

BillWagner
Copy link
Member

@BillWagner BillWagner commented May 9, 2024

Related to #700

Fixes #1089

Add text to define the different "flavors" of reference types: "Nullable oblivious" (class reference types), "nullable reference types", and "non-nullable reference types".

Commit history is less clean than I'd like. I'd start thinking that one PR for the contexts #nullable changes and the types would be fine. That was getting larger. So I split the additional changes for #nullable into #1108

@jskeet
Copy link
Contributor

jskeet commented May 9, 2024

Worth labeling for discussion next week, or is it too early for that?

@BillWagner
Copy link
Member Author

Worth labeling for discussion next week, or is it too early for that?

I'll add the label after making edits. I hope we can discuss it next week.

@BillWagner
Copy link
Member Author

@jskeet @RexJaeschke

As I start on this, I wonder if a separate PR that updates all test examples so they generate the existing output in a nullable context is a good idea. It would separate out that work into one PR to review.

If you both agree, I'll add that PR for Wednesday as well.

@BillWagner BillWagner force-pushed the nullable-preprocessor branch from 89e8279 to 01b1bd7 Compare May 9, 2024 15:48
@jskeet
Copy link
Contributor

jskeet commented May 9, 2024

I'm not entirely sure whether that's a good idea. It might end up making a lot of examples that have nothing to do with nullability very null-syntax heavy. But I could be wrong.

Maybe make a start but see how you feel without doing it for all examples?


> *Note*: Here, “should” is used rather than “shall,” as this a declaration of intent. The presence of a `?` suffix is not enough to make it a non-nullable type; for that, the annotation context has to be enabled. If it is not, the `?` is ignored, and the type is nullable. *end note*

Unlike with nullable value types, where value types `V` and `V?` denote different types, given a reference type `R`, the notations `R` and `R?` denote the exact same type; the difference in their notations indicates, at compile time, only the intent of their usage, and allows for static flow analysis. Unlike a nullable value type, a nullable reference type has no relationship to the type `System.Nullable<T>`.
Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo May 9, 2024

Choose a reason for hiding this comment

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

The "exact same type" wording feels troublesome. If the ? nullable-reference-type annotation is used in an alias

#nullable enable

using ListOfNullableString = System.Collections.Generic.List<string?>;

class C
{
    // warning CS8619
    System.Collections.Generic.List<string> sl = new ListOfNullableString();
}

then the standard needs some wording to specify that the initializer of the field sl gets both its type and its nullability annotations from the alias ListOfNullableString. Currently, §14.5.2 (Using alias directives) just says "the identifier introduced by the using_alias_directive can be used to reference the given namespace or type" and does not mention nullability.

The alternative would be to specify R and R? as different types that have two-way identity conversions between them, like there are between object and dynamic, or between similar tuple types. But perhaps there's something about flow analysis that makes this not possible.

#nullable enable

class TypeArgumentDeductionWithIdentityConversion
{
    static void OneNrt(string? a, string b)
    {
        N(ref a, ref b); // warning CS8601
    }
    
    static void TwoNrt(string? a, string? b)
    {
        N(ref a, ref b);
    }
    
    static void ObjectVsDynamic(object[] a, dynamic[] b)
    {
        N(ref a, ref b);
    }
    
    static void TupleElementNames((int X, int Y) a, (int Z, int W) b)
    {
        N(ref a, ref b);
    }
    
    static void N<T>(ref T a, ref T b)
    {
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

We're thinking that the identity conversion path is the correct one. (We haven't speced out the warning section yet, but there are other identity conversions that can produce warnings).

@BillWagner BillWagner force-pushed the nullable-preprocessor branch from 7d15798 to dcb3731 Compare May 9, 2024 17:37
@RexJaeschke
Copy link
Contributor

@BillWagner As we discussed privately recently, I'm OK with spinning off the examples into a separate PR. I recall there were many, and I applied them in several large commits.

@BillWagner
Copy link
Member Author

As we discussed privately recently, I'm OK with spinning off the examples into a separate PR. I recall there were many, and I applied them in several large commits.

@RexJaeschke @jskeet

I'm leaning that way now too! I updated one sample in this PR because it made sense to show the nullable annotations as part of this PR. That didn't compile in the sample extractor, because nullable was off. Then, when I turned it on, several things failed.

The best way to handle this may be starting with #1106. In that PR, I set the Nullable flag to annotations. Then, as we add other PRs for the nullable reference type feature, we can show it, and it will compile in our test harness. That scopes the changes so that PRs are easier to review.

Then, later, we can change the setting to enable and address any warnings as we see fit.

@BillWagner BillWagner force-pushed the nullable-preprocessor branch from 2cf37c0 to f84f365 Compare May 9, 2024 19:40
@BillWagner
Copy link
Member Author

Note: the experiment I tried in this PR and #1106 worked. In #1106, all existing samples compile with the same result as before. And in this PR, I was able to add a nullable annotation where needed. Let's make that the plan of record and move forward.

@BillWagner BillWagner force-pushed the nullable-preprocessor branch 2 times, most recently from bbff3bc to a9a124e Compare May 9, 2024 21:51
@BillWagner BillWagner force-pushed the nullable-preprocessor branch from 4dab247 to d9a83b7 Compare May 10, 2024 20:49
@BillWagner BillWagner changed the title Define nullable contexts Define different reference types: oblivious, nullable, and non-nullable May 10, 2024
@BillWagner BillWagner changed the title Define different reference types: oblivious, nullable, and non-nullable C# 8 Nullable reference types: Define different reference types: oblivious, nullable, and non-nullable May 10, 2024

Every line of source code has a ***nullable annotation context*** and a ***nullable warning context***. The former controls whether nullable annotations (§Nullable-Annotation-Context) have effect; the latter controls whether nullable warnings (§Nullable-Warning-Context) are issued by the compiler. The state of each of the annotation and warning contexts of a given line is disabled or enabled.

Both nullable contexts may be specified within source code via nullable directives (§Nullable-Directives) and/or via some implementation-specific mechanism external to the source code. If both approaches are used, nullable directives supersede the settings made via an external mechanism.
Copy link
Member Author

Choose a reason for hiding this comment

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

This is causing the section renumber failure. That PR should merge first, then this can be updated as needed.


Both nullable contexts may be specified within source code via nullable directives (§Nullable-Directives) and/or via some implementation-specific mechanism external to the source code. If both approaches are used, nullable directives supersede the settings made via an external mechanism.

The default state for the nullable annotation context and the nullable warning context is implementation defined.
Copy link
Member Author

Choose a reason for hiding this comment

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

Rex had originally written this to say the default state is "disabled". I prefer implementation defined, so in the future, if the implementation changes, it won't require changing the standard. (Yes, it's already documented for roslyn).

@BillWagner BillWagner added the meeting: discuss This issue should be discussed at the next TC49-TG2 meeting label May 14, 2024
@BillWagner
Copy link
Member Author

@jskeet

Adding the meeting discuss label. I don't think this is ready to merge. But, we should discuss terms, definitions, and concepts so that the edits can be refined after this next meeting.

standard/types.md Outdated Show resolved Hide resolved
standard/types.md Outdated Show resolved Hide resolved
standard/types.md Outdated Show resolved Hide resolved
- *nullable*: A *nullable-reference-type* can be assigned `null`. It's default null state is *maybe-null*.
- *non-nullable*" A *non-nullable reference* should not be assigned a `null` value. Its default null state is *not-null*.

Unlike with nullable value types, where value types `V` and `V?` denote different types, given a reference type `R`, the notations `R` and `R?` denote the same underlying type; the difference in their notations indicates, at compile time, only the intent of their usage, and allows for static flow analysis. An identity conversion exists among a nullable reference type, its corresponding non-nullable reference type, and its corresponding null-oblivious reference type (§10.2.2).
Copy link
Contributor

Choose a reason for hiding this comment

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

We've said they're different types - I think we should reword "same underlying type", but it will require careful wordsmithing. We should see what we say about dynamic.

Copy link
Contributor

Choose a reason for hiding this comment

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

@jskeet – Does the above say they are different types? We have:

The terms "nullable reference type" and "non-nullable reference type" refer to annotations that declare whether an expression is intended to permit null values or not.

This language suggests there is one type which can have different annotations, not that there are multiple distinct types.

Maybe the term “underlying type” should not be overloaded to deal with (non-)nullable reference types? Given the text is leaning on annotations maybe a better term to use here is “non annotated type” (is it non annotated, nonannotated or unannotated?). This would give:

[…] given a reference type R, the notations R and R? denote the same non annotated type […]

@@ -27,10 +27,15 @@ For convenience, throughout this specification, some library type names are writ

### 8.2.1 General

A reference type is a class type, an interface type, an array type, a delegate type, or the `dynamic` type.
A reference type is a class type, an interface type, an array type, a delegate type, or the `dynamic` type. For each non-nullable reference type, there is a corresponding nullable reference type noted by appending the `?` to the type name.
Copy link
Contributor

Choose a reason for hiding this comment

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

And a corresponding null-oblivious type? Later on we seem to be treating them as three different types. Let's discuss how we want to describe this, and then try really hard to be consistent.

standard/types.md Show resolved Hide resolved
standard/types.md Show resolved Hide resolved
standard/types.md Show resolved Hide resolved
standard/types.md Show resolved Hide resolved
standard/types.md Outdated Show resolved Hide resolved
The first commit ports in the text from dotnet#700 substantially without change. The only editorial change is to move the clause on "Nullable directives" in lexical-structure before the clause on Pragma directives.
This will likely cause new failures, as I turned on nullable annotations and warnings in one of the templates.

Update code samples for the template in use to remove nullable warnings.

I did this as we plan to have nullable annotations on by default in this version of the spec.
This reverts commit dcb3731.
If the samples compile cleanly in this mode, I can continue in this PR without breaking all the samples.
First draft at language for variations of nullability and reference types.
@BillWagner BillWagner force-pushed the nullable-preprocessor branch from ff073bd to 0efff98 Compare May 29, 2024 19:21
standard/types.md Outdated Show resolved Hide resolved
Co-authored-by: Jon Skeet <skeet@pobox.com>
@BillWagner
Copy link
Member Author

PR #1124 is an alternative that removes the type "null oblivious".
Either this PR or #1124 should be merged.

@BillWagner BillWagner marked this pull request as ready for review May 30, 2024 14:43
Copy link
Contributor

@Nigel-Ecma Nigel-Ecma left a comment

Choose a reason for hiding this comment

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

Overall, as my comments undoubtedly show, I’m not sure it is yet clear what the boundary is between what an implementation (and we probably shouldn't say “compiler” here) must do and may do to be conformant.

- *nullable*: A *nullable-reference-type* can be assigned `null`. It's default null state is *maybe-null*.
- *non-nullable*" A *non-nullable reference* should not be assigned a `null` value. Its default null state is *not-null*.

Unlike with nullable value types, where value types `V` and `V?` denote different types, given a reference type `R`, the notations `R` and `R?` denote the same underlying type; the difference in their notations indicates, at compile time, only the intent of their usage, and allows for static flow analysis. An identity conversion exists among a nullable reference type, its corresponding non-nullable reference type, and its corresponding null-oblivious reference type (§10.2.2).
Copy link
Contributor

Choose a reason for hiding this comment

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

@jskeet – Does the above say they are different types? We have:

The terms "nullable reference type" and "non-nullable reference type" refer to annotations that declare whether an expression is intended to permit null values or not.

This language suggests there is one type which can have different annotations, not that there are multiple distinct types.

Maybe the term “underlying type” should not be overloaded to deal with (non-)nullable reference types? Given the text is leaning on annotations maybe a better term to use here is “non annotated type” (is it non annotated, nonannotated or unannotated?). This would give:

[…] given a reference type R, the notations R and R? denote the same non annotated type […]

standard/types.md Show resolved Hide resolved
standard/types.md Show resolved Hide resolved
standard/types.md Show resolved Hide resolved
standard/types.md Show resolved Hide resolved
standard/types.md Show resolved Hide resolved
@BillWagner
Copy link
Member Author

Closing in favor of #1124

@BillWagner BillWagner closed this Jul 10, 2024
@BillWagner BillWagner deleted the nullable-preprocessor branch July 10, 2024 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
meeting: discuss This issue should be discussed at the next TC49-TG2 meeting
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Nullable Reference Types] Nullable, Non-nullable, and "Nullable oblivous" reference types
5 participants