Allow object initialization to work with nullable analysis #2830
Replies: 8 comments 2 replies
-
I would also like to see flow analysis be able to track which fields have been set prior to the instance escaping the method: public void M1() {
var r = new UserSearchRequest {
Company = "Foo",
Department = "Bar"
}; // no warning for not setting LastName here
// do some stuff here
M2(r); // warning, passing instance to a method before setting LastName
r.LastName = "Baz";
M2(r); // no warning
} |
Beta Was this translation helpful? Give feedback.
-
Note that the compiler already does such analysis for fields of structs. According to the spec, once all fields of a struct are assigned, it is considered definitely assigned: (string, string) t;
t.Item1 = "";
t.Item2 = "";
// t is definitely assigned here So this is actually consistent with the language. |
Beta Was this translation helpful? Give feedback.
-
Yes, that would make it even better! But I guess it could be done in a second step if necessary (I don't suppose removing spurious warnings could count as breaking changes) |
Beta Was this translation helpful? Give feedback.
-
I completely agree with this issue; I have several classes where members are initialized using reflection (similar to the Json deserialization in your example) and have the same issue. |
Beta Was this translation helpful? Give feedback.
-
I think this is a great idea, I have the same issue with the implementation of nullable reference types. The only solution that will work for me is Potential fix 3: Create a non-default constructor and that is just too much boiler plate. I'm going to revert to disabling nullable references for now. |
Beta Was this translation helpful? Give feedback.
-
I have a similar issue, but with dependency injection. Some information isn't known at the time of object construction, so it must be provided in a separate initialization step. But that again leads to a "What is allowed to be null?" scenario. (Aside: Some DI frameworks allow passing parameters when setting up, but Microsoft's does not.) Further, some of the init values are intended to be readonly. However I can't set Conceptually, I would like write-once properties, as opposed to readonly properties, and that the instance object only be considered valid once all the values that need to be initialized have been. The initialization may happen in the constructor, or by setting properties during object creation, or by some initialization function. Of course, that raises the question of how you can call an initialization function when the object itself isn't considered valid, but perhaps that can be resolved with attributes or a variant setter keyword.
And that finishes up setting any uninitialized properties. Basically, |
Beta Was this translation helpful? Give feedback.
-
See #3630 |
Beta Was this translation helpful? Give feedback.
-
Not sure why the attribute is needed. What's the advantage of the current mechanism where you must initialise as part of the declaration in order to avoid the warning? (and in no case can you seem to get a warning if you don't provide values as part of the initializer list when constructing?) |
Beta Was this translation helpful? Give feedback.
-
Background
I have just enabled nullable reference types, and they do seem to work fine in many cases. However, when it comes to members, they mostly work fine for types that are constructed using a constructor that sets all members based on parameters. At least in our codebase, that is actually a small minority of types. Most types are constructed with inline initialization or with deserialization.
A very common use-case for us is that service A creates a request object, serializes it to JSON, posts it to service B that deserializes it and uses it, eg:
The task is to get this code to compile without warnings, and without losing the benefits of nullability analysis.
Issues with the original (unmodified) code
The code gives warnings CS8618: Non-nullable property 'X' is uninitialized for
Company
,Department
, andLastName
in theUserSearchRequest
. Additionally, due to my accidentally commented out line, there will be an NRE when ServiceB eventually tries to accessreq.LastName
, but that is to be expected since I had a nullable warning.Potential fix 1: #nullable disable warnings
Of course, an easy "fix" is to just surround the definition of
UserSearchRequest
with#nullable disable warnings
.Pros:
Cons:
ServiceB.HandleFindUser()
Potential fix 2: Make all properties nullable (as the message suggests)
Pros:
ServiceB.HandleFindUser()
Con:
Sure, my
NullReferenceException
will be replaced by anInvalidOperationException
, but I have received no help from the compiler to find my accidentally commented out line. Also, since this is a lot of typing (and I "know" which fields must be set), I will probably end up doing justin which case I am no different off than if I just went with the
#nullable disable warnings
fixPotential fix 3: Create a non-default constructor
A third option is to add a constructor that initializes the required members:
Pros:
Cons:
to remove this assymetry, but it comes with the price of even more boilerplate.
While it does guard against the NRE, I have now introduced the risk of transposing arguments (thus ending up using the company as the department and vice versa), which is arguably a harder bug to find than the original NRE. This risk can be mitigated by always using named constructor arguments, but it requires discipline from the entire team.
I actually just silently broke the program!
System.Text.Json.JsonSerializer
cannot use non-default constructors.So in order for me to get rid of all warnings, have symmetry for all members, be able to deserialize successfully, and have nullable protection, my class would need to be something like
This is most likely doable, but it is a ridiculous amount of boilerplate, not to mention that all initializations of these request classes must be updated to use constructor syntax.
Proposed feature:
NullableUninitializedAttribute
I propose creating an attribute called
NullableUninitializedAttribute
. When a constructor has this attribute, it means that CS8618 is not generated for the constructor. But it means that CS8618 (or some other warning) is generated wherever this constructor is invoked, unless all non-nullable properties and fields are initialized in an object initializer. So relevant parts of my example become:For some slightly improved ergonomics, it could also be possible to add the attribute to the type:
which means that the attribute is applied to the automatically generated default constructor.
It should still be possible to specify this attribute on only some constructors:
Inheritance
If a constructor in a derived class invokes a
[NullableUninitialized]
constructor, a CS8618 is generated for any nullable member of the base type that is not definitely assigned in the derived constructor (unless the derived constructor is also marked with[NullableUninitialized]
).Possible nice-to-haves
Some initialized members
Perhaps it would be useful to specify that some members are initialized, but some need to be initialized by the caller:
Automatic insertion of this attribute
When the compiler today analyzes a constructor to determine which members to generate CS8618 for, it could instead use this information to automatically populate a
[NullableUninitialized]
attribute for the constructor. I guess this would be a breaking change since it would change where error messages appear, but I think it would be a great improvement for nullable adoption since object initialization is a very common pattern.Related issues and why I chose to create this issue even though they exist:
#2109: Not a very detailed proposal, and it is also buried.
#2328: I think the attribute belongs to constructors rather than the individual members. Also, that issue is buried with no reply since Jun 5.
#2825: I had already started writing this proposal when that issue was opened, and I think I provide more of a solution to the problem than that issue.
Beta Was this translation helpful? Give feedback.
All reactions