-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Indicate that a non-nullable Field must be initialized where the type is constructed #2328
Comments
In Kotlin this feature is called class C {
lateinit val str : String;
} Basically it says that the property is not properly assigned by the constructor but will be assigned at a later point. |
Great idea, but there's some discussion points that should be addressed quickly. What of internal initialization methods? class C
{
[MustInitialize("DefaultConstructor")]
public string Str { get; private set; }
public C() { }
[Initializes("Str")]
public void Init(CHelper helper)
{
Str = helper.GetHello();
}
} Similarly, what of external methods that might allow or disallow uninitialized values? class C
{
[MustInitialize("DefaultConstructor")]
public string Str { get; set; }
}
static void Main()
{
C c = new C();
UseString(c); //warning?
SetString(c); //should not be a warning?
UseString(c); //no warning?
}
[Initializes("c.Str")]
static void SetString(C c)
{
c.Str = "Hello!";
}
[MustBeInitialized("c.Str")]
static void UseString(C c)
{
Console.WriteLine("String length is: " + C.Str.Length);
} For the record, I would assume that this proposal strictly deals with immediate initialization by the caller, and that these considerations are not within the intended scope. |
@DarthVella |
I should have mentioned that the attributes were only to show the intent, not to suggest that that level of user input was necessary. |
I'm generally supportive of the case to have finer granularity over what fields should get initialized by the caller, but I think there's a lot to be said to also have an attribute that can simply cover the whole type in one go. It could be the same, if an appropriate name is available, but my point is that code looks tidier with less attributes all over the place. EDIT: I just realized there's one more caveat that would need to be filled for that to work smoothly: Externally generated values. A typical EF entity would have the ID generated when added to the database, and it'd be quite annoying to see warnings for those. 🤔 |
Brainstorming another potential name for this attribute: |
All examples so far were shown using a String field, but I'm certain this could very well apply to value types as well. It seems to me that, since value types have an automatic default constructor, this would ensure proper initialization of nested members. Ex.:
Does this seem like a proper usage? It seems to fit in with the spirit of annotating what must be initialized by the caller. |
@ZeroUltimax |
#146 is about the usage of default() for all instances of the structure. Here, I'm suggesting that in some cases, you might want to enforce initialization a member, which happens to be a value type. |
@ZeroUltimax |
I think I'm going to close my #2411 (as a duplicate). class A{
public string str; //do not warn "not initialized"
}
void Main(){
A a1 = new A(); //do warn: "str is not initialized"
A a2 = new A{str="str"}; //ok
} that becomes a bit tricky with constructors, I suggest this class A{
public string str1;
public string str2;
public A(){
this.str1="123";
} //warn: "str2 is also must be initialized"
} Just a suggestion |
The fact that this proposal tries to address several different problems (DTOs, object initializers, struct fields) may make things more complicated than they need to be...
Tracking whether a field has been initialized or not seems like an extremely heavy ask... While it's relatively easy to imagine this happening within a single method, once an object starts crossing method (and assembly!) boundaries it's hard to imagine how this would be done. If I receive some instance from a method somewhere, how is the compiler supposed to know which fields on it have been initialized, and which haven't? In other words, while this could solve the object initializer problem (because initialization occurs within a single function), it's hard to imagine how this could address the general problem of deserialized DTOs, or knowing that some field on a struct is uninitialized. I've opened #2452 as an alternative proposal to address the same problems, but hopefully in a simpler way. In a nutshell, this would be a syntactic sugar attribute on auto-properties which produces the following: private Foo? _foo;
public Foo Foo
{
get => _foo ?? throw new InvalidOperationException($"The property has not been initialized");
set => _foo = value;
} This makes the above 3 scenarios work without any additional compiler tracking, but of course doesn't catch cases where a field is accessed prematurely at compile-time. This seems acceptable to me, considering that prematurely accessing a late-initialized field goes against the API contract. |
Definite assignment already does this, and the legwork is literally already in place. I'm not going to say extending definite assignment analysis to definite initialistion analysis is trivial, but it's not a huge issue.
In the long run this could be done by attributes if necessary. However I don't think that's necessarily the point here. The idea is to give you more safety than you currently have. It allows you to safely say these fields won't be initialised by the producer, and must be initialised by the consumer. In 95% of cases that will be done immediately upon creation of the object. The other 5%? That's what the ! operator is for.
The idea is that certain objects must always be initialised by the person who creates the objects. However the serialisation framework used may not work with constructors, and so object Initialisers must be used. The serialisation framework itself works through reflection, and so these warnings are irrelevant to them.
I don't think it does. That's about providing runtime safety. The NRT feature (and this proposal) is about providing compile time safety. |
You mean within the same method, right? If so then you're right, it's essentially already being done.
How? We don't know at compile-time which fields are going to be initialized - that may be conditional on some runtime behavior...
I do agree that something is needed that's better than what we currently have, and I think it makes sense to be able to say "this will be initialized later" (as I also proposed in #2452). However, trying to enforce or detect that by the compiler is a different business... I don't think see how the ! operator could be relevant here - the way I understand it, we're discussing only non-nullable fields here, and not nullable ones. We're just trying to manage the initialization of those non-nullable fields better. However, looking at it another way, it seems like it's possible to first introduce an attribute as in #2452, without any compile-time tracking, and at some point in the future to add the tracking you propose as an additional improvement (although again, I don't see this working outside of the scope of a single method).
I understand the idea, but in reality it seems difficult to define "the person who creates the objects". Typical deserialization/materialization logic instantiates the object in one method, passes it to another one for populating the properties... I believe that initialization tracking that stops at the method boundary is likely to be of little value in many cases. More importantly, I'm not sure that there's that much value in tracking this in compile-time. The end user is supposed to already receive instances which have been fully initialized - so they don't really need our help - and the people writing the deserialization code are less likely to need our help tracking this.
I understand what you're saying, and #2452 wouldn't provide an ideal situation. But it's worth keeping in mind that NRT is supposed to provide compile-time safety around nullability, not around initialization - that's a very important distinction. I'm also trying to be pragmatic here. The chance of a full-fledged compile-time initialization-tracking being introduced in the near future is, well, quite low (and again, I don't believe it could be meaningful as initialization can depend on runtime behavior). #2452 could be implemented pretty easily, and would it make it just as easy to write DTOs as your proposal. I'm concerned less with providing absolute compile time safety, but rather with the fact that the NRT uninitialized warnings make it very difficult/verbose to write DTOs and similar objects in the NRT world, to the point where people are likely to just turn nullability off. |
One more note about your proposal to use attributes to express the initialization status of fields on returned types... The NullableAttribute introduced for NRTs exists on the property of a type. The initialization status, however, would have to be defined by each and every method on the types they return, which is something very different (and much more verbose). So Foo() could return type X with property P initialized, while Bar() could return X with P uninitialized... |
I don't imagine anything as complicated as that. As I stated above, in 95% of cases we initialise an object in the method in which it is declared. In most of the other 5% we may pass it to one or two helpers two initialise it. We can mark the specific helpers with an attribute indicating they initialise the struct manually. |
That may be true of object initializers (in fact, in that case it's even 100%), but I strongly disagree that 95% of all relevant initialization cases happen within a single method. Once again, DTOs and various other types are frequently instantiated here, populated there, etc. etc. In general, the value of providing compile-time warnings about accessing uninitialized fields only within the same method simply seems pretty low to me.
You seem to have a very specific/restricted scenario in mind... For a feature that goes so deep into the language/compiler, it feels like a more complete/comprehensive proposal is appropriate. Finally, as I wrote above, there's nothing wrong with introducing the attribute as a syntactic sugar for runtime checks and warning suppression (#2452) - this solves the immediate difficulty of writing DTOs and similar objects in the NRE world. We can then investigate other compile-time options for that same attribute in the future... |
I like the caller part because it makes it clearer who is supposed to initialize it, but to bikeshed this further, I'd go with However, this is probably not that important as long as the associated warning message is sufficiently clear, e.g.:
(This probably shouldn't get the same warning code CS8618, as the expectation of the developer is different.) |
To clarify an earlier comment. The I found myself using this a lot during unit testing and in rare occasions where a DI container could only initialise my types through property injection. The case for using object initialisers over calling the constructor is weak, however, I can see its usefulness in not breaking existing code.
effectively becomes
|
That sounds more like #2452 |
Please don't do this! Initialization results in many smelly C# libraries and NRT is a huge opportunity to clean this up. It would be a missed opportunity if these workarounds are introduced.
So much better! The first example is unsafe and it's undiscoverable how to construct a valid object. The second is done right. As a result NRTs will improve APIs across the .Net ecosystem.
These ORMs/Serializers are badly structured. But that doesn't matter. What matters is they require a class with a default constructor which doesn't construct a valid object, and after doing various mutations they may guarantee that they have produced a valid object. This is OK. The ORM/Serializers are the ones who face the warnings. The warnings don't seep into user code unless the user is using the default constructor, which should generate an NRT warning (see below).
This is indeed a problem but the solution is to warn on using the default constructor. This would then encourage the author of the struct to give a constructor which creates a valid object. |
NRTs will improve either way. This proposal by Yair means you'll receive a warning at compile time. Suppose the library has: public class C
{
[MustInitialize]
public string Str1 {get; set;}
[MustInitialize]
public string Str2 {get; set;}
} And your code does: |
The LDC have been very clear all this time that they do not intend to boil the ocean. How existing APIs work is fixed, and there is no intention to change them, or to warn on existing legitimate usage of these APIs. Therefore it's impossible for the APIs to start warning when you use a default constructor. At least with the features suggested here, it will be possible to warn if the default constructor is used incorrectly. |
Seems the reasoning is that public class C
{
public string Str {get; init;} = "";
} In this case it would be inappropriate to warn on not setting It is mentioned that NRTs and |
@canton7 I understand your point but I also think it's worth considering things from the PoV of the consumer of a class. Don't you think it would be pretty unfriendly to receive nullable warnings relating to a class when trying to initialise it? Rather than more straightforward errors relating to required properties, i.e. clear violation of code contract?
|
No more than receiving nullable warnings when trying to call a method on that same class. You'll already get nullable warnings if you try to set one of those properties to |
you could always hang on a bang:
|
It's also been mentioned that this would make |
No? If a property is
I think option 1 is always going to annoy. If someone has made a non-nullable
I'm not against the concept of required properties, but I do think they're orthogonal to this issue. Whether you have required properties or not, you still have to design what happens in the case that someone declares a non-nullable
I don't think Coming back to the specific issue in the OP, this issue is about the annoyance of unnecessary warnings when declaring POCOs used by serializers. EDIT: I got nullable and non-nullable mixed up |
What I believe you both are defending/proposing is effectively an implicit I prefer an explicit declaration because it allows better bug catching by requiring intent and offers clearer error messaging to API consumers. I believe it will also make code easier to read (more declarative) and aid automated tooling and analysis. Finally it has the potential to cover additional problem areas such as the value type issue I mentioned. But no problem to agree to differ. I respect you are looking for a simple solution with minimal change. My concern would be it makes a complicated language even more complicated. |
What I am proposing has nothing to do with additional language features or behavior. It has to do with the flow analysis of NRTs, which already has a major gap when it comes to POCOs. That same flow analysis should also apply to
|
@canton7 Precisely, Please note I don't really have yet an opinion on this topic. I was just explaining to @HaloFour an adittional issue with his idea that was exposed by @333fred (correct me if I misinterpreted your point @333fred)
(Emphasis mine) @canton7 The consumer today has no way of knowing that a property has a default value. Yes, an attribute could solve that. |
I guess I am nervous of special-casing the flow analysis to solve one problem and in a way I think could be a head-scratcher for many people.
Well if you set a non-nullable to null you should obviously get a compiler warning as per normal. |
Just to add @HaloFour , I sympathise. Using NRT with POCOs is hard work. I have in some cases had to make duplicate classes (with and without non-nullables) to work around issues..... Basically I want to use NRT to tighten my code and be explicit with my types. I want to use initializers rather than constructors to offer friendly and flexible API syntax. I want to use immutable types and records where appropriate for data objects. But, even with C# 9 as announced, doing all these things together will not be fully possible. To my mind the biggest problems are solved via "required init" and "required set". I suspect in the end you are having similar issues to me. Hopefully we can all find a solution. I'm signing off so bye for now. |
You seen to think that I'm advocating against I am looking at the issue in the OP. Forget about required properties for a minute, the problem in the OP is not about them. The problem in the OP is that you get unwanted warnings when declaring POCOs meant for serialisation. Required properties came up because they are a larger feature which might address the problem in the OP. Fine. I'm not arguing against them. But forget about that potential solution for just a minute.
If that is the case, then we can go and design required properties independently of this issue. |
Actually I don't even know what it is that you're advocating for and I'm indifferent to it. I was just replying to your message, since you tagged me.
The discussion about required properties came up. I've chimed in. End of story.
And I was trying to explain to @HaloFour that his suggestion has another potential obstacle evidenced by @333fred. |
@andre-ss6 my bad, I thought you were part of that thread of conversation. |
@canton7 For the record I don't think you are against |
Some of the examples discussed (string, booleans) have corresponding trivial values (empty string, false). The |
@markm77 it's more that I think this solution falls naturally out of the only sensible interaction between NRTs and I'm also trying to point out that this issue and required properties are more or less two different things, and we can consider them separately. |
@canton7 NRT is not going to help you if the property is a value type though, is it? |
@Richiban We're talking about two different things. If the property is a value type, you won't get the nullable warnings that this issue is complaining about, so it's moot. Required properties are different. It's correct that The point I'm trying to make is that the specific warnings in the OP can, I think, be solved by |
I don't think that's right. The problem in the OP is not that they get unwanted warnings. It's that they want the warnings to appear at the callsite. So this:
Becomes:
Notice that Suggestions to add the |
@chucker you didn't bold the words "When declaring POCOs", but they're important. I agree with you: if you have a non-nullable Please read my original post where I make this clear. |
Why? POCOs are just one example. Razor Components are another. Settings/options types (I guess those are arguably a form of POCO?) are yet another.
I don't agree with that. A property doesn't have to be immutable (
|
I'm using Signum Framework, and we can declare an entity once and serves as DB entity model, DTO, and view model at the same time. We abuse using not-nullable properties to mean mandatory in the UI and not-nullable in the database but really they could be null while you are filling the form. Example: https://github.com/signumsoftware/extensions/blob/master/Signum.Entities.Extensions/Notes/Note.cs We also use a lot of object initializers, not only because the ORM requires a default constructor (it could be private), but because when an entity has more than say three or four properties a constructor becomes harder to read (what was the parameter number 4???) and also is more code that have to be written and maintained. I love NRTs but I agree that the new I like the
The options that I can see are:
With this feature we could have more fine control about what type-safety we want for different hierarchies of objects and enjoy object initializers without feeling guilty for not writing the constructor. For example for DTOs I would use Of course, when using reflection to create the instance (Activator.CreateInstance) all the required properties are empty and up to be set by some run-time magic like a deserializer ignoring |
@YairHalberstadt Should be closed as championed in #3630 ? |
Problem
Consider the following three outstanding issues with NRTs
1. Object Initializers
Object initializers are greatly loved by many C# programmers. However they don't interact very well with NRTs.
Consider this for example:
If Str is to be non-nullable, then when enabling nullability we are forced to completely change all this code:
Making object initializers usable only be nullable properties and fields.
2. DTOs
Many DTOs are initialized by ORM/Serialization frameworks. As a result they often contain only property getters and setters, without any constructors.
This of course causes warnings when NRTs are enabled, as it doesn't know the properties are always initialized by the ORM/Serialization framework.
3. Structs
Structs always have a default constructor, which cannot be overridden.
As a result whenever a struct containing a field of a non-nullable reference type is constructed using the default constructor, the field will initially be null. This leaves a big hole in the nullable-type-system:
Solution
Based on a suggestion by @Joe4evr #36 (comment)
The real issue here is that present mechanisms only allow us to guarantee a field is initialized inside the constructor, but currently many types rely on their invariants being upheld by whoever constructs the type.
It should be possible to mark that a field/auto-property must be initialized at the point it is constructed, before it is used. This could be done via an attribute for example:
Then when an instance of the type is constructed, it should be a warning to use the type before all such fields are initialised:
All structs should have this attribute on all their fields implicitly.
Further issues
Consider this struct:
Then it would be a warning not to initialize
S.Str
when the thenew S(string)
constructor was called, even thoughS.Str
was initialized by the constructor!I think the solution to this would be for the compiler to detect which constructors initialize a field, and implicitly add parameters to the
MustInitialize
attribute which indicate which constructors did not initialize the struct.ie. the compiler should generate code equivalent to the following:
then, when the default constructor is called, the compiler knows it must make sure
S.Str
is initialized.The text was updated successfully, but these errors were encountered: