This project is read-only.

Implementation of auto properties with traits

Topics: C# Language Design
Oct 30, 2014 at 11:28 PM
Edited Oct 30, 2014 at 11:30 PM
Dear all, this is a spin-off thread of this discussion. I think defining different kinds of properties can be done in a relatively generic way using a kind of traits described below. Moreover, traits can be used in a number of other ways.

So I'd like to propose a sketch for traits in C#. The sketch covers just a surface -- the syntax ans its meaning; I hope that an efficient implementation is possible, and that the feature fits into the language's style and design.

Let's define trait within this discussion as a separately compilable part of class definition with replaceable names. The syntax will resemble the class syntax, but unfortunately some changes seem to be needed.

I tried to define the trait's meaning to be not purely syntactical, so that trait can be compiled or at least "half-compiled", but is still powerful enough to work for common use cases.

I am liberally inventing new syntax where it seems to be needed, while trying to keep the general C#'s syntactic style. So, a declaration may look like this:
trait CountInstances
{
    static int totalInstances = 0;
    int instanceNo = totalInstances++;
    public int InstanceNo { get { return instanceNo; } }
}
and one can reuse this code fragment without copy-and-paste:
class C
{
    use CountInstances; // "merges" the code above into the class
    // rest of the class
}
Let's see how one can define different types of auto properties with traits. We can add simple property with the following declaration
trait VanillaProperty<C, T> // C is class type, T is target type
{
    T BackingField;
    init(T value) { BackingField = value; }
    init() { }
    public T @ // @ is a placeholder for trait instance's name
    {
        get { return BackingField; }
        set { BackingField = value; }
    }
}

class C1
{
    use int#VanillaProperty Prop1 = init(5); // int is trait's type,
                                             // Prop1 is trait's name
    use int#VanillaProperty Prop2; // this gets init without arguments
}
this should be expanded by the compiler to the following:
class C1
{
    public C1()
    {
        <Prop1>k__BackingField = 5; // init's code is implicitly added
                                    // to the constructor
    }

    private int <Prop1>k__BackingField; // trait's private fields are hidden
    // the name is generated the same way as autoproperty's backing field

    int Prop1 // trait's public properties are visible,
    {         //  @ is replaced with the name
        get { return <Prop1>k__BackingField; }
        set { <Prop1>k__BackingField = value; }
    }

    // the same expansion for Prop2
}
More complicated lazy-computed properties could be expressed like this:
trait LazyProperty<C, T>
{
    init(Func<T> creator) { Creator = creator; }
    T BackingField;
    bool Created;
    Func<T> Creator;
    public T @
    {
        get { if (!Created) BackingField = Creator(); return BackingField; }
        set { Created = true; BackingField = value; }
    }
}

class C2
{
    double seed;
    public C2(double seed) { this.seed = seed; }
    using double#LazyProperty Result = init(() => SlowCalculation(seed));
}
which would get expanded into
class C2
{
    double seed;
    public C2(double seed)
    {
        <Result>k__Creator = () => SlowCalculation(seed);
        this.seed = seed;
    }
    
    double <Result>k__BackingField;
    bool <Result>k__Created;
    Func<double> <Result>k__Creator;
    public double Result
    {
        get
        {
            if (!<Result>k__Created)
                <Result>k__BackingField = <Result>k__Creator();
            return <Result>k__BackingField;
        }
        set { <Result>k__Created = true; <Result>k__BackingField = value; }
    }
}
Some more examples:

WPF's dependency property
trait DependencyProperty<C, T>
    where C : DependencyObject
{
    // static below means that the expression needs static context:
    // no instance-specific expressions are allowed
    // the assignment will be merged into the C's static constructor
    init (static PropertyMetadata metadata)
    {
        // static and non-static expressions cannot mix.
        // is there a prettier way?
        _metadata = metadata;
    }
    static PropertyMetadata _metadata;
    public T @
    {
        get { return (T)GetValue(@Property); }
        // this is guaranteed to compile, as C is a DependencyObject!
        set { SetValue(@Property, value); }
    }
    // @Property is pasted together to form a new name
    public static readonly DependencyProperty @Property =
        DependencyProperty.Register(nameof(@), typeof(T), typeof(C),
                                    _metadata);
}

class C3 : DependencyObject
{
    use int#DependencyProperty P = init(new PropertyMetadata(42));
}
INotifyPropertyChanged boilerplate:
class ViewModelBase
{
    // this can actually be a trait, too
    public void NotifyPropertyChanged<T>(string name, T oldValue, T newValue)
    {
        // ...
    }
}

trait INPCProperty<C, T> : VanillaProperty<C, T>
    where C : ViewModelBase
{
    public T @
    {
        override set
        {
            if (!EqualityComparer<T>.Default.Equals(value, BackingField))
            {
                var oldValue = BackingField;
                BackingField = value;
                NotifyPropertyChanged(nameof(@), // hopefully this can work
                                      oldValue, value);
            }
        }
    }
}
The traits as described here, if implemented in the language, would allow to reduce the amount of boilerplate code in many cases. So I'd like to discuss whether this proposal is realistic and whether its expressiveness is good enough.

(Well, I don't see the way for auto-creating operations like GetHashCode/Equals/etc., which involves introspection over the all properties in the class, so any suggestions/ideas for improvement are welcome.)