field
keyword nullability
#8360
Labels
Proposal Question
Question to be discussed in LDM related to a proposal
field
keyword nullability
#8360
Related proposal: https://github.com/dotnet/csharplang/blob/main/proposals/field-keyword.md
PR containing the working version of this text: #8346
Goals
Ensure a reasonable level of null-safety for various usage patterns of the
field
keyword feature. Avoid requiring the user to apply[field: AllowNull, MaybeNull]
or similar attributes to the property in order to express common patterns around lazy initialization, etc.One of the key scenarios we would like to "just work", if possible, is the little-l lazy property scenario:
Terms
A property is a field-backed property if it meets any of the following conditions:
get;
/set;
/init;
).field
keyword within any accessor bodies.Some examples of field-backed properties include:
The variable denoted by the
field
keyword in a property's accessors is the backing field of that property.Nullability of the backing field
The backing field has the same type as the property. However, its nullable annotation may differ from the property. To determine this nullable annotation, we introduce the concept of null-resiliency. Null-resiliency intuitively means that the property's
get
accessor preserves null-safety even when the field contains thedefault
value for its type.A field-backed property is determined to be null-resilient or not by performing a special nullable analysis of its
get
accessor.field
is temporarily assumed to have annotated nullability, e.g.string?
. This causesfield
to have maybe-null or maybe-default initial state in theget
accessor, depending on its type.The nullability of the backing field is determined as follows:
[field: MaybeNull]
,AllowNull
,NotNull
, orDisallowNull
, then the field's nullable annotation is the same as the property's nullable annotation.string
orT
) or has the[NotNull]
attribute, and the property is null-resilient, then the backing field has annotated nullability.string
orT
) or has the[NotNull]
attribute, and the property is not null-resilient, then the backing field has not-annotated nullability.Constructor analysis
Currently, an auto-property is treated very similarly to an ordinary field in nullable constructor analysis. We extend this treatment to field-backed properties, by treating every field-backed property as a proxy to its backing field.
We update the following spec language from An alternative approach (TODO rename that section?) to accomplish this:
Note that this is essentially a constrained interprocedural analysis. We anticipate that in order to analyze a constructor, it will be necessary to do binding and "null-resiliency" analysis on all applicable get accessors in the same type, which use the
field
contextual keyword and have not-annotated nullability. We speculate that this is not prohibitively expensive because getter bodies are usually not very complex, and that the "null-resiliency" analysis only needs to be performed once regardless of how many constructors are in the type.Setter analysis
For simplicity, we use the terms "setter" and "set accessor" to refer to either a
set
orinit
accessor.There is a need to check that setters of field-backed properties actually initialize the backing field.
The initial flow state of the backing field in the setter of a field-backed property is determined as follows:
field = default;
.At each explicit or implicit 'return' in the setter, a warning is reported if the flow state of the backing field is incompatible with its annotations and nullability attributes.
Remarks
This formulation is intentionally very similar to ordinary fields in constructors. Essentially, because only the property accessors can actually refer to the backing field, the setter is treated as a "mini-constructor" for the backing field.
Much like with ordinary fields, we usually know the property was initialized in the constructor because it was set, but not necessarily. Simply returning within a branch where
Prop != null
was true is also good enough for our constructor analysis, since we understand that untracked mechanisms may have been used to set the property.Alternatives
In addition to the null-resilience approach outlined above, the working group suggests the following alternatives for the LDM's consideration:
Do nothing
We could introduce no special behavior at all here. In effect:
Note that this would result in nuisance warnings for "lazy property" scenarios, in which case users would likely need to assign
null!
or similar to silence constructor warnings.A "sub-alternative" we can consider is to also completely ignore properties using
field
keyword for nullable constructor analysis. In that case, there would be no warnings anywhere about the user needing to initialize anything, but also no nuisance for the user, regardless of what initialization pattern they may be using.Because we are only planning to ship the
field
keyword feature under the Preview LangVersion in .NET 9, we expect to have some ability to change the nullable behavior for the feature in .NET 10. Therefore, we could consider adopting a "lower-cost" solution like this one in the short term, and growing up to one of the more complex solutions in the long term.field
-targeted nullability attributesWe could introduce the following defaults, achieving a reasonable level of null safety, without involving any interprocedural analysis at all:
field
variable always has the same nullable annotation as the property.[field: MaybeNull, AllowNull]
etc. can be used to customize the nullability of the backing field.field
similarly to constructors.This would mean the "little-l lazy scenario" would look like this instead:
One reason we shied away from using nullability attributes here is that the ones we have are really oriented around describing inputs and outputs of signatures. They are cumbersome to use to describe the nullability of long-lived variables.
[field: MaybeNull, AllowNull]
is required to make the field behave "reasonably" as a nullable variable, which gives maybe-null initial flow state, and allows possible null values to be written to it. This feels cumbersome to ask users to do for relatively common "little-l lazy" scenarios.[field: AllowNull]
is used, suggesting to also addMaybeNull
. This is because AllowNull by itself doesn't do what users need out of a nullable variable: it assumes the field is initially not-null when we never saw anything write to it yet.[field: MaybeNull]
on thefield
keyword, or even fields in general, to allow nulls to also be written to the variable, as ifAllowNull
were implicitly also present.The text was updated successfully, but these errors were encountered: