csharplang/proposals/semi-auto-properties.md

11 KiB

Semi-auto-properties (a.k.a. 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.

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.

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.

Specification changes

The following changes are to be made to §14.7.4:

### Automatically implemented properties

...

- A *property_initializer* may only be given for an automatically implemented property ([Automatically implemented properties](classes.md#automatically-implemented-properties)), and causes the initialization of the underlying field of such properties with the value given by the *expression*.
+ A *property_initializer* may only be given for a property that has a backing field that will be emitted and the property either does not have a setter, or its setter is auto-implemented. The *property_initializer* causes the initialization of the underlying field of such properties with the value given by the *expression*.

...

- An automatically implemented property (or ***auto-property*** for short), is a non-abstract non-extern
- property with semicolon-only accessor bodies. Auto-properties must have a get accessor and can optionally
- have a set accessor.
+ An automatically implemented property (or ***auto-property*** for short), is a non-abstract non-extern
+ property with either or both of:
- When a property is specified as an automatically implemented property, a hidden backing field is automatically
- available for the property, and the accessors are implemented to read from and write to that backing field. If
- the auto-property has no set accessor, the backing field is considered `readonly` ([Readonly fields](classes.md#readonly-fields)).
- Just like a `readonly` field, a getter-only auto-property can also be assigned to in the body of a constructor 
- of the enclosing class. Such an assignment assigns directly to the readonly backing field of the property.
+ 1. an accessor with a semicolon-only body
+ 2. usage of the `field` contextual keyword ([Keywords](lexical-structure.md#keywords)) within the accessors or
+    expression body of the property. The `field` identifier is only considered the `field` keyword when there is
+    no existing symbol named `field` in scope at that location.
+
+ When a property is specified as an auto-property, a hidden, unnamed, backing field is automatically available for
+ the property. For auto-properties, any semicolon-only `get` accessor is implemented to read from, and any semicolon-only
+ `set` accessor to write to its backing field. The backing field can be referenced directly using the `field` keyword
+ within all accessors and within the property expression body. Because the field is unnamed, it cannot be used in a
+ `nameof` expression.
+
+ If the auto-property does not have a set accessor, the backing field can still be assigned to in the body of a 
+ constructor of the enclosing class. Such an assignment assigns directly to the backing field of the property.
+
+ If the auto-property has only a semicolon-only get accessor, the backing field is considered `readonly` ([Readonly fields](classes.md#readonly-fields)).
+
+ An auto-property is not allowed to only have a single semicolon-only `set` accessor without a `get` accessor.

...

- If the auto-property has no set accessor, the backing field is considered `readonly` ([Readonly fields](classes.md#readonly-fields)). Just like a `readonly` field, a getter-only auto-property can also be assigned to in the body of a constructor of the enclosing class. Such an assignment assigns directly to the readonly backing field of the property.
+ If the auto-property has semicolon-only get accessor (without a set accessor or with an init accessor), the backing field is considered `readonly` ([Readonly fields](classes.md#readonly-fields)). Just like a `readonly` field, a getter-only auto property (without a set accessor or an init accessor) can also be assigned to in the body of a constructor of the enclosing class. Such an assignment assigns directly to the backing field of the property.

...

+The following example:
+```csharp
+// No 'field' symbol in scope.
+public class Point
+{
+    public int X { get; set; }
+    public int Y { get; set; }
+}
+```
+is equivalent to the following declaration:
+```csharp
+// No 'field' symbol in scope.
+public class Point
+{
+    public int X { get { return field; } set { field = value; } }
+    public int Y { get { return field; } set { field = value; } }
+}
+```
+which is equivalent to:
+```csharp
+// No 'field' symbol in scope.
+public class Point
+{
+    private int __x;
+    private int __y;
+    public int X { get { return __x; } set { __x = value; } }
+    public int Y { get { return __y; } set { __y = value; } }
+}
+```

+The following example:
+```csharp
+// No 'field' symbol in scope.
+public class LazyInit
+{
+    public string Value => field ??= ComputeValue();
+    private static string ComputeValue() { /*...*/ }
+}
+```
+is equivalent to the following declaration:
+```csharp
+// No 'field' symbol in scope.
+public class Point
+{
+    private string __value;
+    public string Value { get { return __value ??= ComputeValue(); } }
+    private static string ComputeValue() { /*...*/ }
+}
+```

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:

      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.

      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<T>(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:

LDM history: