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?
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.
}
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
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With