Open 333fred opened 4 years ago
To call it out more explicitly: we know we want to do something here, but we need to find a good syntax. I'm looking forward to seeing suggestions from the community as well :).
My vote is a keyword modifier on set
/init
. I kind of like required
although req
and must
are ok, too. mustinit
seems redundant.
public string Foo { get; required set; }
public string Bar { get; required init; }
mustinit
seems redundant.
My idea there was more
public int Prop { get; must init; }
The full word was for if it's a modifier on the property itself.
Obligatory mention of the verbose syntax:
public not default int ReadonlyIsALongWordButItIsSomewhatClearWhatItMeans { get; init; }
I do not like it, actually, since it tells me something about the property, not that I should initialize it. My vote is with must init
.
Here's some more ideas:
public int Prop { get; do init!!; }
public int Prop { get; explicit init; }
public int Prop { get; init!!; }
public int Prop { get; init else throw; }
@333fred
The full word was for if it's a modifier on the property itself.
Got it, so it would be:
public mustinit string Foo { get; init; }
You could argue that the modifier on the property makes it easier to see which properties are required vs. having to potentially scan for the accessors. I still think I'd lean towards required
or must
on the setter accessor.
What's the reasoning being init
props being non-mandatory? I may have missed something, but I imagine that they'll be very rarely used without a req
modifier.
Updated the proposal with a new alternative section on "Initialization Debt", which builds on this proposal as a general way of communicating what properties must be initialized in a more flexible fashion.
What's the reasoning being
init
props being non-mandatory? I may have missed something, but I imagine that they'll be very rarely used without areq
modifier.
@Richiban consider the first example:
class Person
{
string FirstName { get; init; }
string MiddleName { get; init; }
string LastName { get; init; }
}
Not everyone has a MiddleName
(heck, not everyone even has what Americans would consider a family name either), but I still want Person
to be immutable after construction.
Not everyone has a MiddleName (heck, not everyone even has what Americans would consider a family name either), but I still want Person to be immutable after construction.
I get it, but why not simply let them set null explicitly?
var p = new Person
{
FirstName = "Alex",
MiddleName = null,
LastName = "Fooson"
}
It just seems like a significant weakening of a feature (or, perhaps, a requirement to develop two features) to make the case of optional properties a little easier.
EDIT: Even better: use the concept of property initial values to indicate optionality:
class Person
{
string FirstName { get; init; }
string MiddleName { get; init; } = null; // This is optional because it's been given a default value
string LastName { get; init; }
}
@Richiban
Orthogonal concerns. init
accessors state that the property can only be set during initialization, not that they have to be set during initialization. And that doesn't cover set
accessors that would be required during initialization but remain mutable after initialization. Splitting them into two concerns covers both bases.
Are required and initializable properties being considered as orthogonal concepts? Or is required an extra that may only be applied to initializable properties?
I'd like to be able to write property declarations that trigger a compilation failure if I fail to initialize the properties in my own constructors:
public class Person
{
public string KnownAs { get; private required set; }
public string FamilyName { get; private required set; }
public string LegalName { get; private required set; }
public Person(string knownAs, string familyName)
{
KnownAs = knownAs;
FamilyName = familyName;
// Compiler error here because LegalName has not been initialized
}
}
I'd find this useful for expressing intent, especially on classes that I expect will need to be modified in the future as the system develops over time.
For example, if I added
public string Honorific { get; private required set; }
to the above class, having the compiler ensure that I correctly initialized the property in every constructor (including the one in a different file, thanks to a partial class declaration) would be really useful.
@theunrepentantgeek
Interesting scenario. I'm under the impression that this proposal covers the metadata and enforcement of consumers of the class, not of the class itself.
It is an interesting scenario, and one I think could be covered by the initialization debt extension. In that world, those 3 things would be part of the debt, but because they're private to your class you wouldn't be able to publish them as part of your debt contract. They would have to be assigned in the constructor.
I like must
since it flows naturally with set
and init
required
kind of doesnt flow (linguistically) with these two keywords but is useable on property itself not just acessor so privately if we went with keyword on property i would go with required
otherwise must
Orthogonal concerns...
Sometimes it's good to pause and think through what someone says before instantly rejecting what they are saying. When we do so, we see that the true orthogonal concerns here are init
and set
. They do two completely different things and the one solution is not going to fit them both very well.
I suspect I'm not alone in being completely perplexed by the opening statement "The number 1 misconception about init
that we've seen is that putting init
on a property will solve the issue of properties that are required to be set at object creation". The idea that this is a misconception is worrying. Since it's the no. 1 "misconception" it's clear that I'm not alone in assuming init
would be mandatory.
Therefore, let's start with the requirement that it is mandatory. That's what most people will expect it to be and that's what most people will want it to be most of the time.
So how do we address @333fred's conundrum?
class Person
{
string FirstName { get; init; }
string MiddleName { get; init; } // many people don't have a first name so it shouldn't be mandatory
string LastName { get; init; }
}
Well first of all, if it's not mandatory, it has no right being declared as string
. It's a string?
property. And we can then take the same approach as we do with non-mandatory parameters: provide a default value to make it optional (as @Richiban suggested in his edit above):
class Person
{
string FirstName { get; init; }
string? MiddleName { get; init; } = null;
string LastName { get; init; }
}
Now, MiddleName
is an optional initialiser property.
This feature is a nice one and it would be good to add it to set
too. This is an orthogonal issues of course; set
currently cannot be specified as mandatory after all. But "whilst in the area", let's address that too. And we conveniently already have a keyword that means "mandatory when used in initializers": init
For a mutable Person
class where first and last names are mandatory, we then can express it as:
class Person
{
string FirstName { get; init set; }
string? MiddleName { get; set; }
string LastName { get; init set; }
}
Now, init
means "must be set at initialisation time". And without set
it's read-only after initialisation, which therefore causes no change to what was already proposed for init
.
This also then solves @theunrepentantgeek's scenario:
public class Person
{
public string KnownAs { get; private init set; }
public string FamilyName { get; private init set; }
public string LegalName { get; private init set; }
public Person(string knownAs, string familyName)
{
KnownAs = knownAs;
FamilyName = familyName;
// Compiler error here because LegalName has not been initialized
}
}
private init set
is a bit of a mouthful, but I'm not sure how often this pattern would be used. It's likely fine for occasional use. However, if it becomes popular, we'd likely want a neat way of expressing it in a much shorter way. But that definitely is orthogonal...
@DavidArno
So then how do you propose to make a set
accessor also mandatory?
init
doesn't mean mandatory. It means you can only set during initialization. The requirement that it can only be set during initialization says nothing about whether or not you must set it during initialization. That's where this proposal comes in.
So then how do you propose to make a set accessor also mandatory?
As explained above, by using init set
.
init
doesn't mean mandatory. It means you can only set during initialization
As clearly stated by the OP, many (likely most) people assume and will continue to assume it does mean mandatory because it makes sense for it to be mandatory for most cases. So I've turned @333fred's proposal on its head with my counter proposal which allows it to be mandatory and meets all the others requirements mentioned in this thread all without adding an extra keyword that has to be used in most scenarios and is only omitted in exceptional cases.
I admit in my first post that I never considered the case of required but mutable properties, but I would still argue for the language design philosophy of:
the shortest code gets you the strictest behaviour
in the name of safety and having a language that will cause developers to fall into the pit of success.
@DavidArno
Nothing about init
implies that it's required. You seem to be reading that into the keyword and I doubt that will be the common interpretation. In any case your interpretation of init
is not going to happen by C# 9.0 and init
isn't likely to be pushed given that records depends on it.
A few points:
Therefore, let's start with the requirement that it is mandatory. That's what most people will expect it to be and that's what most people will want it to be most of the time.
@DavidArno but that's not what it is. Unfortunately, properties from C# 1.0 were optional by default, and so that's the world we're going to have to live with. In fact, other than conflating init
and req
, your proposal ends up in a very similar place to the original, except that it introduces massive breaking changes to the .NET ecosystem.
Well first of all, if it's not mandatory, it has no right being declared as
string
. It's astring?
property.
Nullable was not enabled in that code snippet, it cannot be string?
. You'll note that my original example did have nullable enabled, and the property was string?
in that example. Any proposal we come up with here is going to have to live in this world. Further, being mandatory has nothing to do with the type of the property. It could be perfectly reasonable to default the property to string.Empty
for cases without a middle name.
Your proposal paints a nice picture, one I even like. However, it would require changes to large swaths of code, and if we were going to do that I don't think it goes far enough. I'd much rather extend the concept of definite assignment to all properties of an object, and have constructors/factory methods export explicit contracts of "these things must be initialized before this object is considered valid". Then we could have a world like this:
#nullable enable
// Maybe the contract on a default constructor could be implicit, but I'm just putting it on the
// class declaration for example's sake
[RequiresInit(nameof(Person.FirstName), nameof(Person.LastName)]
public class Person
{
string FirstName { get; init; }
string? MiddleName { get; init; } = null;
string LastName { get; set; }
}
public static class PersonFactory
{
[RequiresInit(nameof(Person.LastName)]
public static Person MakeMads() => new Person { FirstName = "Mads" };
}
var kristensen = PersonFactory.MakeMads() { LastName = "Kristensen" };
var torgersen = PersonFactory.MakeMads() { LastName = "Torgersen" };
var error = PersonFactory.MakeMads(); // Error: "LastName" is uninitialized"
In this world of mutable last names, no extra keyword would be required here: you didn't initialize the property, so it's part of the "debt" that is exposed from a constructor or factory method. But as much as I like this system, it presumes that properties are required by default, which is unfortunately just not true today, and not likely something that we will be able to change.
the shortest code gets you the strictest behaviour
What we do in the name of backcompat, @Richiban. What we do in the name of backcompat.
What about fields?
The concept of initialization debt could certainly be extended to fields as well.
Whether a property is required may depend on which constructor is used. For example it's not an uncommon pattern to have a default constructor, in which case all properties are required, ans a constructor which sets everything, in which case none are.
That's what the more general initialization debt extension would cover.
All public fields of non-nullable reference types should be required when you use a default constructor on a struct, as should auto properties. I don't think there's any good solutions to private fields in a struct.
Until we get non-defaultable structs I think this is a pretty moot point. It's something we certainly want, but without the feature it doesn't matter.
I still don't understand how init
is not required.
Looking at @DavidArno and @Richiban comments and the replies by @333fred made me think about what is being said here.
Please tell me how can something been ONLY setable during initialization, but then not be mandetory. That property is not setable after, so what is the value going to be? Is it default? Is it null?
In this case I agree with what has been set, the property/field needs to be either nullable or need to be preinitialized (not sure if this makes even sense for something that is called init).
Are we really talking about introducing a keyword or attribute just so the developer doesn't have to initialize during initialization?
I rather write
var person = new Person
{
Firstname = "John",
Middlename = default,
Lastname = "Doe"
}
over introducing a new keyword or Attribute. And also I like that I am forced to initialize the properties and don't forget something, especially when the class is altered afterwards.
Still don't understand what the state of properties will be if they are not initialized even though they can only be set during initialization 🤔
Edit: autocorrection
Still don't understand what the state of properties will be if they are not initialized even though they can only be set during initialization 🤔
It's going to be whatever default was provided, or default(T)
if none was provided.
Please tell me how can something been ONLY setable during initialization
Don't forget that initialization includes field/property initializers (including property init
bodies) and constructors. Lots of places for a default to be set.
Do I understand correctly that C# 9 init
for properties should have the same function as readonly
for fields? Why have two different keywords for the same purpose?
As for this issue, if both { get; init; }
and { get; set; }
are valid, I would expect the former one to be required at initialization but impossible to set afterwards (expressed by the lack of set
). If I wanted to enforce initialization and allow changing it later, { get; init; set; }
would work for me.
Do we expect this to be a commonly used feature? Because an attribute on the property might do the job as well...
@miloush
init
is only like readonly
in that a consumer can only invoke it during construction (although not necessarily during the constructor), but it doesn't prevent the underlying field from being mutated later.
init
as it is expected to ship in C# 9.0 doesn't require initialization, hence this proposal which seeks to add the ability to require initialization to init
properties, set
properties and fields.
Why not just use the !
operator? This seems highly analogous to null-checking of constructor parameters.
class Foo
{
public int Bar! {get; init;}
}
@MgSam
This seems highly analogous to null-checking of constructor parameters.
I don't get the analogy. !!
won't enforce you call a particular constructor, and a required property doesn't care if you assign it to null
, only that you assign it.
@HaloFour They are both indicators of requiredness. Though admittedly slightly different forms of requiredness (null check vs required assignment). I think it would be much better than adding yet another keyword for what on the surface is an extremely similar concept.
!
has already been overloaded several times. =>
also means similar but different things depending on where it is used.
@MgSam
I personally don't see the similarity. I think required assignment and required non-nullability are quite orthogonal. IMO it'd be very strange that a property adorned with !!
would be happy with a null
assignment. I doubt it makes it any less complicated to implement by reusing a character sequence used for another feature.
I tend to agree with @RUSshy here that there is no motivation or problem discussed that this proposal is trying to solve.
The best I can do from the Motivation section is that people misinterpret the upcoming init
to do something else, and because that something else cannot be currently done in the language, rather than "sorry, that's not what you think it is" we need to be able to say "sorry, what you think it is can be done this way".
First, that suggests that the init
as proposed is not very intuitive to people, and second that there is no reasoning behind why what people think it is needs to be done.
To rephrase this, the proposal says "let's have a way that allows properties to be required to be set during construction", the question then is "why we need such properties", what current or future problems "required properties" solve?
It looks a lot like what the nullable is currently doing with fields and constructors. The only argument against is that "nullable has to be on" and we cannot introduce such requirement into existing code. But the proposal is suggesting new language keyword, so in any situation that you can use the proposed keyword you can also turn on nullable.
Additionally, it goes, how does it work with structs for which default
is not a valid option? You declare the backing field as nullable. You can still have the property return the non-nullable equivalent.
Finally, why is this for properties only? If there is a demand for required properties, then I don't see why there wouldn't be a demand for required fields; or even methods that are required to be called during initialization. Any choice of keyword should make sense in case its usage is extended to other member types. As a consequence, whatever mechanism is chosen, it should be associated with the setter only, not the whole property.
As for the proposed set of keywords, must
works in a lot of the contexts set/init/get/add/remove
and because the accessors are all verbs (and normal methods tend to be verbal too), it is clear that you must set the property or must init it. It is not great with fields. It also does not indicate when you must do whatever you must do, in which respect mustinit
is better, but obviously it is a little suboptimal with mustinit init
. I don't like explicit
due to its current interface connotations, so it looks like required
is favorite of mine too. Generally past participles might work the best here, e.g. initialized
- which has the precedence of compiler complaining something is not initialized before use. I prefer required
to req
, this is C# not assembly.
@miloush
There have been many requests and proposals to force object initializer syntax for construction, either via constructors or through other means. This is a requirement that comes more to focus with nominal records, and the "misconception" that init
already does this already highlights that there is enough demand for a feature of this nature.
As it is with init
, this proposal is orthogonal to NRTs. A required property or field dictates that the member must be assigned, not what the member must be set to. You can compose the three features completely separately from one another:
public string? Foo { get; set; } // mutable after construction, nullable, optional during construction
public string Bar { get; set; } // mutable after construction, non-nullable, optional during construction
public string? Baz { get; init } // mutable only during construction, nullable, optional during construction
public required string? Fizz { get; set; } // mutable after construction, nullable, required during construction
public required string Buzz { get; init; } // mutable only during construction, nullable, optional during construction
This is a requirement that comes more to focus with nominal records
Right, and for me that is the main weakness of the proposal. It only makes sense in a nominal record because nominal records effectively replace constructors with init
as an initialization strategy. Positional records, classes and structs have constructors, so if you want something set, you make it a parameter.
So I guess the question is this: where would required properties be used outside of nominal records?
Edit: maybe it would make sense to have all record properties marked with data
be required? And if you want an opt-out, use the longer form with init
? This kills the thing as a general mechanism for requiring arbitrary stuff to be done at initialization time, but my opinion is that's fine given constructors exist.
@HaloFour - that's a very good summary of the options. However, I can't see how this one works?
public string Bar { get; set; } // mutable after construction, non-nullable, optional during construction
If it is optional during construction, what value does it have it not set during construction, because default(string)
is null
, and Bar
is explicitly non-nullable. Or does this create a new NRT warning when the object is created that value has been left in an "invalid" state?
Or does this create a new NRT warning when the object is created that value has been left in an "invalid" state?
Yes, that's the idea.
there is no motivation or problem discussed that this proposal is trying to solve.
You clearly have missed the discussion in #2328.
Why not just use the ! operator? This seems highly analogous to null-checking of constructor parameters.
Actually we want to be able to configure run-time null checks with object initializers too, right? 😀 Hand-written run-time null checks will otherwise be tedious and dis-connected from a property's declaration. So perhaps best to not preclude a nice future solution like (for example)
public string Prop1 { get; req set; } // Prop1 required in initializer
public string Prop2 { get; req! set; } // Prop2 required in initializer and run-time null check performed
I still don't get the insistence to use object initializers as the default way object creation in C#. We all know that, object initializer was introduced as just a syntax sugar. It did not have checks for safe object creation. Now when the nullability has been introduced to the language, the hole that object initializers cannot safely initialize non-nullable properties- is exposed. Two major language versions- C# 8 and 9 are being shipped with this hole. They also did not play nice with immutable properties, which is being solved with new language features. And while promoting the use of initializers, instead of redesigning it to play the role of constructor (which would technically be a breaking change but real code breaks would be within expectation with nullability checking enabled), new keywords and syntaxes are being proposed. This very much looks like bloat to me. Kotlin just uses constructors, with named parameter when preferred, without requiring all these bells and whistles and ensures null-safety. Their data class definitions and other class definitions are short, safe and sweet.
My poorly worded issue #2558 can be viewed for some previous discussion.
I still don't get the insistence to use object initializers as the default way object creation in C#
I don't think anyone is insisting this is the default way to create objects. Simply that it should be treated as a first-class scenario that should work well with other lang features.
@CyrusNajmabadi Why not redesign it to play constructor-like role then? By which I mean, if nullability check is enabled, in any object initializer call-site, compiler will ensure non-nullable properties are initialized to non-null values. If nullability check in not enabled, nothing changes syntax-wise, but internally it is not just the syntax sugar it used to be.
@gulshan
Constructors are positional and inherently brittle to changes. Adding new optional members would require the addition of an overload, and you'd have to be careful to keep the new members at the end of the list otherwise you'll break all consumers. Constructors for data types of more than 5-6 properties are simply cumbersome, even if you treat it as a quasi-initializer syntax.
@HaloFour Like I've said, object initializers should have been redesigned to ensure safe object construction.
@gulshan That's one of the options in the proposal, if I am reading what you are thinking of correctly, and it has been discussed above:
We could also go the route of attempting to retconn nullable and flip the default for properties when nullable is enabled. If a default value is provided for a property, instead of warning at the object definition site, we'd warn or error at the object creation site. We'd have a few issues to work through to make this work, though:
- Are you required to provide a default for struct types? If not, how do you specific that a struct property must be initialized?
- Is this going to produce a warning or an error? What will it do in non-nullable enabled code?
- Is this expressive enough? Namely, does it communicate the intent of the developer well enough that we feel confident in reading the code later?
- How breaking of a change is this? Doing this would already have been a pretty big semantic break from pre-C# 8 code, and it's an even bigger break now that nullable has been out for a while.
@SingleAccretion I think the focus should be on object-initializers being repurposed to play the role of constructors. Then both immutability and non-nullability checks within object initializers will be performed by the compiler similarly they are done within a constructor. Modifiers like init
and req
should not be required.
@gulshan
I think the focus should be on object-initializers being repurposed to play the role of constructors.
That is what these proposals are focusing on. This is an attempt to get the behavior of constructors with the flexibility of initializers.
Then both immutability and non-nullability checks within object initializers will be performed by the compiler similarly they are done within a constructor. Modifiers like
init
andreq
should not be required.
I don't understand how that would be possible. Object initializers already mean something and that can't change. Without init
there is no such thing as a property that can only be set during initialization. Without req
(or similar) there's no way to enforce that the property is required to be set during initialization.
@CyrusNajmabadi Why not redesign it to play constructor-like role then? By which I mean, if nullability check is enabled, in any object initializer call-site, compiler will ensure non-nullable properties are initialized to non-null values.
This is the space we are looking at.
@SingleAccretion I think the focus should be on object-initializers being repurposed to play the role of constructors. Then both immutability and non-nullability checks within object initializers will be performed by the compiler
This is the space we are looking at.
Whatever the syntax for required initialization ends up being (I don't care if it's the default for the bare init
keyword or something more verbose like req init
), I think it's a mistake to ship records in C# 9 before this feature lands, and to have records not use it by default for auto-properties. There's a reason that this “misconception” that record property initialization is mandatory is so widespread: mandatory initialization for properties is exactly what people are expecting and want from record types coming from F#, Rust, and more. The whole benefit of records in C# in the first place is not having to write a bunch of boilerplate keywords to get reasonable defaults for a record-like type, compared to fighting against the defaults when using the class
keyword instead. If those defaults don't go all the way and still require additional modifiers in order to get what people expect out of a record type, then in my opinion records will be a relatively lackluster feature.
Also, consider that if creating records where properties must be initialized is more verbose, then users who are already using NRT might just not bother, since it will warn them anyway upon object construction. Therefore, I worry that unless this mandatory initialization is the default for records, then this proposal will compete with NRTs.
@Serentty That's why I'll be using positional records when C#9 is released.
Why can't it be just string FirstName { get; req; }
or string FirstName { get; required; }
without init
? What's the benefit of using two keywords instead of one?
Because there are four scenarios that the LDM want to support:
Required Properties
Specification: https://github.com/dotnet/csharplang/blob/master/proposals/csharp-11.0/required-members.md
Design meetings
https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-09-16.md#required-properties https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-12-07.md https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-01-11.md https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-03-03.md#required-members https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-10-25.md#required-members https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-12-15.md#required-parsing https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-21.md#open-question-in-required-members https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-23.md#open-questions-in-required-members https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#effect-of-setsrequiredmembers-on-nullable-analysis https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-11-30.md#revise-membernotnull-for-required