Open migueldeicaza opened 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.
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
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
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 ;-))
This looks a lot like source generators under a different name, and also less useful since it would only be applicable to properties.
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; }
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.
Wow @MHDante - it looks like Kotlin pioneered this idea before Swift got it. Yes, this is very similar.
@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.
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
:With property wrappers, the boilerplate for raising the event could be replaced by applying the attribute [PropertyChanged]:
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:into this:
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:
[UserDefault ("config.hostname")] public string Hostname;
[Atomic] public int Counter;
[Clamp(0,100)] public int Percentage { get; set; }
[Clamp (0,255)] public int Red { get; set; }
The community has built a few more:
[Logging] public string Url
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:
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 constructnameof(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:
Composing Property Wrappers
Property wrappers can be composed, for example:
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:
Would transform to something like this:
Note the additional
nameof()
parameter passed to the struct; this is needed in case the struct usesnameof(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 fornameof(this)
references.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/