A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://github.com/dotnet/csharplang/issues/2657 below:

Property Wrappers · Issue #2657 · dotnet/csharplang · GitHub

Property Wrappers for C#

This proposal solves a long-time request of C# users to reduce the amount of repetitive code, and boilerplate code when implementing properties. .NET has a history of special casing a few attributes in properties and give them a special meaning ([ThreadStatic], [Weak]). While it is possible to do this for a handful of capabilities, it would be much easier to empower our users to do this by allowing users to define their own patterns for using properties. This proposal is inspired by Swift Property Wrappers pitch (specification) to the Swift Evolution mailing list, but applied to C#. Kotlin also has a similar capability, delegated properties.

This proposal borrows heavily from the Swift proposal by Doug Gregor and Joe Groff.

Motivation

Various application models in .NET surface properties and mandate a specific set of operations to be implemented for them. This has been a pain point in the community for things like implementing INotifyPropertyChanged where developers need to implement properties with repetitive code. Sweeping changes to property patterns are often cumbersome and error prone as well.

Our users have resorted over the years to generators [1] and post-processors [2], both can interact in undesirable ways with the build system, complicate builds, and can make builds briddle. There have been various proposals on using Roslyn hooks to produce the code.

This is a common pattern to implement the INotifyPropertyChanged:

public class Home : INotifyPropertyChanged {
    string _address;
    public string Address { 
        get => _address;
        set {
            if (value != _address) {
                 _address = value;
                 OnAddressPropertyChanged ();
            }
        }
    }
    public event PropertyChangedEventHandler AddressChanged;
    protected void OnAddressChanged ()
    {
        AddressChanged?.Invoke (this, new PropertyChangedEventArgs ("Address"));
    }
}

With property wrappers, the boilerplate for raising the event could be replaced by applying the attribute [PropertyChanged]:

public class Home : INotifyPropertyChanged {
    [PropertyChanged (OnAddressChanged)]
    public string Address;

    public event PropertyChangedEventHandler AddressChanged;
    protected void OnAddressChanged ()
    {
        AddressChanged?.Invoke (this, new PropertyChangedEventArgs ("Address"));
    }
}

In Xamarin, we introduced the special [Weak] runtime attribute to assist with strong cycles when working with the Objective-C runtime in Apple platforms. It removes boilerplate and turns When applied to a field, it turns it into a WeakReference:

class demo {
    [Weak] MyType myInstance;
    public demo (MyType reference)
    {
        myInstance = reference;
    }

    void func ()
    {
        if (myInstance != null)
            myInstance.Property = 1
    }
}

into this:

class demo {
    WeakReference<MyType> myInstance;
    public demo (MyType reference)
    {
        myInstance = new WeakReference<MyType> (reference)
    }

    void func ()
    {
        if (myInstance != null)
            if (myInstance.TryGetTarget (out var x))
                x.Property = 1;
    }
}

With property wrappers, we could remove this capability from the runtime and rely on the compiler for it (also, saves us work to do in .NET 5 to support this).

Apple’s SwiftUI uses these property wrappers to simplify data binding (@State, @EnvironmentObject, @ObjectBinding). Apple’s proposal also includes a few examples for property wrappers:

The community has built a few more:

The Swift developer community has found various uses for dependency properties and in the .NET world, external tools like Fody have been used for patching assemblies as a post-processing step to achieve some of these effects.

Proposed Solution

This proposal introduces a new feature called property wrappers, which allow a property declaration to state which wrapper is used to implement it. The wrapper itself is a type declaration that has been annotated with the PropertyWrapper attribute, allowing the type itself to be used as an attribute on a property.

This type contains a blueprint for expanding the property getter and setter, which relies on the specially named property WrappedValue as the template to use when expanding the getter and setter. Helper methods and any helper fields are available when the property itself is inlined.

Because this is a template that is expanded at compile time, the compiler needs to be extended to read the IL definition from an assembly to be able to perform the expansion in the place where it is used (as an optimization, non-public property wrappers can only exist in memory, during the compilation process).

An example:

