Init Only Setters ===== ## Summary This proposal adds the concept of init only properties and indexers to C#. These properties and indexers can be set at the point of object creation but become effectively `get` only once object creation has completed. This allows for a much more flexible immutable model in C#. ## Motivation The underlying mechanisms for building immutable data in C# haven't changed since 1.0. They remain: 1. Declaring fields as `readonly`. 1. Declaring properties that contain only a `get` accessor. These mechanisms are effective at allowing the construction of immutable data but they do so by adding cost to the boilerplate code of types and opting such types out of features like object and collection initializers. This means developers must choose between ease of use and immutability. A simple immutable object like `Point` requires twice as much boiler plate code to support construction as it does to declare the type. The bigger the type the bigger the cost of this boiler plate: ```cs struct Point { public int X { get; } public int Y { get; } public Point(int x, int y) { this.X = x; this.Y = y; } } ``` The `init` accessor makes immutable objects more flexible by allowing the caller to mutate the members during the act of construction. That means the object's immutable properties can participate in object initializers and thus removes the need for all constructor boilerplate in the type. The `Point` type is now simply: ```cs struct Point { public int X { get; init; } public int Y { get; init; } } ``` The consumer can then use object initializers to create the object ```cs var p = new Point() { X = 42, Y = 13 }; ``` ## Detailed Design ### init accessors An init only property (or indexer) is declared by using the `init` accessor in place of the `set` accessor: ```cs class Student { public string FirstName { get; init; } public string LastName { get; init; } } ``` An instance property containing an `init` accessor is considered settable in the following circumstances: - During an object initializer - During a `with` expression initializer - Inside an instance constructor of the containing or derived type, on `this` or `base` - Inside the `init` accessor of any property, on `this` or `base` - Inside attribute usages with named parameters The times above in which the `init` accessors are settable are collectively referred to in this document as the construction phase of the object. This means the `Student` class can be used in the following ways: ```cs var s = new Student() { FirstName = "Jared", LastName = "Parosns", }; s.LastName = "Parsons"; // Error: LastName is not settable ``` The rules around when `init` accessors are settable extend across type hierarchies. If the member is accessible and the object is known to be in the construction phase then the member is settable. That specifically allows for the following: ```cs class Base { public bool Value { get; init; } } class Derived : Base { Derived() { // Not allowed with get only properties but allowed with init Value = true; } } class Consumption { void Example() { var d = new Derived() { Value = true; }; } } ``` At the point a `init` accessor is invoked the instance is known to be in the open construction phase. Hence an `init` accessor is allowed to take the following actions in addition to what a normal `set` accessor can do: 1. Call other `init` accessors available through `this` or `base` 1. Assign `readonly` fields declared on the same type through `this` ```cs class Complex { readonly int Field1; int Field2; int Prop1 { get; init ; } int Prop2 { get => 42; init { Field1 = 13; // okay Field2 = 13; // okay Prop1 = 13; // okay } } } ``` The ability to assign `readonly` fields from an `init` accessor is limited to those fields declared on the same type as the accessor. It cannot be used to assign `readonly` fields in a base type. This rule ensures that type authors remain in control over the mutability behavior of their type. Developers who do not wish to utilize `init` cannot be impacted from other types choosing to do so: ```cs class Base { internal readonly int Field; internal int Property { get => Field; init => Field = value; // Okay } internal int OtherProperty { get; init; } } class Derived : Base { internal readonly int DerivedField; internal int DerivedProperty { get => DerivedField; init { DerivedField = 42; // Okay Property = 0; // Okay Field = 13; // Error Field is readonly } } public Derived() { Property = 42; // Okay Field = 13; // Error Field is readonly } } ``` When `init` is used in a virtual property then all the overrides must also be marked as `init`. Likewise it is not possible to override a simple `set` with `init`. ```cs class Base { public virtual int Property { get; init; } } class C1 : Base { public override int Property { get; init; } } class C2 : Base { // Error: Property must have init to override Base.Property public override int Property { get; set; } } ``` An `interface` declaration can also participate in `init` style initialization via the following pattern: ```cs interface IPerson { string Name { get; init; } } class Init { void M() where T : IPerson, new() { var local = new T() { Name = "Jared" }; local.Name = "Jraed"; // Error } } ``` Restrictions of this feature: - The `init` accessor can only be used on instance properties - A property cannot contain both an `init` and `set` accessor - All overrides of a property must have `init` if the base had `init`. This rule also applies to interface implementation. ### Metadata encoding Property `init` accessors will be emitted as a standard `set` accessor with the return type marked with a modreq of `IsExternalInit`. This is a new type which will have the following definition: ```cs namespace System.Runtime.CompilerServices { public sealed class IsExternalInit { } } ``` The compiler will match the type by full name. There is no requirement that it appear in the core library. If there are multiple types by this name then the compiler will tie break in the following order: 1. The one defined in the project being compiled 1. The one defined in corelib If neither of these exist then a type ambiguity error will be issued. The design for `IsExternalInit` is futher covered in [this issue](https://github.com/dotnet/runtime/issues/34978) ## Questions ### Breaking changes One of the main pivot points in how this feature is encoded will come down to the following question: > Is it a binary breaking change to replace `init` with `set`? Replacing `init` with `set` and thus making a property fully writable is never a source breaking change on a non-virtual property. It simply expands the set of scenarios where the property can be written. The only behavior in question is whether or not this remains a binary breaking change. If we want to make the change of `init` to `set` a source and binary compatible change then it will force our hand on the modreq vs. attributes decision below because it will rule out modreqs as a soulution. If on the other hand this is seen as a non-interesting then this will make the modreq vs. attribute decision less impactful. **Resolution** This scenario is not seen as compelling by LDM. ### Modreqs vs. attributes The emit strategy for `init` property accessors must choose between using attributes or modreqs when emitting during metadata. These have different trade offs that need to be considered. Annotating a property set accessor with a modreq declaration means CLI compliant compilers will ignore the accessor unless it understands the modreq. That means only compilers aware of `init` will read the member. Compilers unaware of `init` will ignore the `set` accessor and hence will not accidentally treat the property as read / write. The downside of modreq is `init` becomes a part of the binary signature of the `set` accessor. Adding or removing `init` will break binary compatbility of the application. Using attributes to annotate the `set` accessor means that only compilers which understand the attribute will know to limit access to it. A compiler unaware of `init` will see it as a simple read / write property and allow access. This would seemingly mean this decision is a choice between extra safety at the expense of binary compatibility. Digging in a bit the extra safety is not exactly what it seems. It will not protect against the following circumstances: 1. Reflection over `public` members 1. The use of `dynamic` 1. Compilers that don't recognize modreqs It should also be considered that, when we complete the IL verification rules for .NET 5, `init` will be one of those rules. That means extra enforcement will be gained from simply verifying compilers emitting verifiable IL. The primary languages for .NET (C#, F# and VB) will all be updated to recognize these `init` accessors. Hence the only realistic scenario here is when a C# 9 compiler emits `init` properties and they are seen by an older toolset such as C# 8, VB 15, etc ... C# 8. That is the trade off to consider and weigh against binary compatibility. **Note** This discussion primarily applies to members only, not to fields. While `init` fields were rejected by LDM they are still interesting to consider for the modreq vs. attribute discussion. The `init` feature for fields is a relaxation of the existing restriction of `readonly`. That means if we emit the fields as `readonly` + an attribute there is no risk of older compilers mis-using the field because they would already recognize `readonly`. Hence using a modreq here doesn't add any extra protection. **Resolution** The feature will use a modreq to encode the property `init` setter. The compelling factors were (in no particular order): * Desire to discourage older compilers from violating `init` semantics * Desire to make adding or removing `init` in a `virtual` declaration or `interface` both a source and binary breaking change. Given there was also no significant support for removing `init` to be a binary compatible change it made the choice of using modreq straight forward. ### init vs. initonly There were three syntax forms which got significant consideration during our LDM meeting: ```cs // 1. Use init int Option1 { get; init; } // 2. Use init set int Option2 { get; init set; } // 3. Use initonly int Option3 { get; initonly; } ``` **Resolution** There was no syntax which was overwhelmingly favored in LDM. One point which got significant attention was how the choice of syntax would impact our ability to do `init` members as a general feature in the future. Choosing option 1 would mean that it would be difficult to define a property which had a `init` style `get` method in the future. Eventually it was decided that if we decided to go forward with general `init` members in future we could allow `init` to be a modifier in the property accessor list as well as a short hand for `init set`. Essentially the following two declarations would be identical. ```cs int Property1 { get; init; } int Property1 { get; init set; } ``` The decision was made to move forward with `init` as a standalone accessor in the property accessor list. ### Warn on failed init Consider the following scenario. A type declares an `init` only member which is not set in the constructor. Should the code which constructs the object get a warning if they failed to initialize the value? At that point it is clear the field will never be set and hence has a lot of similarities with the warning around failing to initialize `private` data. Hence a warning would seemingly have some value here? There are significant downsides to this warning though: 1. It complicates the compatibility story of changing `readonly` to `init`. 1. It requires carrying additional metadata around to denote the members which are required to be initialized by the caller. Further if we believe there is value here in the overall scenario of forcing object creators to be warned / error'd about specific fields then this likely makes sense as a general feature. There is no reason it should be limited to just `init` members. **Resolution** There will be no warning on consumption of `init` fields and properties. LDM wants to have a broader discussion on the idea of required fields and properties. That may cause us to come back and reconsider our position on `init` members and validation. ## Allow init as a field modifier In the same way `init` can serve as a property accessor it could also serve as a designation on fields to give them similar behaviors as `init` properties. That would allow for the field to be assigned before construction was complete by the type, derived types, or object initializers. ```cs class Student { public init string FirstName; public init string LastName; } var s = new Student() { FirstName = "Jarde", LastName = "Parsons", } s.FirstName = "Jared"; // Error FirstName is readonly ``` In metadata these fields would be marked in the same way as `readonly` fields but with an additional attribute or modreq to indicate they are `init` style fields. **Resolution** LDM agrees this proposal is sound but overall the scenario felt disjoint from properties. The decision was to proceed only with `init` properties for now. This has a suitable level of flexibility as an `init` property can mutate a `readonly` field on the declaring type of the property. This will be reconsidered if there is significant customer feedback that justifies the scenario. ### Allow init as a type modifier In the same way the `readonly` modifier can be applied to a `struct` to automatically declare all fields as `readonly`, the `init` only modifier can be declared on a `struct` or `class` to automatically mark all fields as `init`. This means the following two type declarations are equivalent: ```cs struct Point { public init int X; public init int Y; } // vs. init struct Point { public int X; public int Y; } ``` **Resolution** This feature is too *cute* here and conflicts with the `readonly struct` feature on which it is based. The `readonly struct` feature is simple in that it applies `readonly` to all members: fields, methods, etc ... The `init struct` feature would only apply to properties. This actually ends up making it confusing for users. Given that `init` is only valid on certain aspects of a type we rejected the idea of having it as a type modifier. ## Considerations ### Compatibility The `init` feature is designed to be compatible with existing `get` only properties. Specifically it is meant to be a completely additive change for a property which is `get` only today but desires more flexbile object creation semantics. For example consider the following type: ```cs class Name { public string First { get; } public string Last { get; } public Name(string first, string last) { First = first; Last = last; } } ``` Adding `init` to these properties is a non-breaking change: ```cs class Name { public string First { get; init; } public string Last { get; init; } public Name(string first, string last) { First = first; Last = last; } } ``` ### IL verification When .NET Core decides to re-implement IL verification the rules will need to be adjusted to account for `init` members. This will need to be included in the rule changes for non-mutating acess to `readonly` data. The IL verification rules will need to be broken into two parts: 1. Allowing `init` members to set a `readonly` field. 1. Determining when an `init` member can be legally called. The first is a simple adjustment to the existing rules. The IL verifier can be taught to recognize `init` members and from there it just needs to consider a `readonly` field to be settable on `this` in such a member. The second rule is more complicated. In the simple case of object initializers the rule is straight forward. It should be legal to call `init` members when the result of a `new` expression is still on the stack. That is until the value has been stored in a local, array element or field or passed as an argument to another method it will still be legal to call `init` members. This ensures that once the result of the `new` expression is published to a named identifier (other than `this`) then it will no longer be legal to call `init` members. The more complicated case though is when we mix `init` members, object initializers and `await`. That can cause the newly created object to be temporarily hoisted into a state machine and hence put into a field. ```cs var student = new Student() { Name = await SomeMethod() }; ``` Here the result of `new Student()` will be hoised into a state machine as a field before the set of `Name` occurs. The compiler will need to mark such hoisted fields in a way that the IL verifier understands they're not user accessible and hence doesn't violate the intended semantics of `init`. ### init members The `init` modifier could be extended to apply to all instance members. This would generalize the concept of `init` during object construction and allow types to declare helper methods that could partipate in the construction process to initialize `init` fields and properties. Such members would have all the restricions that an `init` accessor does in this design. The need is questionable though and this can be safely added in a future version of the language in a compatible manner. ### Generate three accessors One potential implementation of `init` properties is to make `init` completely separate from `set`. That means that a property can potentially have three different accessors: `get`, `set` and `init`. This has the potential advantage of allowing the use of modreq to enforce correctness while maintaining binary compatibility. The implementation would roughly be the following: 1. An `init` accessor is always emitted if there is a `set`. When not defined by the developer it is simply a reference to `set`. 1. The set of a property in an object initializer will always use `init` if present but fall back to `set` if it's missing. This means that a developer can always safely delete `init` from a property. The downside of this design is that is only useful if `init` is **always** emitted when there is a `set`. The language can't know if `init` was deleted in the past, it has to assume it was and hence the `init` must always be emitted. That would cause a significant metadata expansion and is simply not worth the cost of the compatibility here.