Defaultable Value Types #5337
Replies: 3 comments 3 replies
-
Could the Opt-in annotation also work for [MaybeDefault]
enum E
{
A = 1, B, C
}
class Program
{
static void M1(E e) { }
static void M2(E~ e) { }
static void Main()
{
// warning
M1(0);
M1(default);
// no warning
M1(E.A);
M2(0);
M2(default);
}
} This example can be replaced by nullable |
Beta Was this translation helpful? Give feedback.
-
Is it possible make auto- and semi-auto-properties implicitly annotated? If possible, wouldn't significant percentage of defaultable analysis be solved? |
Beta Was this translation helpful? Give feedback.
-
The nullability improvements working group discussed this feature yesterday. |
Beta Was this translation helpful? Give feedback.
-
The following is an incomplete draft proposal. I have been thinking about this problem for a while now, but I haven't been able to come up with a proposal that addresses all the complications of the feature. The sections "Properties" and "Transitioning from previous C#" in particular point out some issues that need to be addressed before the proposal can go to LDM. Any feedback or suggestions you have would be appreciated.
Defaultable Value Types
Structs containing non-nullable reference types are a known hole in nullable analysis.
Previous discussion
Context
It's never been possible to truly encapsulate a struct's fields. Some behaviors affected by the set of fields in a struct include:
If we follow the patterns set by these analyses, we could arrive at a solution for how to nullable analyze structs.
Defaultable annotation
When a struct type is used, the user indicates whether the struct is nullable-initialized using the "defaultable" annotation.
We need to choose a token to serve as this annotation. We can't use
?
because that already meansSystem.Nullable
, and we don't think we can use??
because it introduces too much parsing ambiguity with the??
binary operator. Therefore, for this proposal we use the annotation~
.If no annotation is used on the struct type, it is assumed to have all its non-nullable reference type fields initialized at all levels of nesting.
If a "defaultable" annotation is used, all non-nullable reference type fields in the struct might contain null.
If the flow analysis finds that all non-nullable reference type fields contain non-null values, we permit conversion from an "annotated" to "non-annotated" struct type. This is probably accomplished by tracking a flow state for the struct variable itself.
~
will flow through type argument inference as expected.~
is an all-or-nothing thing, so we won't be producing a distinctTypeWithState
for every combination of field flow states for a struct, for example. This is because we would prefer to sidestep the possibility of indicating which individual fields on a member's inputs and outputs may be null or not--it invites some very complex type system problems.?
on unconstrained generics will "turn into"~
when the type parameter is substituted with a value type.Flow analysis attributes
It will be necessary to have an
AllowDefault
parallel toAllowNull
, and similarly with other common flow analysis attributes. Alternatively, we could adjust the meaning of existing attributes. For example,AllowNull
could mean "allow default" when used with value types, it would be associated with the 'this' parameter when applied to methods, properties, etc.Opt-in
If a struct has no non-nullable reference type fields, then it is generally convertible from its "defaultable" to "non-defaultable" form. However, there are other structs out there which might benefit from an annotation which allows the struct to pretend that it does contain non-nullable reference type fields, and therefore can participate in nullable flow analysis.
In this case, the state of the struct wouldn't be determined by the state of its fields, but rather by annotations on members it is used with, etc.
operator default
There are multiple reasons we might want an
operator default
in the language.ImmutableArray<int> arr = default; arr is [1]
throws an exception.It might be reasonable to enforce that
operator default
only returnsfalse
in a code path where all non-nullable reference fields contain non-null values. Given this enforcement (via warning), we could assume the automatic notion of "non-default" is compatible the user-defined notion. However, if the user applies the "opt-in" attribute to the struct type, we might want to avoid any enforcement on the implementation, and try our best to just get out of the user's way.Open question: should the user also be required to define operators
==
/!=
if they defineoperator default
?"defaultable" reference types?
This road leads to giving users the ability to define "null-like" values for non-value types. The Unity GameObject scenario is the biggest use case we are aware of currently.
This proposal does not include changing the analysis of reference types in this way, but it's good for us to have confidence that "defaultable value types" doesn't put us into a corner when it comes to reference types.
In a nutshell: It feels risky to introduce distinct "maybe-null" and "maybe-default" flow states for reference types. Rather, to solve the Unity scenario it might make more sense to introduce
operator null
for reference types, and allow the user to define how the language decides if something is null for the purposes ofis null
,??
,?.
, etc. Effectively,operator default
would be exclusive to value types andoperator null
would be exclusive to reference types.Properties
OK, now for the hard part. Properties are supposed to be opaque, but they are known to frequently be simple encapsulations of fields. Definite assignment treats properties as truly opaque, and that provides an acceptable experience, particularly since assigning
default
/new()
will always definitely assign all struct fields. However, it feels like a usable "defaultable struct types" feature will also need to be able to do the right thing in a scenario like the following:Ideally, we would like for it to also understand the equivalent manually implemented properties:
The only practical way to do this is to use an annotation on the property. What would that look like? We probably wouldn't want something like the following, since it feels like the signature is oriented around the "uncommon" scenario rather than the "main" scenario, and it probably doesn't do the right thing in all scenarios:
We would also want to appropriately handle the following additional scenarios (among others!):
The property throws if a non-nullable field is not initialized.
The property encapsulates a nested field, and a different property directly exposes the containing field.
The problem essentially is:
Interaction with "required properties"
It's not completely clear how this feature interacts with the "required properties" feature. For example, if a struct contains required fields or properties, should it be allowed to simply take the
default
value of the struct and pass it around? Is a struct considered "initialized" once all its non-nullable fields and required members have been assigned? Is there additional expressiveness/enforcement needed to say "no, you need to run the constructor and assign the required properties to use this struct"?Transitioning from previous C#
Another major problem with this proposal is that it's getting more difficult over time for us to allow new nullable diagnostics. We need to explore whether there is a straightforward way to make it so the new analysis level is "opt-in", and that people who update their SDK without updating to the latest language version won't be disrupted by a bunch of new warnings.
We may have to consider things like making flow analysis behavior vary depending on language version, or placing specific new diagnostics into a new warning wave.
Beta Was this translation helpful? Give feedback.
All reactions