29

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?


Daniel A. White
  • 187,200
  • 47
  • 362
  • 445
V0ldek
  • 9,623
  • 1
  • 26
  • 57

3 Answers3

25

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.

Yair Halberstadt
  • 5,733
  • 28
  • 60
2

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.

David L
  • 32,885
  • 8
  • 62
  • 93
1

I am not sure I have a definitive answer, but here are my thoughts based on what I have read. This

public record MyRecord(string foo, int bar);

is equivalent to:

public class MyRecord 
{
    string foo { get; init; } // Code correction - set to init
    int bar { get; init; }
}

The constructor and properties are both inferred in the single line. While I am sure it possible to add concrete assignment to this inferred set of constructs (and maybe will in a future version), it is likely easier to work with the chaining of constructors for the first pass. As one who has used chained constructors in some classes (not usually pocos), it makes sense. But, I am not sure how many times I would overload the constructor and I don't see a huge benefit, at least architecturally, doing it the way you did.

Gregory A Beamer
  • 16,870
  • 3
  • 25
  • 32
  • A record is immutable by default so there's no `set;` component. The [What's new C# 9 docs](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9#record-types) do a good job of breaking down the differences (immutable, value based equality, override of comparison, `ToString`, `GetHashCode`, copy and clone features) – Rudu Dec 21 '20 at 17:56
  • @Rudu - edited to get the correct code. It should have been init, not set, which means it can only be "set" on initialization. – Gregory A Beamer Dec 21 '20 at 20:19