-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Field nullability revisions #9184
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
Conversation
- This is because when the user starts applying nullability attributes to the field, we no longer want to infer anything, we just want the nullability to be *what the user said*. | ||
- If the containing property has ***oblivious*** or ***annotated*** nullability, then the backing field has the same nullability as the property. | ||
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`) or has the `[NotNull]` attribute, and the property is ***null-resilient***, then the backing field has ***annotated*** nullability. | ||
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`) or has the `[NotNull]` attribute, and the property is ***not null-resilient***, then the backing field has ***not-annotated*** nullability. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the only difference this change makes is whether this becomes a constructor warning requiring initialization instead of a "possible null reference return" warning on get
?
[NotNull]
public string? Prop
{
get;
set => field = value ?? "";
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think so. And yes I decided that this isn’t a scenario where we need to do inference. Also, in the case of unconstrained T, inferring not nullable field may not be sufficient for things to work satisfactorily here. You’re already expressing things in a “wonky” way, go ahead and call it [AllowNull] string instead, or, attribute the field so that it works how you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
proposals/field-keyword.md
Outdated
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`) or has the `[NotNull]` attribute, and the property is ***null-resilient***, then the backing field has ***annotated*** nullability. | ||
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`) or has the `[NotNull]` attribute, and the property is ***not null-resilient***, then the backing field has ***not-annotated*** nullability. | ||
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`), and the property is ***null-resilient***, then the backing field has ***annotated*** nullability. | ||
- If the containing property has *not-annotated* nullability (e.g. `string` or `T`), and the property is ***not null-resilient***, then the backing field has ***not-annotated*** nullability. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider an alternative approach where we say that the backing field is always annotated if we get into this section. This doesn't change our ability to decide if the get
works with an unannotated return type, it just changes how we emit the field. I'm curious what specific things break, from a user and roslyn API perspective, if the field is always annotated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been turning this idea over in my mind quite a bit and I think this is actually going to simplify implementation and improve clarity.
it just changes how we emit the field
This seems fine. If we say a backing field of reference type is always nullable, when field
keyword is used, it would affect some reflection behaviors, but I would be surprised if it was anything that breaking. EF, for example, does not look at the backing field's nullability, only the property's nullability.
I'm curious what specific things break, from a user and roslyn API perspective, if the field is always annotated?
Basically it means that for field
in string Prop { get => field; }
, Quick Info shows string? Prop.field
, instead of string Prop.field
. I actually think this is a good thing. Currently the implementation always shows string Prop.field
, even when we inferred a nullable annotation for it.
The current design was based on the idea that if we just infer the nullable annotation, then constructor analysis will just do the right thing, in terms of knowing whether to enforce initialization of the property. If field is string?
, then no need for enforcement. If it is string!
, then need to enforce.
My stretch goal was to make public API show the inferred annotation. But if we do that, I'm concerned about "Schrodinger's null", where assigning maybe-null to the field
actually changes its type from string
to string?
. This seems confusing, as the point of saying its type is string
is to convey to the user that they are not allowed to put null in it.
If we go with "field is always annotated", then we need to adjust the constructor analysis design so that it uses the "null resilient" concept for these backing fields instead. "Null-resilient" means no need for enforcement. "Not null-resilient" means need to enforce. But that's not a big deal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the current design is to give the field the same nullability as the property, when any nullability attributes are used on the field. If we said the field is "always annotated when reference type", then we would probably want to do that even when attributes are used on the field.
This may break some existing cases where only a precondition or only postcondition attribute is used. Cases where both are specified (e.g. [DisallowNull, NotNull]
won't change. I do not think such a break is problematic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to pursue the on the implementation side but it just ended up being too much churn at that time. I ended up pushing a change to bring us back around to being "annotation-oriented" which reflects what we shipped.
@jnm2 @CyrusNajmabadi @333fred This PR now reflects the reality of what we shipped except that setter analysis is not implemented yet (dotnet/roslyn#77215). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me, thank you for the updates!
- If the get accessor is auto-implemented, the property is not null-resilient. | ||
|
||
The nullability of the backing field is determined as follows: | ||
- If the field has nullability attributes such as `[field: MaybeNull]`, `AllowNull`, `NotNull`, or `DisallowNull`, then the field's nullable annotation is the same as the property's nullable annotation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm glad we didn't go this route.
Co-authored-by: Joseph Musser <[email protected]>
- If the property is not *null-resilient*, the nullable annotation is *not-annotated*. | ||
|
||
> [!NOTE] | ||
> The inferred nullable annotation of the backing field is not exposed in the Roslyn symbol APIs due to the dependency on binding and flow analysis of methods. The inferred annotation is instead used only in nullable analysis (and surfaced indirectly through APIs such as GetSymbolInfo/GetTypeInfo for expressions.) The nullable annotation of the property is used instead in the symbol APIs. See also subsection of [Nullability alternatives](#infer-the-initial-flow-state-of-the-field). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really know if this note is appropriate for a csharplang spec?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to move it to any more appropriate location, I just want the fact recorded somewhere that it can be found in the future when investigating behavior of this feature area.
@333fred removed references to public API. I'm going to open a separate PR to Roslyn which adjusts a doc comment to include the information that I want to preserve related to this. |
That works for me, thanks! |
From simply trying the feature out I decided that 2-pass is needed. Also simplified some language relating to
[NotNull]
on the property which didn't seem to be necessary in practice.