From 8a49b976d4a8d652eae4edf5713618c950168c4e Mon Sep 17 00:00:00 2001 From: jnm2 Date: Mon, 22 Apr 2024 20:58:53 -0400 Subject: [PATCH 01/28] Add detailed design to clarify the field keyword spec --- proposals/semi-auto-properties.md | 285 +++++++++++++++++++++++------- 1 file changed, 223 insertions(+), 62 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 37638a9040..8ea128e965 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -10,6 +10,228 @@ Two common scenarios are that you want to apply a constraint on the setter, ensu In these cases by now you always have to create an instance field and write the whole property yourself. This not only adds a fair amount of code, but it also leaks the `field` into the rest of the type's scope, when it is often desirable to only have it be available to the bodies of the accessors. +## Detailed design + +For properties with an `init` accessor, everything that applies below to `set` would apply instead to the `init` accessor. + +**Principle 1:** Every property can be thought of as having a backing field by default, which is elided when not used. The field is referenced using the keyword `field` and its visibility is scoped to the accessor bodies. + +**Principle 2:** `get;` will now be considered syntactic sugar for `get => field;`, and `set;` will now be considered syntactic sugar for `set => field = value;`. + +This means that properties may now mix and match auto accessors with full accessors. For example: + +```cs +{ get; set => Set(ref field, value); } +``` + +```cs +{ get => field ?? GetDefault(); set; } +``` + +Both accessors may be full accessors with either one or both making use of `field`: + +```cs +{ get => field; set => field = value; } +``` + +```cs +{ get => field; set => throw new InvalidOperationException(); } +``` + +```cs +{ get => overriddenValue; set => field = value; } +``` + +Expression-bodied properties and properties with only a `get` accessor may also use `field`: + +```cs +public string LazilyComputed => field ??= Compute(); +``` + +```cs +public string LazilyComputed { get => field ??= Compute(); } +``` + +A special case is made to prevent properties with only a `set` accessor from using `field` because there would be no way to access the value of `field` except in subsequent sets, when it will be presumed to be overwritten. + +```cs +// ❌ Error, will not compile +public string Why { set => field = value; } +``` + +### Field-targeted attributes + +Just as with regular auto-properties, any property that uses a backing field in one of its accessors will be able to use field-targeted attributes: + +```cs +[field: Xyz] +public string Name => field ??= Compute(); + +[field: Xyz] +public string Name { get => field; set => field = value; } +``` + +A field-targeted attribute will remain invalid unless an accessor uses a backing field: + +```cs +// ❌ Error, will not compile +[field: Xyz] +public string Name => Compute(); +``` + +### Constructor assignment + +Just like with existing auto-properties, assignment in the constructor calls the setter if it exists, and if there is no setter it falls back to directly assigning to the backing field. + +```cs +class C +{ + public C() + { + P1 = 1; // Assigns P1's backing field directly + P2 = 2; // Assigns P2's backing field directly + P3 = 3; // Calls P3's setter + } + + public int P1 => field; + public int P2 { get => field; } + public int P3 { get => field; set => field = value; } +} +``` + +### Property initializers + +Properties with initializers may use `field`. The backing field is directly initialized rather than the setter being called ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#open-questions-in-field)). + +Calling a setter for an initializer is not an option; initializers are processed before calling base constructors, and it is illegal to call any instance method before the base constructor is called. This is also important for default initialization/definite assignment of structs. + +This luckily gives control over whether or not you want to initialize the backing field directly or call the property setter: if you want to initialize without calling the setter, you use a property initializer. If you want to initialize by calling the setter, you use assign the property an initial value in the constructor. + +Here's an example of where this is useful. The `field` keyword will find a lot of its use with view models because of the neat solution it brings for the `INotifyPropertyChanged` pattern. View model property setters are likely to be databound to UI and likely to cause change tracking or trigger other behaviors. The following code needs to initialize the default value of `IsActive` without setting `HasPendingChanges` to `true`: + +```cs +using System.Runtime.CompilerServices; + +class SomeViewModel +{ + public bool HasPendingChanges { get; private set; } + + public bool IsActive { get; set => Set(ref field, value); } = true; + + private bool Set(ref T location, T value) + { + if (RuntimeHelpers.Equals(location, value)) return false; + location = value; + HasPendingChanges = true; + return true; + } +} +``` + +This difference in behavior between a property initializer and assigning from the constructor can also be seen with virtual auto-properties in previous versions of the language: + +```cs +using System; + +// Nothing is printed; the property initializer is not +// equivalent to `this.IsActive = true`. +_ = new Derived(); + +class Base +{ + public virtual bool IsActive { get; set; } = true; +} + +class Derived : Base +{ + public override bool IsActive + { + get => base.IsActive; + set + { + base.IsActive = value; + Console.WriteLine("This will not be reached"); + } + } +} +``` + +### Definite assignment in structs + +A semi-auto property will be treated as a regular auto property for the purposes of calculating default backing field initialization if its setter is automatically implemented, or if it does not have a setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#property-assignment-in-structs)). + +Default initialize a struct when calling a manually implemented semi-auto property setter, and issue a warning when doing so, like a regular property setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#definite-assignment-of-manually-implemented-setters)). + +### Nullability + +When `{ get; }` is written as `{ get => field; }`, or `{ get; set; }` is written as `{ get => field; set => field = value; }`, a similar warning should be produced when a non-nullable property is not initialized: + +```cs +class C +{ + // ⚠️ CS8618: Non-nullable property 'P' must contain a + // non-null value when exiting constructor. + public string P { get => field; set => field = value; } +} +``` + +No warning should be produced if the property is initialized to a non-null value via constructor assignment or property initializer: + +```cs +class C +{ + public C() { P = ""; } + + public string P { get => field; set => field = value; } +} +``` + +```cs +class C +{ + public string P { get => field; set => field = value; } = ""; +} +``` + +For reference-typed fields, the `field` type should be nullable. No warning should be produced in the following example: + +```cs +class C +{ + public string P => field ??= "test"; +} +``` + +### `nameof` + +`nameof(field)` will be disallowed. It is not like `nameof(value)`, which is the thing to use when property setters throw ArgumentException as some do in the .NET core libraries. In contrast, `nameof(field)` has no expected use cases. If it did anything, it would return the string `"field"`, consistent with how `nameof` behaves in other circumstances by returning the C# name or alias, rather than the metadata name. + +### Overrides + +Like with regular auto properties, semi-auto properties that override a base property must override all accessors ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#partial-overrides-of-virtual-properties)). + +### Shadowing + +`field` can be shadowed by parameters or locals in a nested scope ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#open-questions-in-field)). Since `field` represents a field in the type, even if anonymously, the shadowing rules of regular fields should apply. + +### Captures + +`field` should be able to be captured in local functions and lambdas, and references to `field` from inside local functions and lambdas should be allowed even if there are no other references ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-21.md#open-question-in-semi-auto-properties)): + +```cs +public class C +{ + public static int P + { + get + { + Func f = static () => field; + return f(); + } + } +} +``` + ## Specification changes The following changes are to be made to [§14.7.4](https://github.com/dotnet/csharpstandard/blob/draft-v6/standard/classes.md#1474-automatically-implemented-properties): @@ -120,71 +342,10 @@ The following changes are to be made to [§14.7.4](https://github.com/dotnet/csh +``` ```` -## Open LDM questions: +## Open LDM questions 1. If a type does have an existing accessible `field` symbol in scope (like a field called `field`) should there be any way for an auto-prop to still use `field` internally to both create and refer to an auto-prop field. Under the current rules there is no way to do that. This is certainly unfortunate for those users, however this is ideally not a significant enough issue to warrant extra dispensation. The user, after all, can always still write out their properties like they do today, they just lose out from the convenience here in that small case. -2. Should initializers use the backing field or the property setter? If the latter, what about `public int P { get => field; } = 5;`? - - * Calling a setter for an initializer is not an option because initializers are processed before calling base constructor and it is illegal to call any instance method before the base constructor is called. - - * If the initializer assigns directly to the backing field when there is a setter, then the initializer does one thing and an assignment to the property within constructors does a different thing (calls the setter). Today there is already a semantic difference between an initializer and an assignment in constructors in such cases. This difference can be observed with virtual auto properties: - - ```cs - using System; - - // Nothing is printed; the property initializer is not - // equivalent to `this.IsActive = true`. - _ = new Derived(); - - class Base - { - public virtual bool IsActive { get; set; } = true; - } - - class Derived : Base - { - public override bool IsActive - { - get => base.IsActive; - set - { - base.IsActive = value; - Console.WriteLine("This will not be called"); - } - } - } - ``` - - * There is a practical benefit when initializers skip calling the setter. It allows users of the language to choose whether to invoke the setter or not (constructor assignment or property initializer). If property initializers call the setter, this choice is taken away. Allowing language users to make this choice means that the `field` feature would be able to be used in more scenarios. - - One example of this is view models. The `field` keyword will find a lot of its use with view models because of the neat solution it brings for the `INotifyPropertyChanged` pattern. View model property setters are likely to be databound to UI and likely to cause change tracking or trigger other behaviors. Consider the following example which needs to initialize the default value of `Foo` without setting `HasPendingChanges` to `true`. If initializers call the setter, using the `field` keyword would not be an option or would require setting `HasPendingChanges` back to false in the constructor(s) which feels like a workaround: unnecessary work is being done which also leaves behind a "mess" which needs to be manually reversed, if the property initializer calls the setter. - - ```cs - using System.Runtime.CompilerServices; - - class SomeViewModel - { - public bool HasPendingChanges { get; private set; } - - public int Foo { get; set => Set(ref field, value); } = 1; - - private bool Set(ref T location, T value) - { - if (RuntimeHelpers.Equals(location, value)) return false; - location = value; - HasPendingChanges = true; - return true; - } - } - ``` - - * A preexisting expectation may exist as a result of refactoring to and from auto properties. Some language users turn field initializers into property initializers and vice versa rather than moving the initialization far away from the field declaration into the constructor(s). This forms a mental model that is consistent with how the language already behaves with virtual auto properties. (See the virtual auto property example above.) - -3. Definite assignment related questions: - - https://github.com/dotnet/csharplang/issues/5563 - - https://github.com/dotnet/csharplang/pull/5573#issuecomment-1002110830 - ## LDM history: - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-03-10.md#field-keyword - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-14.md#field-keyword From c2bf68767f04832699c1ae38ba51377e6b610738 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Tue, 23 Apr 2024 00:07:54 -0400 Subject: [PATCH 02/28] Tweaks --- proposals/semi-auto-properties.md | 74 +++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 8ea128e965..63b9d20f20 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -52,16 +52,16 @@ public string LazilyComputed => field ??= Compute(); public string LazilyComputed { get => field ??= Compute(); } ``` -A special case is made to prevent properties with only a `set` accessor from using `field` because there would be no way to access the value of `field` except in subsequent sets, when it will be presumed to be overwritten. +As with regular auto-properties, a setter that uses a backing field is disallowed when there is no getter. This restriction could be loosened in the future to allow the setter to do something only in response to changes, by comparing `value` to `field` (see open questions). ```cs // ❌ Error, will not compile -public string Why { set => field = value; } +{ set => field = value; } ``` ### Field-targeted attributes -Just as with regular auto-properties, any property that uses a backing field in one of its accessors will be able to use field-targeted attributes: +As with regular auto-properties, any property that uses a backing field in one of its accessors will be able to use field-targeted attributes: ```cs [field: Xyz] @@ -79,26 +79,6 @@ A field-targeted attribute will remain invalid unless an accessor uses a backing public string Name => Compute(); ``` -### Constructor assignment - -Just like with existing auto-properties, assignment in the constructor calls the setter if it exists, and if there is no setter it falls back to directly assigning to the backing field. - -```cs -class C -{ - public C() - { - P1 = 1; // Assigns P1's backing field directly - P2 = 2; // Assigns P2's backing field directly - P3 = 3; // Calls P3's setter - } - - public int P1 => field; - public int P2 { get => field; } - public int P3 { get => field; set => field = value; } -} -``` - ### Property initializers Properties with initializers may use `field`. The backing field is directly initialized rather than the setter being called ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#open-questions-in-field)). @@ -156,6 +136,26 @@ class Derived : Base } ``` +### Constructor assignment + +As with existing auto-properties, assignment in the constructor calls the setter if it exists, and if there is no setter it falls back to directly assigning to the backing field. + +```cs +class C +{ + public C() + { + P1 = 1; // Assigns P1's backing field directly + P2 = 2; // Assigns P2's backing field directly + P3 = 3; // Calls P3's setter + } + + public int P1 => field; + public int P2 { get => field; } + public int P3 { get => field; set => field = value; } +} +``` + ### Definite assignment in structs A semi-auto property will be treated as a regular auto property for the purposes of calculating default backing field initialization if its setter is automatically implemented, or if it does not have a setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#property-assignment-in-structs)). @@ -346,6 +346,34 @@ The following changes are to be made to [§14.7.4](https://github.com/dotnet/csh 1. If a type does have an existing accessible `field` symbol in scope (like a field called `field`) should there be any way for an auto-prop to still use `field` internally to both create and refer to an auto-prop field. Under the current rules there is no way to do that. This is certainly unfortunate for those users, however this is ideally not a significant enough issue to warrant extra dispensation. The user, after all, can always still write out their properties like they do today, they just lose out from the convenience here in that small case. +1. Which of these scenarios should be allowed to compile? Assume that the "field is never read" warning would apply just like with a manually declared field. + + 1. `{ set; }` - Disallowed today, continue disallowing + 1. `{ set => field = value; }` + 1. `{ get => unrelated; set => field = value; }` + 1. `{ get => unrelated; set; }` + 1. ```cs + { + set + { + if (field == value) return; + field = value; + SendEvent(nameof(Prop), value); + } + } + ``` + 1. ```cs + { + get => unrelated; + set + { + if (field == value) return; + field = value; + SendEvent(nameof(Prop), value); + } + } + ``` + ## LDM history: - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-03-10.md#field-keyword - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-14.md#field-keyword From 299b07f5c33ae329f87fcf7392796ecb257d8d2a Mon Sep 17 00:00:00 2001 From: jnm2 Date: Tue, 23 Apr 2024 21:59:18 -0400 Subject: [PATCH 03/28] Respond to review feedback --- proposals/semi-auto-properties.md | 37 +++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 63b9d20f20..96e77892ca 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -14,7 +14,7 @@ In these cases by now you always have to create an instance field and write the For properties with an `init` accessor, everything that applies below to `set` would apply instead to the `init` accessor. -**Principle 1:** Every property can be thought of as having a backing field by default, which is elided when not used. The field is referenced using the keyword `field` and its visibility is scoped to the accessor bodies. +**Principle 1:** Every property declaration can be thought of as having a backing field by default, which is elided when not used. The field is referenced using the keyword `field` and its visibility is scoped to the accessor bodies. **Principle 2:** `get;` will now be considered syntactic sugar for `get => field;`, and `set;` will now be considered syntactic sugar for `set => field = value;`. @@ -25,7 +25,7 @@ This means that properties may now mix and match auto accessors with full access ``` ```cs -{ get => field ?? GetDefault(); set; } +{ get => field ?? parent.AmbientValue; set; } ``` Both accessors may be full accessors with either one or both making use of `field`: @@ -42,6 +42,18 @@ Both accessors may be full accessors with either one or both making use of `fiel { get => overriddenValue; set => field = value; } ``` +```cs +{ + get; + set + { + if (field == value) return; + field == value; + OnXyzChanged(); + } +} +``` + Expression-bodied properties and properties with only a `get` accessor may also use `field`: ```cs @@ -148,11 +160,13 @@ class C P1 = 1; // Assigns P1's backing field directly P2 = 2; // Assigns P2's backing field directly P3 = 3; // Calls P3's setter + P4 = 4; // Calls P4's setter } public int P1 => field; public int P2 { get => field; } - public int P3 { get => field; set => field = value; } + public int P4 { get => field; set => field = value; } + public int P3 { get => field; set; } } ``` @@ -193,21 +207,30 @@ class C } ``` -For reference-typed fields, the `field` type should be nullable. No warning should be produced in the following example: +In the same vein as how `var` infers as nullable for reference types, the `field` type should be nullable for reference types. This makes sense of `field ??` as not being followed by dead code, and it avoids producing a misleading warning in the following example: ```cs -class C +public string AmbientValue { - public string P => field ??= "test"; + get => field ?? parent.AmbientValue; + set + { + if (value == parent.AmbientValue) + field = null; // No warning here. Resume following the parent's value. + else + field = value; // Stop following the parent's value + } } ``` ### `nameof` -`nameof(field)` will be disallowed. It is not like `nameof(value)`, which is the thing to use when property setters throw ArgumentException as some do in the .NET core libraries. In contrast, `nameof(field)` has no expected use cases. If it did anything, it would return the string `"field"`, consistent with how `nameof` behaves in other circumstances by returning the C# name or alias, rather than the metadata name. +`nameof(field)` will fail to compile, like `nameof(nint)`. It is not like `nameof(value)`, which is the thing to use when property setters throw ArgumentException as some do in the .NET core libraries. In contrast, `nameof(field)` has no expected use cases. If it did anything, it would return the string `"field"`, consistent with how `nameof` behaves in other circumstances by returning the C# name or alias, rather than the metadata name. ### Overrides +Overriding properties may use `field`. Such usages of `field` refer to the backing field for the overriding property, separate from the backing field of the base property if it has one. There is no ABI for exposing the backing field of a base property to overriding classes since this would break encapsulation. + Like with regular auto properties, semi-auto properties that override a base property must override all accessors ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#partial-overrides-of-virtual-properties)). ### Shadowing From e507eb40b0e76085faadf1933113d694af800fcb Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 8 May 2024 22:04:15 -0400 Subject: [PATCH 04/28] Further thought on nullability design --- proposals/semi-auto-properties.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 96e77892ca..d9909a89d9 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -223,6 +223,10 @@ public string AmbientValue } ``` +Just like with nullability of `var`, it's expected that such patterns will exist with manually-declared backing fields. + +To land in this sweet spot implicitly, without having to write an attribute each time, nullability analysis will combine an inherent nullability of the field with the behavior of `[field: NotNull]`. This allows maybe-null assignments without warning, which is desirable as shown above, while simultaneously allowing a scenario like `=> field.Trim();` without requiring an intervention to silence a warning that `field` could be null. Making sure `field` has been assigned is already covered by the warning that ensures non-nullable properties are assigned by the end of each constructor. + ### `nameof` `nameof(field)` will fail to compile, like `nameof(nint)`. It is not like `nameof(value)`, which is the thing to use when property setters throw ArgumentException as some do in the .NET core libraries. In contrast, `nameof(field)` has no expected use cases. If it did anything, it would return the string `"field"`, consistent with how `nameof` behaves in other circumstances by returning the C# name or alias, rather than the metadata name. From 7ae7db0bedffed85b82f5aa97f2bff7014bc6ae2 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 8 May 2024 22:16:10 -0400 Subject: [PATCH 05/28] Further clarifications --- proposals/semi-auto-properties.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index d9909a89d9..ad36c79a31 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -18,6 +18,8 @@ For properties with an `init` accessor, everything that applies below to `set` w **Principle 2:** `get;` will now be considered syntactic sugar for `get => field;`, and `set;` will now be considered syntactic sugar for `set => field = value;`. +Both of these principles only apply in locations where `{ get; }` or `{ get; set; }` already declares an auto property. Abstract and interface properties are excluded, as well as indexers. + This means that properties may now mix and match auto accessors with full accessors. For example: ```cs From 21fedfbdd04207d1cb2aad276c3ceb2604b3073c Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 8 May 2024 22:16:16 -0400 Subject: [PATCH 06/28] Fix typo --- proposals/semi-auto-properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index ad36c79a31..f215564ab3 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -50,7 +50,7 @@ Both accessors may be full accessors with either one or both making use of `fiel set { if (field == value) return; - field == value; + field = value; OnXyzChanged(); } } From 70104d2587b18c342954af3b6f8bddbfc0ca4df8 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 8 May 2024 22:21:07 -0400 Subject: [PATCH 07/28] Add spec link for auto-property declaration conditions --- proposals/semi-auto-properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index f215564ab3..e1b3679440 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -18,7 +18,7 @@ For properties with an `init` accessor, everything that applies below to `set` w **Principle 2:** `get;` will now be considered syntactic sugar for `get => field;`, and `set;` will now be considered syntactic sugar for `set => field = value;`. -Both of these principles only apply in locations where `{ get; }` or `{ get; set; }` already declares an auto property. Abstract and interface properties are excluded, as well as indexers. +Both of these principles only apply under the same conditions where `{ get; }` or `{ get; set; }` already declares an auto property ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). Abstract and interface properties are excluded, as well as indexers. This means that properties may now mix and match auto accessors with full accessors. For example: From 5ae6b7ff5c69d4661fb2c47e474fdb43950a984f Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 8 May 2024 22:24:23 -0400 Subject: [PATCH 08/28] Stop quoting just part of the list of restrictions from the linked spec section --- proposals/semi-auto-properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index e1b3679440..43654c6ae9 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -18,7 +18,7 @@ For properties with an `init` accessor, everything that applies below to `set` w **Principle 2:** `get;` will now be considered syntactic sugar for `get => field;`, and `set;` will now be considered syntactic sugar for `set => field = value;`. -Both of these principles only apply under the same conditions where `{ get; }` or `{ get; set; }` already declares an auto property ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). Abstract and interface properties are excluded, as well as indexers. +Both of these principles only apply under the same conditions where `{ get; }` or `{ get; set; }` already declares an auto property ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). Indexers also remain unaffected. This means that properties may now mix and match auto accessors with full accessors. For example: From b6a2e75a08f7b86bd439508ef82b92bb44667c14 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Thu, 9 May 2024 00:47:23 -0400 Subject: [PATCH 09/28] Clarify that not all instances of nameof(field) are disallowed --- proposals/semi-auto-properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 43654c6ae9..03ca991ab6 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -231,7 +231,7 @@ To land in this sweet spot implicitly, without having to write an attribute each ### `nameof` -`nameof(field)` will fail to compile, like `nameof(nint)`. It is not like `nameof(value)`, which is the thing to use when property setters throw ArgumentException as some do in the .NET core libraries. In contrast, `nameof(field)` has no expected use cases. If it did anything, it would return the string `"field"`, consistent with how `nameof` behaves in other circumstances by returning the C# name or alias, rather than the metadata name. +In places where `field` is a keyword (see the [Shadowing](#shadowing) section), `nameof(field)` will fail to compile, like `nameof(nint)`. It is not like `nameof(value)`, which is the thing to use when property setters throw ArgumentException as some do in the .NET core libraries. In contrast, `nameof(field)` has no expected use cases. If it did anything, it would return the string `"field"`, consistent with how `nameof` behaves in other circumstances by returning the C# name or alias, rather than the metadata name. ### Overrides From 62c6f9c35cdb1442caad4b6fad14320894360325 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sat, 25 May 2024 13:18:37 -0400 Subject: [PATCH 10/28] Bring back explicit example of abstract/interface properties --- proposals/semi-auto-properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 03ca991ab6..9e646ecc70 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -18,7 +18,7 @@ For properties with an `init` accessor, everything that applies below to `set` w **Principle 2:** `get;` will now be considered syntactic sugar for `get => field;`, and `set;` will now be considered syntactic sugar for `set => field = value;`. -Both of these principles only apply under the same conditions where `{ get; }` or `{ get; set; }` already declares an auto property ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). Indexers also remain unaffected. +Both of these principles only apply under the same conditions where `{ get; }` or `{ get; set; }` already declares an auto property ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). For example, abstract and interface properties are excluded. Indexers also remain unaffected. This means that properties may now mix and match auto accessors with full accessors. For example: From a85baec101dc275ac0d0ef2c85ca26f0bac5db6f Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sat, 25 May 2024 13:34:41 -0400 Subject: [PATCH 11/28] Add glossary --- proposals/semi-auto-properties.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 9e646ecc70..488c0e51d6 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -10,6 +10,14 @@ Two common scenarios are that you want to apply a constraint on the setter, ensu In these cases by now you always have to create an instance field and write the whole property yourself. This not only adds a fair amount of code, but it also leaks the `field` into the rest of the type's scope, when it is often desirable to only have it be available to the bodies of the accessors. +## Glossary + +- **Auto property**: Short for "automatically implemented property" ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). Accessors on an auto property have no body. The implementation and backing storage are both provided by the compiler. + +- **Auto accessor**: Short for "automatically implemented accessor." This is an accessor that has no body. The implementation and backing storage are both provided by the compiler. + +- **Full accessor**: This is an accessor that has a body. The implementation is not provided by the compiler, though the backing storage may still be (as in the example `set => field = value;`). + ## Detailed design For properties with an `init` accessor, everything that applies below to `set` would apply instead to the `init` accessor. From de6d277680c637cd4deea042525c099e57e50fb6 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sat, 25 May 2024 13:57:16 -0400 Subject: [PATCH 12/28] Reword to remove the term 'semi-auto' since the feature covers properties that have no auto accessors --- proposals/semi-auto-properties.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 488c0e51d6..272dd1d225 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -1,14 +1,16 @@ -# Semi-auto-properties (a.k.a. `field` keyword in properties) +# `field` keyword in properties ## Summary -Extend auto-properties to allow them to still have an automatically generated backing field, while still allowing for bodies to be provided for accessors. Auto-properties can also use a new contextual `field` keyword in their body to refer to the auto-prop field. + +Extend all properties to allow them to reference an automatically generated backing field using the new contextual keyword `field`. Properties may now also contain an accessor _without_ a body alongside an accessor _with_ a body. ## Motivation -Standard auto-properties only allow for setting or getting the backing field directly, giving some control only by access modifying the accessor methods. Sometimes there is more need to have control over what happens when accessing an auto-property, without being confronted with all overhead of a standard property. -Two common scenarios are that you want to apply a constraint on the setter, ensuring the validity of a value. The other being raising an event that informs about the property going to be changed/having been changed. +Auto properties only allow for directly setting or getting the backing field, giving some control only by placing access modifiers on the accessors. Sometimes there is a need to have additional control over what happens in one or both accessors, but this confronts users with the overhead of declaring a backing field. The backing field name must then be kept in sync with the property, and the backing field is scoped to the entire class which can result in accidental bypassing of the accessors from within the class. + +There are two common scenarios in particular: applying a constraint on the setter to ensuring the validity of a value, and raising an event such as `INotifyPropertyChanged.PropertyChanged`. -In these cases by now you always have to create an instance field and write the whole property yourself. This not only adds a fair amount of code, but it also leaks the `field` into the rest of the type's scope, when it is often desirable to only have it be available to the bodies of the accessors. +In these cases by now you always have to create an instance field and write the whole property yourself. This not only adds a fair amount of code, but it also leaks the backing field into the rest of the type's scope, when it is often desirable to only have it be available to the bodies of the accessors. ## Glossary @@ -74,7 +76,7 @@ public string LazilyComputed => field ??= Compute(); public string LazilyComputed { get => field ??= Compute(); } ``` -As with regular auto-properties, a setter that uses a backing field is disallowed when there is no getter. This restriction could be loosened in the future to allow the setter to do something only in response to changes, by comparing `value` to `field` (see open questions). +As with auto properties, a setter that uses a backing field is disallowed when there is no getter. This restriction could be loosened in the future to allow the setter to do something only in response to changes, by comparing `value` to `field` (see open questions). ```cs // ❌ Error, will not compile @@ -83,7 +85,7 @@ As with regular auto-properties, a setter that uses a backing field is disallowe ### Field-targeted attributes -As with regular auto-properties, any property that uses a backing field in one of its accessors will be able to use field-targeted attributes: +As with auto properties, any property that uses a backing field in one of its accessors will be able to use field-targeted attributes: ```cs [field: Xyz] @@ -130,7 +132,7 @@ class SomeViewModel } ``` -This difference in behavior between a property initializer and assigning from the constructor can also be seen with virtual auto-properties in previous versions of the language: +This difference in behavior between a property initializer and assigning from the constructor can also be seen with virtual auto properties in previous versions of the language: ```cs using System; @@ -160,7 +162,7 @@ class Derived : Base ### Constructor assignment -As with existing auto-properties, assignment in the constructor calls the setter if it exists, and if there is no setter it falls back to directly assigning to the backing field. +As with auto properties, assignment in the constructor calls the setter if it exists, and if there is no setter it falls back to directly assigning to the backing field. ```cs class C @@ -182,9 +184,9 @@ class C ### Definite assignment in structs -A semi-auto property will be treated as a regular auto property for the purposes of calculating default backing field initialization if its setter is automatically implemented, or if it does not have a setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#property-assignment-in-structs)). +A property which uses an automatic backing field will be treated as an auto property for the purposes of calculating default backing field initialization if its setter is automatically implemented, or if it does not have a setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#property-assignment-in-structs)). -Default initialize a struct when calling a manually implemented semi-auto property setter, and issue a warning when doing so, like a regular property setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#definite-assignment-of-manually-implemented-setters)). +Default-initialize a struct when calling a manually implemented setter of a property which uses an automatic backing field, and issue a warning when doing so, like a regular property setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#definite-assignment-of-manually-implemented-setters)). ### Nullability @@ -245,7 +247,7 @@ In places where `field` is a keyword (see the [Shadowing](#shadowing) section), Overriding properties may use `field`. Such usages of `field` refer to the backing field for the overriding property, separate from the backing field of the base property if it has one. There is no ABI for exposing the backing field of a base property to overriding classes since this would break encapsulation. -Like with regular auto properties, semi-auto properties that override a base property must override all accessors ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#partial-overrides-of-virtual-properties)). +Like with auto properties, properties which use the `field` keyword and override a base property must override all accessors ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#partial-overrides-of-virtual-properties)). ### Shadowing @@ -381,7 +383,7 @@ The following changes are to be made to [§14.7.4](https://github.com/dotnet/csh ## Open LDM questions -1. If a type does have an existing accessible `field` symbol in scope (like a field called `field`) should there be any way for an auto-prop to still use `field` internally to both create and refer to an auto-prop field. Under the current rules there is no way to do that. This is certainly unfortunate for those users, however this is ideally not a significant enough issue to warrant extra dispensation. The user, after all, can always still write out their properties like they do today, they just lose out from the convenience here in that small case. +1. If a type does have an existing accessible `field` symbol in scope (like a field called `field`) should there be any way for a property to still use `field` internally to both create and refer to an automatically-implemented backing field. Under the current rules there is no way to do that. This is certainly unfortunate for those users, however this is ideally not a significant enough issue to warrant extra dispensation. The user, after all, can always still write out their properties like they do today, they just lose out from the convenience here in that small case. 1. Which of these scenarios should be allowed to compile? Assume that the "field is never read" warning would apply just like with a manually declared field. From 37191a316b42740fd8d0a55e1b383d43856ce2ea Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sat, 25 May 2024 14:09:11 -0400 Subject: [PATCH 13/28] Add clarification that auto properties are properties without accessor bodies --- proposals/semi-auto-properties.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 272dd1d225..c038115a44 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -30,6 +30,8 @@ For properties with an `init` accessor, everything that applies below to `set` w Both of these principles only apply under the same conditions where `{ get; }` or `{ get; set; }` already declares an auto property ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). For example, abstract and interface properties are excluded. Indexers also remain unaffected. +"Auto property" will continue to mean a property whose accessors have no bodies. None of the examples below will be considered auto properties. + This means that properties may now mix and match auto accessors with full accessors. For example: ```cs From abc4532b5d5f4fc8415473d2f9ff50056f4a8295 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sat, 25 May 2024 14:25:39 -0400 Subject: [PATCH 14/28] Call out missing warning and add as open question --- proposals/semi-auto-properties.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index c038115a44..ac0af1bed2 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -241,6 +241,24 @@ Just like with nullability of `var`, it's expected that such patterns will exist To land in this sweet spot implicitly, without having to write an attribute each time, nullability analysis will combine an inherent nullability of the field with the behavior of `[field: NotNull]`. This allows maybe-null assignments without warning, which is desirable as shown above, while simultaneously allowing a scenario like `=> field.Trim();` without requiring an intervention to silence a warning that `field` could be null. Making sure `field` has been assigned is already covered by the warning that ensures non-nullable properties are assigned by the end of each constructor. +This sweet spot does come with the downside that there would be no warning in this situation: + +```cs +public string AmbientValue +{ + get => field; // No warning, but could return null! + set + { + if (value == parent.AmbientValue) + field = null; + else + field = value; + } +} +``` + +Open question: Should flow analysis combine the maybe-null end state for `field` from the setter with the "depends on nullness of `field`" for the getter's return, enabling a warning in the scenario above? + ### `nameof` In places where `field` is a keyword (see the [Shadowing](#shadowing) section), `nameof(field)` will fail to compile, like `nameof(nint)`. It is not like `nameof(value)`, which is the thing to use when property setters throw ArgumentException as some do in the .NET core libraries. In contrast, `nameof(field)` has no expected use cases. If it did anything, it would return the string `"field"`, consistent with how `nameof` behaves in other circumstances by returning the C# name or alias, rather than the metadata name. @@ -415,6 +433,8 @@ The following changes are to be made to [§14.7.4](https://github.com/dotnet/csh } ``` +1. Should nullable flow analysis provide a warning for non-nullable properties when a setter allows `field` to be null and the getter returns something whose nullability depends on `field`? + ## LDM history: - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-03-10.md#field-keyword - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-14.md#field-keyword From 3573c8fff6f88bae69730d8acf26fcfe0b515e8f Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sat, 25 May 2024 14:51:27 -0400 Subject: [PATCH 15/28] Clarify wording --- proposals/semi-auto-properties.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index ac0af1bed2..26bd7154f6 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -111,13 +111,11 @@ Properties with initializers may use `field`. The backing field is directly init Calling a setter for an initializer is not an option; initializers are processed before calling base constructors, and it is illegal to call any instance method before the base constructor is called. This is also important for default initialization/definite assignment of structs. -This luckily gives control over whether or not you want to initialize the backing field directly or call the property setter: if you want to initialize without calling the setter, you use a property initializer. If you want to initialize by calling the setter, you use assign the property an initial value in the constructor. +This yields flexible control over initialization. If you want to initialize without calling the setter, you use a property initializer. If you want to initialize by calling the setter, you use assign the property an initial value in the constructor. -Here's an example of where this is useful. The `field` keyword will find a lot of its use with view models because of the neat solution it brings for the `INotifyPropertyChanged` pattern. View model property setters are likely to be databound to UI and likely to cause change tracking or trigger other behaviors. The following code needs to initialize the default value of `IsActive` without setting `HasPendingChanges` to `true`: +Here's an example of where this is useful. We believe the `field` keyword will find a lot of its use with view models because of the elegant solution it brings for the `INotifyPropertyChanged` pattern. View model property setters are likely to be databound to UI and likely to cause change tracking or trigger other behaviors. The following code needs to initialize the default value of `IsActive` without setting `HasPendingChanges` to `true`: ```cs -using System.Runtime.CompilerServices; - class SomeViewModel { public bool HasPendingChanges { get; private set; } @@ -126,7 +124,9 @@ class SomeViewModel private bool Set(ref T location, T value) { - if (RuntimeHelpers.Equals(location, value)) return false; + if (RuntimeHelpers.Equals(location, value)) + return false; + location = value; HasPendingChanges = true; return true; @@ -221,7 +221,7 @@ class C } ``` -In the same vein as how `var` infers as nullable for reference types, the `field` type should be nullable for reference types. This makes sense of `field ??` as not being followed by dead code, and it avoids producing a misleading warning in the following example: +In the same vein as how `var` infers as nullable for reference types, the `field` type is the nullable type of the property whenever the property's type is not a value type. Otherwise, `field ??` would appear to be followed by dead code, and it avoids producing a misleading warning in the following example: ```cs public string AmbientValue @@ -237,7 +237,7 @@ public string AmbientValue } ``` -Just like with nullability of `var`, it's expected that such patterns will exist with manually-declared backing fields. +`var` was designed to declare nullability so that subsequent assignments to the variable could be nullable, due to established patterns in C#. It's expected that the same rationale would apply to property backing fields. To land in this sweet spot implicitly, without having to write an attribute each time, nullability analysis will combine an inherent nullability of the field with the behavior of `[field: NotNull]`. This allows maybe-null assignments without warning, which is desirable as shown above, while simultaneously allowing a scenario like `=> field.Trim();` without requiring an intervention to silence a warning that `field` could be null. Making sure `field` has been assigned is already covered by the warning that ensures non-nullable properties are assigned by the end of each constructor. @@ -275,7 +275,7 @@ Like with auto properties, properties which use the `field` keyword and override ### Captures -`field` should be able to be captured in local functions and lambdas, and references to `field` from inside local functions and lambdas should be allowed even if there are no other references ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-21.md#open-question-in-semi-auto-properties)): +`field` should be able to be captured in local functions and lambdas, and references to `field` from inside local functions and lambdas are allowed even if there are no other references ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-21.md#open-question-in-semi-auto-properties)): ```cs public class C From 01f8cc855c1a816cc09852b810c76214b2dc5b45 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sat, 25 May 2024 15:26:39 -0400 Subject: [PATCH 16/28] Update spec for breaking changes decisions --- proposals/semi-auto-properties.md | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 52a5c493b7..3c03fd0b82 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -85,6 +85,12 @@ As with auto properties, a setter that uses a backing field is disallowed when t { set => field = value; } ``` +### Breaking changes + +The existence of the `field` contextual keyword within property accessor bodies is a potentially breaking change, proposed as part of a larger [Breaking Changes](https://github.com/dotnet/csharplang/issues/7964) feature. + +Since `field` is a keyword and not an identifier, it can only be "shadowed" by an identifier using the normal keyword-escaping route: `@field`. All identifiers named `field` declared within property accessor bodies can safeguard against breaks when upgrading from C# versions prior to 13 by adding the initial `@`. + ### Field-targeted attributes As with auto properties, any property that uses a backing field in one of its accessors will be able to use field-targeted attributes: @@ -188,7 +194,19 @@ class C A property which uses an automatic backing field will be treated as an auto property for the purposes of calculating default backing field initialization if its setter is automatically implemented, or if it does not have a setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#property-assignment-in-structs)). -Default-initialize a struct when calling a manually implemented setter of a property which uses an automatic backing field, and issue a warning when doing so, like a regular property setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#definite-assignment-of-manually-implemented-setters)). +Default-initialize a struct when calling a manually implemented setter of a property which uses an automatic backing field, and issue a warning when doing so, like a regular property setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#definite-assignment-of-manually-implemented-setters)). That enables the following code to compile without warning "CS9020: The 'this' object is read before all of its fields have been assigned, causing preceding implicit assignments of 'default' to non-explicitly assigned fields": + +```cs +public struct C +{ + public C() + { + P = 5; // No warning + } + + public int P { get => field; set => field = value; } +} +``` ### Nullability @@ -261,7 +279,7 @@ Open question: Should flow analysis combine the maybe-null end state for `field` ### `nameof` -In places where `field` is a keyword (see the [Shadowing](#shadowing) section), `nameof(field)` will fail to compile, like `nameof(nint)`. It is not like `nameof(value)`, which is the thing to use when property setters throw ArgumentException as some do in the .NET core libraries. In contrast, `nameof(field)` has no expected use cases. If it did anything, it would return the string `"field"`, consistent with how `nameof` behaves in other circumstances by returning the C# name or alias, rather than the metadata name. +In places where `field` is a keyword, `nameof(field)` will fail to compile ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-05-15.md#usage-in-nameof)), like `nameof(nint)`. It is not like `nameof(value)`, which is the thing to use when property setters throw ArgumentException as some do in the .NET core libraries. In contrast, `nameof(field)` has no expected use cases. ### Overrides @@ -269,13 +287,9 @@ Overriding properties may use `field`. Such usages of `field` refer to the backi Like with auto properties, properties which use the `field` keyword and override a base property must override all accessors ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#partial-overrides-of-virtual-properties)). -### Shadowing - -`field` can be shadowed by parameters or locals in a nested scope ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#open-questions-in-field)). Since `field` represents a field in the type, even if anonymously, the shadowing rules of regular fields should apply. - ### Captures -`field` should be able to be captured in local functions and lambdas, and references to `field` from inside local functions and lambdas are allowed even if there are no other references ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-21.md#open-question-in-semi-auto-properties)): +`field` should be able to be captured in local functions and lambdas, and references to `field` from inside local functions and lambdas are allowed even if there are no other references ([LDM decision 1](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-21.md#open-question-in-semi-auto-properties), [LDM decision 2](https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-05-15.md#should-field-and-value-be-considered-keywords-in-lambdas-and-local-functions-within-property-accessors)): ```cs public class C @@ -374,8 +388,6 @@ public class Point ## Open LDM questions -1. If a type does have an existing accessible `field` symbol in scope (like a field called `field`) should there be any way for a property to still use `field` internally to both create and refer to an automatically-implemented backing field. Under the current rules there is no way to do that. This is certainly unfortunate for those users, however this is ideally not a significant enough issue to warrant extra dispensation. The user, after all, can always still write out their properties like they do today, they just lose out from the convenience here in that small case. - 1. Which of these scenarios should be allowed to compile? Assume that the "field is never read" warning would apply just like with a manually declared field. 1. `{ set; }` - Disallowed today, continue disallowing From 825e4036f1d1f32b58464846938a4601327abd54 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 29 May 2024 16:17:09 -0400 Subject: [PATCH 17/28] Use simpler explanation of LDM decisions on definite assignment in structs --- proposals/semi-auto-properties.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 3c03fd0b82..cb6adcd11f 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -192,19 +192,19 @@ class C ### Definite assignment in structs -A property which uses an automatic backing field will be treated as an auto property for the purposes of calculating default backing field initialization if its setter is automatically implemented, or if it does not have a setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#property-assignment-in-structs)). - -Default-initialize a struct when calling a manually implemented setter of a property which uses an automatic backing field, and issue a warning when doing so, like a regular property setter ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#definite-assignment-of-manually-implemented-setters)). That enables the following code to compile without warning "CS9020: The 'this' object is read before all of its fields have been assigned, causing preceding implicit assignments of 'default' to non-explicitly assigned fields": +Even though they can't be referenced in the constructor, backing fields denoted by the `field` keyword are subject to default-initialization and disabled-by-default warnings under the same conditions as any other struct fields ([LDM decision 1](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#property-assignment-in-structs), [LDM decision 2](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#definite-assignment-of-manually-implemented-setters)). For example: ```cs -public struct C +public struct S { - public C() + public S() { - P = 5; // No warning + _ = P1; // Disabled-by-default warning + P2 = 5; // Disabled-by-default warning } - public int P { get => field; set => field = value; } + public int P1 { get => field; } + public int P2 { get => field; set => field = value; } } ``` From 0a3ac7530eb0ccd476c92cbb5015ddc4ceb9ab86 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Fri, 7 Jun 2024 11:36:34 -0400 Subject: [PATCH 18/28] Add examples to each glossary entry --- proposals/semi-auto-properties.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index cb6adcd11f..2f6da5f3b9 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -14,9 +14,9 @@ In these cases by now you always have to create an instance field and write the ## Glossary -- **Auto property**: Short for "automatically implemented property" ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). Accessors on an auto property have no body. The implementation and backing storage are both provided by the compiler. +- **Auto property**: Short for "automatically implemented property" ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). Accessors on an auto property have no body. The implementation and backing storage are both provided by the compiler. Auto properties have `{ get; }`, `{ get; set; }`, or `{ get; init; }`. -- **Auto accessor**: Short for "automatically implemented accessor." This is an accessor that has no body. The implementation and backing storage are both provided by the compiler. +- **Auto accessor**: Short for "automatically implemented accessor." This is an accessor that has no body. The implementation and backing storage are both provided by the compiler. `get;`, `set;` and `init;` are auto accessors. - **Full accessor**: This is an accessor that has a body. The implementation is not provided by the compiler, though the backing storage may still be (as in the example `set => field = value;`). From 87cd574e3f6e846e8a8e30919718f3feb03866fb Mon Sep 17 00:00:00 2001 From: jnm2 Date: Fri, 7 Jun 2024 11:38:53 -0400 Subject: [PATCH 19/28] Save principles for later --- proposals/semi-auto-properties.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 2f6da5f3b9..3fe8e104e8 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -24,15 +24,13 @@ In these cases by now you always have to create an instance field and write the For properties with an `init` accessor, everything that applies below to `set` would apply instead to the `init` accessor. -**Principle 1:** Every property declaration can be thought of as having a backing field by default, which is elided when not used. The field is referenced using the keyword `field` and its visibility is scoped to the accessor bodies. +There are two syntax changes: -**Principle 2:** `get;` will now be considered syntactic sugar for `get => field;`, and `set;` will now be considered syntactic sugar for `set => field = value;`. +1. There is a new contextual keyword, `field`, which may be used within property accessor bodies to access a backing field for the property declaration. -Both of these principles only apply under the same conditions where `{ get; }` or `{ get; set; }` already declares an auto property ([§15.7.4](https://github.com/dotnet/csharpstandard/blob/standard-v7/standard/classes.md#1574-automatically-implemented-properties)). For example, abstract and interface properties are excluded. Indexers also remain unaffected. +2. Properties may now mix and match auto accessors with full accessors. "Auto property" will continue to mean a property whose accessors have no bodies. None of the examples below will be considered auto properties. -"Auto property" will continue to mean a property whose accessors have no bodies. None of the examples below will be considered auto properties. - -This means that properties may now mix and match auto accessors with full accessors. For example: +Examples: ```cs { get; set => Set(ref field, value); } From c4d42bdf450f1fe49a853e658cb08311f704c71a Mon Sep 17 00:00:00 2001 From: jnm2 Date: Fri, 7 Jun 2024 11:41:58 -0400 Subject: [PATCH 20/28] Copy the nullability example to its open question --- proposals/semi-auto-properties.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 3fe8e104e8..b6522e8629 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -416,6 +416,25 @@ public class Point 1. Should nullable flow analysis provide a warning for non-nullable properties when a setter allows `field` to be null and the getter returns something whose nullability depends on `field`? + ```cs + public string AmbientValue + { + get => field; // No warning, but could return null! + set + { + if (value == parent.AmbientValue) + { + // A valid strategy if the getter returned field ?? parent.AmbientValue + field = null; + } + else + { + field = value; + } + } + } + ``` + ## LDM history: - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-03-10.md#field-keyword - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-14.md#field-keyword From e020a0c3bca44e52e85cbe1ce24e73913b5a38c6 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 12 Jun 2024 15:19:44 -0400 Subject: [PATCH 21/28] Make warning samples less confusing --- proposals/semi-auto-properties.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index b6522e8629..87da814f64 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -198,11 +198,21 @@ public struct S public S() { _ = P1; // Disabled-by-default warning + } + + public int P1 { get => field; } +} +``` + +```cs +public struct S +{ + public S() + { P2 = 5; // Disabled-by-default warning } public int P1 { get => field; } - public int P2 { get => field; set => field = value; } } ``` From a08339f548d95cd940a7d8aef9b27b7c328619ac Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 12 Jun 2024 15:26:07 -0400 Subject: [PATCH 22/28] Clarify that these diagnostics are not warnings by default --- proposals/semi-auto-properties.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 87da814f64..3e6cf3eaab 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -190,14 +190,18 @@ class C ### Definite assignment in structs -Even though they can't be referenced in the constructor, backing fields denoted by the `field` keyword are subject to default-initialization and disabled-by-default warnings under the same conditions as any other struct fields ([LDM decision 1](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#property-assignment-in-structs), [LDM decision 2](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#definite-assignment-of-manually-implemented-setters)). For example: +Even though they can't be referenced in the constructor, backing fields denoted by the `field` keyword are subject to default-initialization and disabled-by-default warnings under the same conditions as any other struct fields ([LDM decision 1](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-02.md#property-assignment-in-structs), [LDM decision 2](https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#definite-assignment-of-manually-implemented-setters)). + +For example (these diagnostics are silent by default): ```cs public struct S { public S() { - _ = P1; // Disabled-by-default warning + // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit + // assignments of 'default' to non-explicitly assigned fields. + _ = P1; } public int P1 { get => field; } @@ -209,7 +213,9 @@ public struct S { public S() { - P2 = 5; // Disabled-by-default warning + // CS9020 The 'this' object is read before all of its fields have been assigned, causing preceding implicit + // assignments of 'default' to non-explicitly assigned fields. + P2 = 5; } public int P1 { get => field; } From 6f707cc92526b921fbf8e3ccbed61d1e5ce7c4a1 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 12 Jun 2024 15:26:37 -0400 Subject: [PATCH 23/28] Grammar fix --- proposals/semi-auto-properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 3e6cf3eaab..16bdd8a877 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -8,7 +8,7 @@ Extend all properties to allow them to reference an automatically generated back Auto properties only allow for directly setting or getting the backing field, giving some control only by placing access modifiers on the accessors. Sometimes there is a need to have additional control over what happens in one or both accessors, but this confronts users with the overhead of declaring a backing field. The backing field name must then be kept in sync with the property, and the backing field is scoped to the entire class which can result in accidental bypassing of the accessors from within the class. -There are two common scenarios in particular: applying a constraint on the setter to ensuring the validity of a value, and raising an event such as `INotifyPropertyChanged.PropertyChanged`. +There are two common scenarios in particular: applying a constraint on the setter to ensure the validity of a value, and raising an event such as `INotifyPropertyChanged.PropertyChanged`. In these cases by now you always have to create an instance field and write the whole property yourself. This not only adds a fair amount of code, but it also leaks the backing field into the rest of the type's scope, when it is often desirable to only have it be available to the bodies of the accessors. From b2782ce064ffaa294883df82f7bdc9e1ff319390 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 12 Jun 2024 15:39:17 -0400 Subject: [PATCH 24/28] Add nullability and semi-auto as open questions --- proposals/semi-auto-properties.md | 37 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 16bdd8a877..c2c7be3f0d 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -253,6 +253,8 @@ class C } ``` +#### Open question: nullability of `field` + In the same vein as how `var` infers as nullable for reference types, the `field` type is the nullable type of the property whenever the property's type is not a value type. Otherwise, `field ??` would appear to be followed by dead code, and it avoids producing a misleading warning in the following example: ```cs @@ -402,6 +404,20 @@ public class Point ## Open LDM questions +1. Should it be disallowed to pair a manually implemented accessor with an automatically implemented accessor? An original guiding principle for the design was that, in places where `get; set;` defines an auto property, `get;` is now considered syntax sugar for `get => field;` and `set;` is now short for `set => field = value;`. This allowance was encoded in the original name for the feature, "semi-auto properties," where half the property would be automatically implemented and the other half would not be automatically implemented. + + The typical INotifyPropertyChanged use case would be: + + ```cs + public string Name { get; set => Set(ref field, value); } + ``` + + If this was disallowed, the typical use case would expand slightly to: + + ```cs + public string Name { get => field; set => Set(ref field, value); } + ``` + 1. Which of these scenarios should be allowed to compile? Assume that the "field is never read" warning would apply just like with a manually declared field. 1. `{ set; }` - Disallowed today, continue disallowing @@ -430,26 +446,7 @@ public class Point } ``` -1. Should nullable flow analysis provide a warning for non-nullable properties when a setter allows `field` to be null and the getter returns something whose nullability depends on `field`? - - ```cs - public string AmbientValue - { - get => field; // No warning, but could return null! - set - { - if (value == parent.AmbientValue) - { - // A valid strategy if the getter returned field ?? parent.AmbientValue - field = null; - } - else - { - field = value; - } - } - } - ``` +1. Should the proposed nullability of `field` be accepted? See the [Nullability](#nullability) section, and the open question within. ## LDM history: - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-03-10.md#field-keyword From 05d1836ef3d31fada201b29648b6599e93baaafb Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sat, 22 Jun 2024 18:53:06 -0400 Subject: [PATCH 25/28] Use headers instead of bullets for open questions --- proposals/semi-auto-properties.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index c2c7be3f0d..afd0627a7b 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -404,7 +404,9 @@ public class Point ## Open LDM questions -1. Should it be disallowed to pair a manually implemented accessor with an automatically implemented accessor? An original guiding principle for the design was that, in places where `get; set;` defines an auto property, `get;` is now considered syntax sugar for `get => field;` and `set;` is now short for `set => field = value;`. This allowance was encoded in the original name for the feature, "semi-auto properties," where half the property would be automatically implemented and the other half would not be automatically implemented. +### Mixing auto and full accessors + +Should it be disallowed to pair a manually implemented accessor with an automatically implemented accessor? An original guiding principle for the design was that, in places where `get; set;` defines an auto property, `get;` is now considered syntax sugar for `get => field;` and `set;` is now short for `set => field = value;`. This allowance was encoded in the original name for the feature, "semi-auto properties," where half the property would be automatically implemented and the other half would not be automatically implemented. The typical INotifyPropertyChanged use case would be: @@ -418,7 +420,11 @@ public class Point public string Name { get => field; set => Set(ref field, value); } ``` -1. Which of these scenarios should be allowed to compile? Assume that the "field is never read" warning would apply just like with a manually declared field. +### Scenarios similar to `{ set; }` + +`{ set; }` is currently disallowed and this makes sense: the field which this creates can never be read. There are now new ways to end up in a situation where the setter introduces a backing field that is never read, such as the expansion of `{ set; }` into `{ set => field = value; }`. + +Which of these scenarios should be allowed to compile? Assume that the "field is never read" warning would apply just like with a manually declared field. 1. `{ set; }` - Disallowed today, continue disallowing 1. `{ set => field = value; }` @@ -446,7 +452,9 @@ public class Point } ``` -1. Should the proposed nullability of `field` be accepted? See the [Nullability](#nullability) section, and the open question within. +### Nullability of `field` + +Should the proposed nullability of `field` be accepted? See the [Nullability](#nullability) section, and the open question within. ## LDM history: - https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-03-10.md#field-keyword From 9edd99debc145058a640bb29e999c929d2d6d1a7 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 3 Jul 2024 16:27:36 -0400 Subject: [PATCH 26/28] Open question was resolved in LDM --- proposals/semi-auto-properties.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 43b3b6ab0c..9b2db5eda9 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -28,7 +28,7 @@ There are two syntax changes: 1. There is a new contextual keyword, `field`, which may be used within property accessor bodies to access a backing field for the property declaration. -2. Properties may now mix and match auto accessors with full accessors. "Auto property" will continue to mean a property whose accessors have no bodies. None of the examples below will be considered auto properties. +2. Properties may now mix and match auto accessors with full accessors ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-24.md#mixing-auto-accessors)). "Auto property" will continue to mean a property whose accessors have no bodies. None of the examples below will be considered auto properties. Examples: @@ -404,22 +404,6 @@ public class Point ## Open LDM questions -### Mixing auto and full accessors - -Should it be disallowed to pair a manually implemented accessor with an automatically implemented accessor? An original guiding principle for the design was that, in places where `get; set;` defines an auto property, `get;` is now considered syntax sugar for `get => field;` and `set;` is now short for `set => field = value;`. This allowance was encoded in the original name for the feature, "semi-auto properties," where half the property would be automatically implemented and the other half would not be automatically implemented. - - The typical INotifyPropertyChanged use case would be: - - ```cs - public string Name { get; set => Set(ref field, value); } - ``` - - If this was disallowed, the typical use case would expand slightly to: - - ```cs - public string Name { get => field; set => Set(ref field, value); } - ``` - ### Scenarios similar to `{ set; }` `{ set; }` is currently disallowed and this makes sense: the field which this creates can never be read. There are now new ways to end up in a situation where the setter introduces a backing field that is never read, such as the expansion of `{ set; }` into `{ set => field = value; }`. From 416971f4383b4ae53586f37f35af991912f5914e Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 3 Jul 2024 16:36:55 -0400 Subject: [PATCH 27/28] Add LDM decision about keywordness of `field` --- proposals/semi-auto-properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 9b2db5eda9..923c186d77 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -26,7 +26,7 @@ For properties with an `init` accessor, everything that applies below to `set` w There are two syntax changes: -1. There is a new contextual keyword, `field`, which may be used within property accessor bodies to access a backing field for the property declaration. +1. There is a new contextual keyword, `field`, which may be used within property accessor bodies to access a backing field for the property declaration ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-05-15.md#should-field-and-value-be-keywords-in-property-or-accessor-signatures-what-about-nameof-in-those-spaces)). 2. Properties may now mix and match auto accessors with full accessors ([LDM decision](https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-24.md#mixing-auto-accessors)). "Auto property" will continue to mean a property whose accessors have no bodies. None of the examples below will be considered auto properties. From 5263bc42c5df1e49fb3307a031e3d376bb9c0524 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Wed, 3 Jul 2024 17:04:32 -0400 Subject: [PATCH 28/28] Responding to review --- proposals/semi-auto-properties.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/semi-auto-properties.md b/proposals/semi-auto-properties.md index 923c186d77..f0313a8b9a 100644 --- a/proposals/semi-auto-properties.md +++ b/proposals/semi-auto-properties.md @@ -8,7 +8,7 @@ Extend all properties to allow them to reference an automatically generated back Auto properties only allow for directly setting or getting the backing field, giving some control only by placing access modifiers on the accessors. Sometimes there is a need to have additional control over what happens in one or both accessors, but this confronts users with the overhead of declaring a backing field. The backing field name must then be kept in sync with the property, and the backing field is scoped to the entire class which can result in accidental bypassing of the accessors from within the class. -There are two common scenarios in particular: applying a constraint on the setter to ensure the validity of a value, and raising an event such as `INotifyPropertyChanged.PropertyChanged`. +There are several common scenarios. Within the getter, there is lazy initialization, or default values when the property has never been given. Within the setter, there is applying a constraint to ensure the validity of a value, or detecting and propagating updates such as by raising the `INotifyPropertyChanged.PropertyChanged` event. In these cases by now you always have to create an instance field and write the whole property yourself. This not only adds a fair amount of code, but it also leaks the backing field into the rest of the type's scope, when it is often desirable to only have it be available to the bodies of the accessors. @@ -168,7 +168,7 @@ class Derived : Base ### Constructor assignment -As with auto properties, assignment in the constructor calls the setter if it exists, and if there is no setter it falls back to directly assigning to the backing field. +As with auto properties, assignment in the constructor calls the (potentially virtual) setter if it exists, and if there is no setter it falls back to directly assigning to the backing field. ```cs class C