dotnet / csharplang

The official repo for the design of the C# programming language
11.44k stars 1.02k forks source link

Proposal: Property Wrappers #2657

Open migueldeicaza opened 5 years ago

migueldeicaza commented 5 years ago

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/

HaloFour commented 5 years ago

All of this sounds like the domain of source generators. I'm hoping that after C# 8.0 releases that the team will have some bandwidth to explore that again and maybe look into ways of limiting it so that they can ship something usable.

tannergooding commented 5 years ago

I'm hoping that after C# 8.0 releases that the team will have some bandwidth to explore that again and maybe look into ways of limiting it so that they can ship something usable.

Luckily, @jaredpar tweeted that this is something they'll likely be looking into: https://twitter.com/jaredpar/status/1146903992963702784?s=20

benaadams commented 5 years ago

Was wondering what a less general solution would be; probably like?

public class Home : INotifyPropertyChanged 
{
    [NotifyPropertyChanged]
    public string Address { get; set; }
}

And it would generate the PropertyChangedEventHandler AddressChanged and PropertyChangedEventArgs (nameof(Address)) using the property name.

Perhaps with an overridable name

public class Home : INotifyPropertyChanged 
{
    [NotifyPropertyChanged("Address")]
    public string Address1 { get; set; }

    [NotifyPropertyChanged("Address")]
    public string Address2 { get; set; }
}

The proposal is obviously more flexible

msedi commented 5 years ago

I would really prefer plugins to hook into the parsing and code generation process so that I can control things myself. In the end some sort of a better preprocessor (don't get me wrong, I don't want the C++ macros back ;-))

MgSam commented 5 years ago

This looks a lot like source generators under a different name, and also less useful since it would only be applicable to properties.

quinmars commented 5 years ago

Personally, I prefer to tackle INotifyPropertyChanged with #140. If I write

    [NotifyPropertyChanged]
    public string Address { get; set; }

or

    public string Address { get; set => SetProperty(ref field, value); }

there is not much difference from a user perspective, i.e., no type or name repetition. The latter, however, is easier to understand and does not need any performance optimization of the CLR nor the roslyn compiler.

BTW, I don't know if you omitted it accidently or intentionally, but I hope

[Clamp (0,255)] public int Red;

should read as

[Clamp (0,255)] public int Red { get; set; }
MHDante commented 5 years ago

I've been tracking this issue (https://github.com/dotnet/csharplang/issues/1096) for a while, and would like this to be brought to the design team's attention.

Delegated properties are a great way of hiding accidental complexity in otherwise straight forward systems.

migueldeicaza commented 5 years ago

Wow @MHDante - it looks like Kotlin pioneered this idea before Swift got it. Yes, this is very similar.

b-straub commented 4 years ago

@migueldeicaza

I think SG could give this proposal a push, see my issue and sample here Extend partial to properties

With MAUI on the horizon I feel reactive MVU can be way simpler than using RxUI. That's why I started a first attempt using the new Roslyn SG feature to prepare reactive support in a SwiftUI inspired way.