Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is an explicit `this` constructor initializer required in records with a primary constructor?

In C# 9 we can create positional records causing them to get a constructor, which the spec draft calls a primary constructor. We can create a custom constructor as well, but as stated in the spec:

If a record has a primary constructor, any user-defined constructor, except "copy constructor" must have an explicit this constructor initializer.

So this is disallowed:

public record A(string Foo, int Bar)
{
    public A(MyClass x)
    {
        Foo = x.Name;
        Bar = x.Number;
    }
}

and indeed causes CS8862 "A constructor declared in a record with parameter list must have 'this' constructor initializer." We have to write:

public record A(string Foo, int Bar)
{
    public A(MyClass x) : this(x.Name, x.Number) {}
}

instead. In this case this is hardly an issue, but one could imagine a much longer initialization logic that just didn't fit into the this constructor initializer.

The question is: why does this restriction exist? I'm guessing lifting it would enable a way to somehow break some of the features of records, but it's a feature new enough that I can't come up with a way to do that. Does the autogenerated primary constructor do something that is crucial for the record to work correctly, and thus it must be called?


like image 544
V0ldek Avatar asked Dec 13 '20 01:12

V0ldek


2 Answers

This is because primary constructor parameters are a little bit special - they are in scope throughout initialization of the record. Guess what the following program prints:

System.Console.WriteLine(new Foo(Bar: 42).Baz);

public record Foo(int Bar) {
    public int Bar => 41;
    public int Baz = Bar;
}

41 or 42?

And the answer is...

drumroll please...

42!

What's going on here?

During the initialization of the record, any references to Bar don't refer to the property Bar, but to the primary constructor parameter Bar.

What this means is that the primary constructor must be called. Otherwise what would happen in this case:

System.Console.WriteLine(new Foo().Baz);

public record Foo(int Bar) {
    public Foo(){}
    public int Bar => 41;
    public int Baz = Bar; //  What is Bar here when the primary constructor isn't called.
}

Aside

The Bar parameter is only in scope during initialization. After initialization the Bar property is in scope instead. If we were to change our example ever so slightly:

System.Console.WriteLine(new Foo(Bar: 42).Baz);

public record Foo(int Bar) {
    public int Bar => 41;
    public int Baz => Bar; //Note this is `=>` not `=`
}

It would print 41.

like image 144
Yair Halberstadt Avatar answered Sep 20 '22 08:09

Yair Halberstadt


Based on a quick Linqpad decompilation, yes, it looks like this constructor IS in fact doing work that otherwise might not be inferable due to not knowing how to map the type to the record properties.

public A(string Foo, int Bar)
{
    this.Foo = Foo;
    this.Bar = Bar;
    base..ctor();
}

public A(MyClass x)
    : this(x.Name, x.Number)
{
}

A record expects all properties to be initialized via the constructor. If properties could arbitrarily be mapped in an additional constructor without explicitly calling this, there would be no (immediately obvious) way to ensure that every property parameter requirement had been met. As a result, a call to this(params) is required to enforce the property mapping.

like image 44
David L Avatar answered Sep 18 '22 08:09

David L