Nominal Parameter Lists (aka Required Properties) #4209
-
Nominal Parameter Lists (aka Required Properties)An updated version of this is now in pr: #4493. A few months ago, I submitted #3630, which proposed a way for type authors to require that properties are initialized when a type is created, rather than in the constructor. Since then, a small group has been hard at work brainstorming and refining this proposal. We discussed the initial proposal in LDM here: https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-09-16.md#required-properties. Since that meeting, we've been working on the next version of the proposal based on feedback, and I'm happy to present that vision here. Detailed DesignSyntaxWe propose adding a new syntax for property parameter lists, which can be added to constructors to require that the consumer of a constructor initializes the properties in that list. If a constructor chains to a different constructor (such as class Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
// The new syntax is init { PropertyList }
public Person() init { FirstName, LastName }
{}
}
class Student : Person
{
public int Id { get; init; }
// The base() init call implies the property parameter list from Person,
// which is FirstName and LastName. It is then added to with another property
// list, which adds a requirement that Id is initialized.
public Student() : base() init { Id }
{}
} Property parameter lists can use assignments to remove requirements inherited from base constructors. Assignments can reference regular parameters, use constant values, or assign a discard to indicate that the body of the constructor will perform a more complicated assignment. class Rectangle
{
public int Length { get; init; }
public int Width { get; init; }
public Rectangle() init { Length, Width }
}
class Square : Rectangle
{
public Square(int side) : base() init { Length = side, Width = side }
{
// Assignments are actually run here, before any user code
}
// Another way of expressing this, if you had a more complicated algorithm:
public Square(int side) : base() init { Length = _, Width = _ }
{
var transformed = transformSideLength(side);
Length = transformed;
Width = transformed;
static void transformSideLength(int side) { ... }
}
} Derived constructors must include a property parameter list in order to pass the chained constructor requirements along, even if the list is empty. Leaving the property parameter list off indicates that consumers do not have to set anything. This allows users to adopt required properties in base types where it makes sense, while not breaking consumers who already depend on an existing constructor signature. class Laptop
{
float ScreenSize { get; set; }
float ProcessorFrequency { get; set; }
public Laptop() init { ScreenSize, ProcessorFrequency }
{}
}
class Surface : Laptop
{
// Passes all requirements onto consumers.
public Surface() : base() init { }
{}
// Passes any requirements from `base()` not specified in `init` onto consumers. If new properties are added to the base
// (perhaps laptop comes from a different assembly) it is the consumer's responsibility to initialize.
public Surface(Surface original) : base() init { ScreenSize = original.ScreenSize, ProcessorFrequency = original.ProcessorFrequency }
{}
// Does not pass any requirements onto consumers. New properties added to the base are not the consumer's responsibility
// to initialize.
public Surface(Surface original) : base()
{
this.ScreenSize = original.ScreenSize;
this.ProcessorFrequency = original.ProcessorFrequency;
}
} Property parameter lists can reference any property that is settable by callers of the constructor. Any inherited property that is not settable by callers must be set in the constructor that inherits it. This means a protected constructor on an abstract class could reference a property that is only settable by derived types, which will force the derived type to set that property itself: abstract class BoundNode
{
public BoundKind Kind { get; protected init; }
protected BoundNode() init { Kind }
{}
}
sealed class BoundPropertyAccess : BoundNode
{
// Good: Kind is assigned by this constructor and not passed to consumers
public BoundPropertyAccess() : base() init { Kind = BoundKind.PropertyAccess }
{ }
}
sealed class BoundFieldAccess : BoundNode
{
// Error: Kind is passed to consumers, but it is not accessible to them. Compiler error results.
public BoundFieldAccess() : base() init { }
} Each constructor can specify its own property parameter list. This means that copy constructors, for example, can specify nothing, as callers do not have to set anything, while the standard parameterless constructor can require a user to specify everything. We're still designing how primary constructors will look in general, but this should interact well them. Rewriting a couple previous examples using records: record Person() init { FirstName, LastName }
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
record Student() : Person() init { Id }
{
public int Id { get; init; }
}
record Rectangle() init { Length, Width }
{
public int Length { get; init; }
public int Width { get; init; }
}
// In order to avoid declaring a new property, Length had to be reused.
record Square(int Length) : Rectangle() init { Length = Length, Width = Length };
// Alternatively, the base constructor could be called without an init { },
// not passing any additional requirements onto consumers.
record Square(int Length) : Rectangle()
{
public Square
{
Length = Length;
Width = Length;
}
} EnforcementRequirements are enforced at the point of construction, not on the type itself: an object creation expression that calls a constructor with a non-empty property parameter list must also include an object initializer that sets the properties. Constructors expose 2 types of metadata: a list properties that they require, and a chained constructor whose property parameter list must be followed. This second bit is also optionally accompanied by a list of properties from the chained constructor that are not required. These will likely be encoded as attributes on the constructor, and marking the constructor itself with a modreq that indicates it has a property parameter list. When inheriting from a chained constructor, any property that is not accessible to potential callers of the constructor must be initialized the inheritor. For example, a base constructor could require an internal property be set, but a public derived constructor would have to set that property, as not all consumers will be able to access that internal property to set it. This also applies to hidden properties: if a required property from the base is hidden with a Enforcement in the body of a chained constructor that uses a discard assignment to remove a requirement is discussed in open questions below. Open QuestionsEnforcement of discard assignments in chained constructorsWe allow a constructor that chains to another constructor to remove a requirement using
Declaring properties in property parameter listsThe record examples are somewhat verbose, and it would be nice if we could make them shorter, like positional records. This isn't just an arbitrary drive for terseness: short syntax will be essential for making discriminated unions useful. If there is no shorthand for nominal records in discriminated unions, it's likely that positional will be preferred for no other reason than brevity. We've discussed a couple of different syntax for inline property declarations: // Syntax 1:
record Person()
init
{
public string FirstName,
public string LastName
}
{
public string MiddleName { get; init; } = string.Empty;
};
// Syntax 2:
record Person init
{
public string FirstName { get; init; },
public string LastName { get; init; }
}; Both syntax forms would declare a public init-only property, but with varying levels of brevity. We have some concerns about both forms, however:
Everything required annotationsThe original required property proposal used a keyword on the properties themselves to create a list of the required properties in a type, and this list was then inherited by every constructor. This is useful for types with multiple constructors, but we think that constructor chaining is the appropriate way to handle this scenario. A type would provide a default constructor, possibly Interactions with factoriesThe LDM has looked several times at first-class factory methods in the language, which would allow users to use object initializers on the return of a factory method. A factory is a more interesting place to consider the everything required scenario, because it seems reasonable that a factory would want to say "you must initialize all the things in the type, except the things that I initialize for you." Our perennial example is Interation with other languagesThe questions here are mainly around how far do we want to go with enabling interop. We believe there's a good story here with leaving off Property-based requirementsThis proposal takes a very constructor-centric approach to expressing requirements, but depending on the shorthands we end up allowing, this could require that properties need to be duplicated multiple times for simple classes, which is something we were trying to avoid with this proposal. An alternative solution would be to allow the properties themselves to be declared "required" in some fashion, which would build up a list that would be added to all constructors. The downsides here is that we'd then need to add some syntax to allow constructors to opt-out of these requirements (copy-constructors, for example) and not pass on requirements to users. Unsatisfiable contractsThis proposal does allow the possibility of contracts being unsatisfiable, such as if one dependency updates to add a new requirement to a protected constructor that is then not handled by an intermediate assembly. How do we want to enforce these? Source vs binary break inconsistenciesAs the proposal exists today, most additions and removals of individual properties are source-breaks only. However, if we use a modreq to indicate that the constructor has required properties, it will mean that going from no required properties to any required properties is a binary-breaking change. We need to consider going either way here: we can try again to make it a binary-break to add or remove required properties, or use the Discussed in LDM: https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-12-07.md |
Beta Was this translation helpful? Give feedback.
Replies: 15 comments 66 replies
-
How does validation fit into this proposal, if at all? |
Beta Was this translation helpful? Give feedback.
-
I will need more time to digest the whole proposal, but it looks incredibly overwrought for the primary use case: make the compiler check that multiple properties and/or fields have been initialized in the initializer. There's one question that I want to ask specifically: are there really use cases for factories returning half-baked instances that require an initializer? |
Beta Was this translation helpful? Give feedback.
-
This is a nice proposal but doesn't allow a programmer to express in-place that a property's auto-default is meaningless and should never be used and that the property should always be initialised. Thus bugs will creep in because the new task of maintaining "init lists" will be imperfectly performed. Actually I think the original proposal of "required" would work well alongside (in addition to) this proposal so tooling/complier could automatically update/warn about "init lists" based on "required". |
Beta Was this translation helpful? Give feedback.
-
I hate to say this because I can see how much effort has gone into this proposal, but for me it really looks like this misses the point of required properties; that you're able to express a type by listing its properties and state that they (or some subset of them) are required without having to repeat yourself in the class definition as you do today through use of constructors. Certainly the proposal has the advantage of playing nicely with potentially complicated type hierarchies and constructor chains, but the simple case (here's a type made up of properties and they're all required) still simply requires too much code. I see that you've recognised this in the section on records with mention of a shorter syntax that would allow the developer to combine property and init lists, but why only recognise the need for this de-duplication in records? |
Beta Was this translation helpful? Give feedback.
-
I'm not sure I see huge benefits to this over just making the constructor take those items as parameters. The constructor's role in the past has been to ensure the object is initialized to a legitimate starting state. If a property needs to be initialized, just put it in the constructor. Let the non-essential properties be set in object-initializer syntax, but the essential ones come through the constructor. I'll admit there are probably use cases for this anyway but do keep in mind that every new feature complicates the language. The benefits of new language features should be clear and significant in order to be added to the language. In my opinion, this adds complexity at a much higher rate than it adds value. |
Beta Was this translation helpful? Give feedback.
-
As I said in #3630 (comment) solving nullable initialization is a big use-case for this and the design presented here is really not appealing in that regard. I get that if you want to require initialisation of any property, .e.g an This design is cute with 2 properties. In our real code we have real classes that may have 30+ properties (think mapping large DB entities). Many of which might be nullable refs (e.g. This feels like Java checked exceptions lists more than C# :( |
Beta Was this translation helpful? Give feedback.
-
I'm surprised nobody has told you to write an analyzer for this yet... |
Beta Was this translation helpful? Give feedback.
-
If a property is required then it should go into the constructor parameter list. The whole point of constructors is to ensure the instance is initialized to a correct state at the point of creation. Why is another syntax needed? |
Beta Was this translation helpful? Give feedback.
-
How will this be stored in metadata? It's important this is simple (i.e. not like NRTs encoding) since IOC containers will need to make use of this information. |
Beta Was this translation helpful? Give feedback.
-
I do think that this is more in line with what I'd want to use. By default, required properties would always be required, but constructors could opt-out using e.g. an attribute. You could add a warning/error if a constructor opts-out without actually setting the property. At least for me, the majority use-case would be an object where all required properties would need to be set by the caller, and only in very rare cases would a required property be set by a constructor. Repeating the properties' names in these cases feels very noisy. class Person
{
public string FirstName { get; required init; }
public string LastName { get; required init; }
public Person() { }
[Initializes(nameof(FirstName), nameof(LastName))]
public Person(Person other)
{
FirstName = other.FirstName;
LastName = other.LastName;
}
[Initializes(nameof(FirstName))]
public Person(string firstName)
{
FirstName = firstName;
}
} If you suspect that the majority of constructors which initialize properties would initialize them all, then allow e.g.: [InitializesAll]
public Person(Person other)
{
FirstName = other.FirstName;
LastName = other.LastName;
} Constructor chaining comes naturally: class Rectangle
{
public int Length { get; required init; }
public int Width { get; required init; }
}
class Square : Rectangle
{
[Initializes(nameof(Length), nameof(Width))]
public Square(int side)
{
Length = side;
Width = side;
}
[Initializes(nameof(Length), nameof(Width))]
public Square(int side)
{
var transformed = transformSideLength(side);
Length = transformed;
Width = transformed;
static void transformSideLength(int side) { ... }
}
} There's a decision to be made on whether chained constructors automatically inherit the For bonus points, there's a route through to factories: [Uninitialized(nameof(Person.LastName))]
public static Person CreateAMads()
{
return new Person() { FirstName = "Mads" };
} The compiler enforces that the method Implementation-wise, this might mean a modreq on all construtors which don't initialize all properties, or you could potentially tie it into the validation discussion (which is happening wrt records) and fall back to a validator-generated runtime exception if a downlevel compiler / other language / ioc container doesn't set all required properties. Obviously, all attribute names, I've probably missed some vital point which shoots this down, as well... |
Beta Was this translation helpful? Give feedback.
-
For a DTO class, I just want to put names and types of the properties, and optionally default values. Whether a property is required or not should be inferred, based on nullability of the types and default values. And these properties should be immutable and public by default. I can do it in very few languages. TypeScript and Kotlin are closest to what I want. I don't know if it will ever be possible in C#. |
Beta Was this translation helpful? Give feedback.
-
What are the benefits of object initialization syntax?
What are the benefits of constructor parameter initialization?
As far as I can tell, the only real feature difference between object initialization and constructors today is in fact the requiredness issue. For some reason, the LDT seems to "forget" about named parameters in these proposals about fixing/bypassing constructors. Named parameters are perfectly legitimate and give most of the same benefits attributed to object-initialization syntax. IMO, this proposal seems to be giving us duplicative functionality that doesn't actually save much code because you're still repeating yourself by typing every property name again. |
Beta Was this translation helpful? Give feedback.
-
I am also curious whether the LDT is interested in init-methods (i.e., methods that can only be called in initializer, related issues: dotnet/roslyn#5365, #4218). Some points I can think of:
|
Beta Was this translation helpful? Give feedback.
-
We discussed required properties again in LDM today, with the following proposal as an extension to the original proposal in this issue. As we're going to keep iterating on this, I'm not going to fully update the original proposal in this thread or in the championed issue thread until we come to a better consensus on the form we want this to take. See also the notes from today's LDM: https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-01-11.md. Required Properties Simple Base SyntaxLast time we talked about required properties, we came to the conclusion that while we like the principle, and we liked many aspects of the proposal, we felt it was too complicated for the simple case. Much of the community feedback to that proposal falls into a similar vein. To try and remedy this, we've gone back and taken another look at allowing requiredness to be stated on the property, coming to a version that I've been calling exploded require lists. Almost all of the syntax from the previous proposal still applies here: this extension is simply adding a new sugar to make the simple cases easier to express. It also changes the modifier from
|
Beta Was this translation helpful? Give feedback.
-
LDM looked at a new version of this spec today (3/3/2021, read that month-date-year or day-month-year, as you please). The spec is PR here, and while there is still some design work and refinement to be done, the overall shape is in a good state now. |
Beta Was this translation helpful? Give feedback.
LDM looked at a new version of this spec today (3/3/2021, read that month-date-year or day-month-year, as you please). The spec is PR here, and while there is still some design work and refinement to be done, the overall shape is in a good state now.