-
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
Nullable Reference Types: Allow initialization at construction site #2109
Comments
I think you'd need a way for the compiler to know that this is a DTO type. Otherwise, from a metadata perspective, the compiler would never be able to know that those properties need to be initialized. As far as it knows a default constructor set them to some non-null default value. Also, for the second example: var dto = new MyDto
{
LastName = "Doe"
}; // warning: FirstName is uninitialized I'd say that there shouldn't be a warning here because you haven't done anything with |
@HaloFour fair enough regarding the second point. |
What about having some sort of "this field can be null, but once it's set, it will never be null again" indicator? Allows post constructor initialization but once it's been assigned it is certainly not null |
You mean that the compiler would annotate the type with this metadata as it compiles it, as long as |
@johnkellyoxford how do you analyze that? The compiler currently tracks that once you initialize a nullable property to non null, it will be non null for the rest of the method. Even costly whole program analysis couldn't reliable track this i believe. |
Related: #1684 |
I would like to see a unified proposal that neatly solves both issues. I don't believe either of these are strong enough on their own. |
@Suchiman didn't really think of the implementation details of it. |
Did you mean public class MyDto
{
public string? LastName { get; set; }
public string? FirstName { get; set; }
} |
@Thaina no. |
This is yet another use case crying out for the presence of records. Your DTO doesn't want null in its fields but the only way to accomplish that in today's C# is to write your own constructor, because that's the only way that C# currently has of forcing you to provide data when you instantiate a class. |
While I agree that there is overlap here with records I don't think that records would make sense for a lot of DTOs. Having positional construction for a DTO with a lot of fields would be very cumbersome, and additions would always be a breaking change which is not currently the case. |
True that; in fact, I wouldn't recommend (in any language) using positional deconstruction with any more than two or three properties.
Surely the only options for either records or DTOs is that you either allow null (or some other default value) or it is necessarily a breaking change. |
@HaloFour |
Did you mean "construction" there? My tolerance for construction would probably be higher than 2-3, but I've commonly dealt with DTOs with 20+ non-null fields. That would be absurd for a constructor, even if you used named parameters.
The question is when you require the property to be assigned. As a DTO is traditionally a class of properties with no constructor or other business logic it doesn't fit well into the pattern expected by NRTs. If appeasing the compiler requires a lot of refactoring that would discourage people from using NRTs. Regardless, I think that the team should really look into how NRTs play with DTO scenarios to see where they could perhaps smooth over the experience.
Optional parameters are still a breaking binary change and require all source consuming that method to be recompiled. And if they have a safe default (especially one that is supported for an optional parameter) there's little reason to add it to the constructor anyway. |
No, I did mean deconstruction, but I still feel that we're mostly on the same page. My point was that it might be common and entirely reasonable to deconstruct a When it comes to construction though, other code style guidelines go out the window for me and I will lean on the type system as much as possible to make my code as safe as possible, readability be damned. I will write: public class Person
{
public Person(
string FirstName,
string surname,
DateTime dob,
string favouriteFood,
Color eyeColor,
IReadOnlyCollection<Person> parents,
IReadOnlyCollection<Person> Children
) {
FirstName = firstName ?? throw new ArgumentNullException("firstName");
Surname = surname ?? throw new ArgumentNullException("surname");
Dob = dob ?? throw new ArgumentNullException("dob");
FavouriteFood = favouriteFood ?? throw new ArgumentNullException("favouriteFood");
EyeColor = eyeColor ?? throw new ArgumentNullException("eyeColor");
Parents = parents ?? throw new ArgumentNullException("parents");
Children = children ?? throw new ArgumentNullException("children");
}
public string FirstName { get; }
public string surname { get; }
public DateTime dob { get; }
public string favouriteFood { get; }
public Color eyeColor { get; }
public IReadOnlyCollection<Person> parents { get; }
public IReadOnlyCollection<Person> Children { get; }
} Yes, I am sick to death of these bloody constructors, even I can generate them the first time using visual studio and no, I am not prepared to give them up. |
That's your prerogative and I'm certainly not arguing against the use of constructors in your code base. However, it's my experience that most DTO-generating tools do not generate constructors and even if they understand the concept of non-nullability they don't enforce it at the DTO level, and that the frameworks with which these DTOs are used have little/no support for constructors anyway. This includes Entity Framework and LINQ to SQL. EF Core looks like it's adding constructor support, but I doubt it'll ever come to Entity Framework proper, but even then I doubt most existing projects would be changed to use it and I'd expect that it wouldn't be used to force 100% assignment of non-nullable properties anyway. I think DTOs also have additional considerations not likely considered by NRTs. First, I'd posit that it's relatively rare for code to actually create the type directly. It's expected that the ORM or serialization library creates the type most of the time. Second, I'd posit that, especially for ORM-related DTOs, that not all of the non-nullable properties are expected to be populated at the time of creation. This would be true of any server-generated fields like identities or calculated fields. But given that those properties would be correctly populated in the majority of cases I think it's appropriate that they would be marked as non-null. |
Can't we have a middle ground when it comes to DTOs then, and say that even with nullable reference types enabled this does not produce a warning / error... public class Foo
{
public string A { get; set; }
public string B { get; set; }
} ... but this does? var foo = new Foo
{
A = "Hello world"
}; // Error: Non-null property `B` has not been assigned |
That's exactly what this proposal suggests. 😄 I suggested that maybe the compiler wait to warn until // 1.
var foo = new Foo
{
A = "Hello",
B = "World"
};
return foo; // doesn't warn
// 2.
var foo = new Foo();
foo.A = "Hello";
foo.B = "World";
return foo; // doesn't warn
// 3.
var foo = new Foo()
{
A = "Hello"
};
foo.B = "World";
return foo; // doesn't warn
// 4.
var foo = new Foo();
return foo; // warns
// 5.
var foo = new Foo()
{
A = "Hello"
};
return foo; // warns
// 6.
var foo = new Foo();
foo.A = "Hello";
return foo; // warns This is quite a bit more complicated and involves the compiler tracking the assignment state of every non-nullable property of the DTO. It also doesn't solve the issue when it comes to server-generated fields, but that might just be unavoidable. Either way, the conversation is about enforcement of non-null-ness by the consumer of the DTO, not within the DTO itself. |
I'm inclined to write that code something like this:
which does give me warnings about non-nullable properties being uninitialized though I am unsure how to fix them. Changing the builder constructor to this works to rid the warnings:
But that really seems quite unhelpful and it does nothing to help me know that this code is really a problem:
|
@HaloFour While that could certainly work, I'm not sure I like it due to the fact that it's now possible to write C# statements that cannot be extracted to a method, despite the fact that it would be perfectly valid to do so. For example, I think it would be quite hard to explain users why this is allowed: public Foo M()
{
var foo = new Foo();
foo.A = "A";
foo.B = "B"
return foo;
} but this is not: public Foo M()
{
var foo = new Foo();
foo.A = "A";
N(foo);
return foo;
}
public void N(Foo foo)
{
foo.B = "B";
} I think it's much more natural to have a rule that "we allow mutable DTOs to have their non-nullable properties set my the initialzing code rather than the constructor, but you must instantiate the object and set its properties in a single statement (i.e. in an object initialization expression)". |
@Richiban works: using System;
public class C
{
public void M()
{
Test test;
test.A = 42;
test.B = 21;
Console.WriteLine(test.ToString());
}
}
public struct Test
{
public int A;
public int B;
public override string ToString()
{
return A.ToString() + " " + B.ToString();
}
} error CS0165: Use of unassigned local variable 'test': using System;
public class C
{
public void M()
{
Test test;
test.A = 42;
N(test); // error CS0165: Use of unassigned local variable 'test'
Console.WriteLine(test.ToString());
}
public void N(Test test)
{
test.B = 21;
}
}
public struct Test
{
public int A;
public int B;
public override string ToString()
{
return A.ToString() + " " + B.ToString();
}
} |
I honestly didn't know you can do this in C#... I'm still not sure I fully understand it though. I was exploring it in LinqPad just now and found that, never mind other methods, if you just remove the assignment to
then you get the same compiler error! |
@Richiban it's actually even in the documentation 😉 https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/using-structs#example-2 |
Well well, you really do learn something every day! |
I don't see why DTO types would need to be treated specially in any way. Warning at the construction site (and never at the declaration site) if a property is left uninitialized should probably just be the universal default behavior.
I'm not convinced that it makes sense to allow constructing the object in an incomplete state at all without producing a warning (or better yet, error). If so, then at least the compiler would have to be clever enough to produce a warning or error if you try to call a method or property on the object before it is completely initialized. |
This and #2328 seems related; can the two issues be merged? And, FWIW:
Records don't actually fix this, because there's a subtle difference between each property being init-only/immutable (which records do guarantee), and each being required to have a value (which they don't). The other issue features some discussion over a separate notion of |
Closing as championed in #3630 |
When you turn on Nullable Reference Types, all fields and properties must be initialized at constructor time.
This effectively kills object initializer syntax and usual cases like EntityFramework and DTOs where you don't have constructors, only a bag of properties.
What i'd like to see would be support for full initialization at construction site.
Effectively, similiar to how you can skip calling the constructor of a struct when you explicitly assign all fields (using definitive assignment rules), i'd like to request support for following to work:
The text was updated successfully, but these errors were encountered: