This project is read-only.

Init autoproperties - a new take on readonly and primary constructors

Topics: C# Language Design
Apr 30, 2014 at 9:31 PM
Edited May 1, 2014 at 7:37 PM
So much of the new readonly-assignable autoproperties seem to hinge on primary constructors, that of creating a new scope from the outside from which values can appear to be assigned near the property.

I have two different, possibly compatible suggestions.

The first is to allow straight-up assignment to a getter-only autoproperty in any place where assigning to the underlying readonly field would be legal - i.e. aside from in class scope, also inside the constructor. I assume the reason this is not allowed is that it seems like action at a distance - something becomes valid if code elsewhere is added.

NOTE: After typing up all this, I just saw that this was already decided in part II of the Apr 21 minutes.
So yay!

The second is a little more involved.

Let's first agree that autoproperties are used to shorten common patterns. Their incapability to do something is a nuisance, not a hindrance to go make that something happen by writing out the code.

I propose the addition of one keyword to serve as an accessor for autoproperties and a part of a constructor signature. That keyword is init. (Or it may be something else, but let's say init for now.) Let me explain by showing a series of gradually expanding definition:
class Point {
    public int X { init; }
    public int Y { init; }
}
The init accessor is only valid on autoproperties and it must be the only accessor. It means "a get of the same visibility as the property and a readonly backing field, to be filled in by the constructor". As an attribute and XML documentation target, it is equivalent to get, to which it codegens. Expand further...
class Point {

    public Point(init) { // identical to default conjured constructor
    }

    public int X { init; }
    public int Y { init; }
}
As implied by the above, there's a constructor with init in the parameter list. (If no other constructor is provided, a constructor is conjured since those fields will need to be set somehow.)

init in the parameter list means "in order of property declaration, one parameter each for all init autoproperties. In the face of partial and maybe of code "cleaning" going far enough to reorder members, this may be a little too brittle for some people's tastes, so you can also parametrize init with the names of the properties to specify where they go:
class Point {

    public Point(init(X, Y)) {
    }

    public int X { init; }
    public int Y { init; }
}
// the same as
class Point {

    public Point(/* extra parameters could go here */ init(X), /* or here */ init(Y) /* or here */) {
    }

    public int X { init; }
    public int Y { init; }
}
The init parcels are expanded in place and, apart from eschewing the types, is analogous to a sub-parameter list. This allows, but does not require, other expected features to be integrated naturally, like default values, which could show up as init(X = 42), attribute targets or special assignment types (init(int name) for an int? property where an implicit conversion exists.

Eventually, it will be flattened/lowered to roughly the below:
[CompilerGenerated]
class Point {

    private readonly int _<unmentionable>x;
    private readonly int _<unmentionable>y;

    public Point(int x, int y) {
        _<unmentionable>x = x;
        _<unmentionable>y = y;
    }

    public int X { get { return _<unmentionable>x; } }
    public int Y { get { return _<unmentionable>y; } }
}
As you can see, and as is the case with autoproperties, async methods, iterators and anonymous types, there are no lasting imprints in the IL. There are no questions about derived classes or surrounding code having to handle the init lists in the constructors differently than the eventually generated code.

Another idea, for API hygiene, is to automatically first-lowercase the name of the property when used for a parameter name and to override that with init(X(x)) or init(X:x).

A final idea is to issue warnings for uninited autoproperties in these cases. That would mean either that structs can't contain init autoproperties since you can create them with .Empty or the parameterless constructor, or that they would behave differently, both of which would probably be sufficiently surprising as to be a trap. However, if that would make it in, you could also use an incantation in the parameter list to say yes, really, I'm ignoring these - or just assigning default(T) in the constructor body, but where's the fun in that?
May 1, 2014 at 6:46 PM
I think if you have a simple immutable class it should be much simpler and cleaner yet more powerful.
For instance something like in Nemerle language:
[Record]
// One can specify include pattern using regular expression
// [Record(Include = "I|J")]
// Or specify an exclude
// [Record(Exclude = "K.*")]
// Or specify if add null check, perhaps this should be a default.
// [Record(NotNull = ".*")]
class A
{
  public I : int { get; }
  public J : string { get; }

  // Not in constructor
  [RecordIgnore] public K : double { get; set; }
}
This creates a constructor with readonly fields.
You can add a new constructor of your own of course.

In C# I'd like to see something similar without requiring to write a primary constructor.
Maybe like this with a new context keyword , or 'class A(init)' as you wrote.
Also I think it is more likely that you want all properties to be included and just write an exclude.
data class A
// class A(init)
{
  public int I { get; }
  public string J { get; }

  public double K { get; set; noinit; }
}
May 1, 2014 at 7:21 PM
Edited May 1, 2014 at 7:31 PM
I considered "record classes", going so far as to actually call them that too, and that would have had "record" instead of "class" and no property block for the properties. That'd make it all-or-nothing, which I would find limiting. (Looking like fields would likely also be very confusing.) One of the great things about autoproperties is that they're not special - you can use them anywhere you can use an ordinary property, it's just the compiler taking over the nitty gritty of the property implementation for you. Falling off a cliff and having to re-do a class as some other form of class would be annoying. I swear loudly every time I have to solidify an anonymous type into a class, especially when I'm missing Resharper.

Also, I've worked with enough environments where 'record' means more or less particularly optimized dumb storage for a few key-value pairs where you can get and set them until the cows come home, which this would not be, so that's why I left it out of my proposal.

Regarding exclusion: I see it as two cases.

Case one: adding an init autoproperty to a class where you already have properties. It'll be invasive enough that you have to add stuff to the constructor to make it work (which you also have to do with the manual readonly route), having to flip all of the other autoproperties sounds a bit too invasive. You rarely have to "opt out of your surroundings" in C#, and in the places you do, you usually just say what you want to happen instead of what you don't want to happen (like checked/unchecked blocks).

Case two: new classes. No worries, just init all of the autoproperties and don't worry about the constructor. Or worry about the constructor if you have other non-init autoproperties, but you'd only need to deal with init once in the constructor parameter list instead of all the autoproperties. I didn't mention this, but if you didn't write anything, the constructor that would be conjured would be A(init) like you said and that would by itself leave out K in your example.

So your example cast in my proposal:
class A {
    public int I { init; }
    public string J { init; }

    public double K { get; set; }
}
...is shorter than your example. I think init is obvious enough to point out that they will be inited with the constructor and that K won't, but I made all of this up in the first place, so what do I know. Shorter itself doesn't mean anything, but it means less new things to figure out.
May 1, 2014 at 7:52 PM
You don't have to call it record :)
I talk more about flexibility.
For instance, how do you specify if you want or don't want nulls ?
Using macro-attribute (compile time pseudo-attribute) it allows you to add any feature without breaking the code.

You propose to add a special syntax for initialization, and then maybe special syntax for null checks, and so on.
I see check for null as a default behaviour but if you won't want it, you must write everything manually , or vice versa if the default is not null check.
May 1, 2014 at 8:06 PM
Adding macros is adding "special syntax" to be able to add more "special syntax". That said, I'd like macros too, and if I had macros and not this, I'd want to use it. If you want to spec out macros, I'm happy to critique that design.

I don't say anything about null checking. I'm proposing a new property accessor meaning "get only and a readonly backing field, so don't allow set". I'm also proposing a way to position those fields in constructors. You're right that this wouldn't magically solve or induce validation, but it wouldn't make validation impossible, you could just say if (X == null) throw new ArgumentNullException("X") in the constructor as usual.
May 1, 2014 at 11:48 PM
Note that the latest change to getter-only auto-properties removes their special connection to primary constructor. They can now also be initialized from a normal constructor. See design notes here.
May 1, 2014 at 11:57 PM
Yeah, I saw that. The right call, as far as I'm concerned, so thanks for making it.
May 2, 2014 at 5:03 AM
I don't talk about adding macros.
Actually you propose to add a new syntax too :)

IMO adding null check manually or anything else just kills the simplicity.
In that case just write your own constructor and don't rely on auto-generated one.
That way, you don't need 'init' keyword at all.
May 2, 2014 at 8:52 PM
Yes, I proposed new syntax since nearly any new language feature will require it.

Yes, it's a bit more involved than autoproperties, which were just { get; set; }, but it's to solve a more involved scenario. There's no way of having readonly properties in any shape without piping it through a constructor somewhere (or initializing it to a constant value, in which case: why bother?). That's what readonly means, after all.

I like writing immutable classes myself. I would love to go from:
class Point {
    private readonly int _x;
    private readonly int _y;

    public Point(int x, int y) {
        _x = x;
        _y = y;
    }

    public int X { get { return _x; } }
    public int Y { get { return _y; } }
}
to
class Point {
    public int X { init; }
    public int Y { init; }
}
and have that not only be functionally the same, but shorter code than the corresponding mutable version. The rest of the complications are to make sure one doesn't fall off an edge if you have something that's slightly different. That's why it's there, not because I love contortions.

I like the primary constructor versions, but their weakness is that the validation has nowhere to go. With the recent change Mads mentioned (and I mentioned in the first post), I guess it's fairly close with this code:
class Point {
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) {
        X = x;
        Y = y;
    }
}
May 3, 2014 at 4:30 PM
How about allowing autoproperties to include private var in their declaration (e.g. int foo {get; private var;}, to indicate that the name should refer to the backing field within the class, and to the property outside of the class. Useprivate readonly varto indicate that the variable in question should be read-only. This would allow structure constructors to set their autoproperties without having to be blank-initilaized first. It would also allow the class to pass its autoproperties asreforout` parameters--something which is otherwise not possible.