[PropertyWrapper]
public struct RegistryValue<T> {
   string keyStorage;
   string defaultValue;
   public RegistryValue(string key, string defaultValue)
   {
       keyStorage = "HKEY_CURRENT_USER\\" + key;
       this.defaultValue = defaultValue;
   }
   public T WrappedValue {
       get {
          Registry.GetValue (keyStorage, "", defaultValue);
       }
       set {
          Debug.Log ($"Setting the value for {keyValue} to {value} in property {nameof(this)}");
          Registry.SetValue (keyStorage, value);
       }
   }
}

class PaintApp {
    [RegistryValue ("PaintApp\DefaultColor", "Red")]
    public string DefaultColor { get; set; }
}

The above structure would be compiled down to IL, and could be referenced with the custom attribute syntax when applied to a property.

Open to implementation discussion is whether to inline the storage into the caller with some kind of unique naming, or if we should just use a field of type RegistryValue<T>

The body of the property would be replaced with the implementation of WrappedValue. The special construct nameof(this) provides access to the name of the property that triggered this property wrapper.

Open Topics Referencing Other Fields

Unlike attributes, it would be desirable for property wrappers to reference fields in the class, but this would require that those are initialized before they are used, or for the property itself to not be available until after the object has been constructed.

One option would be to prevent any wrapper property from being accessed from a constructor until all fields in the object have been initialized:

The scenario would be something like this:

class MyOrder {
  string restEndpoint;
  [Query(restEndpoint, "/customerName?v=d")] public string CustomerName { get; set; }

  public MyOrder (string restEndpoint)
  {
    CustomerName = "test"; // ERROR: // Accessing before initializing restEndPoint
    this.restEndpoint = restEndpoint;
    CustomerName = "test"; // Ok, all fields of the class have been initialized before accessing the wrapper property.
  }
}
Composing Property Wrappers

Property wrappers can be composed, for example:

class SampleView {
  [Weak, LogAccess] UIView parentView;
}
Implementation Strategies Struct Backing Field

The compiler could transform the property by adding an field of the wrapper struct and generating the property getter/setter as trivial calls to the wrapper struct’s WrappedValue.

For example, this:

[RegistryValue ("PaintApp\DefaultColor", "Red")]
public string DefaultColor { get; set; }

Would transform to something like this:

RegistryValue<string> __<>k_BackingField
  = new RegistryValue<string> (nameof(DefaultColor), "PaintApp\DefaultColor", "Red")

public string DefaultColor {
  get =>__<>k_BackingField.WrappedValue;
  set =>__<>k_BackingField.WrappedValue = value;
}

Note the additional nameof() parameter passed to the struct; this is needed in case the struct uses nameof(this) to refer to the property name. The compiler would implicitly add this additional constructor argument into any struct annotated by [``PropertyWrapper``], assign it to a field in the struct, and use that field for nameof(this) references.

public RegistryValue(string propertyName, string key, string defaultValue)
{
  this.__<>k_propertyName = propertyName;
...

This implementation would be very straightforward for the C# compiler, and the runtime would be responsible for optimization.

The runtime would be expected to optimize this by inlining the backing struct’s getter and setter into the class property’s trivial getter and setter. However, it could also potentially inline the entire struct into the class, including the constructor and fields. In this RegistryValue example, constant folding could then completely eliminate the inlined fields.

IL Payload

An alternative approach would be for the optimization to be the job of the C# compiler. This would mean that reference assemblies would need to contain the struct implementation so that roslyn could inline the getter/setter into properties annotated with this attribute.

It would be perhaps the first time that Roslyn would need extract IL from a compiled assembly to extract the contents of the implementation to be inlined.

The property wrapper definition only needs to be serialized into the ECMA metadata image if it is public. Internal and private would be fully eliminated, and only used for the sake of compiling the internal version of it.

[1] https://github.com/neuecc/NotifyPropertyChangedGenerator
[2] Fody https://github.com/Fody/PropertyChanged and PostSharp https://www.postsharp.net/

jamesmontemagno, slang25, AlexanderButs, MarioGruda, benaadams and 28 morexparadoxical


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4