Primary Constructors
When you define a record with a parameter list, the compiler generates property declarations automatically, as well as a primary constructor (and a deconstructor). As weâve seen, this works well in simple cases, and in more complex cases you can omit the parameter list and write the property declarations and constructor manually.
C# also offers a mildly useful intermediate optionâif youâre willing to deal with the curious semantics of primary constructorsâwhich is to define a parameter list while writing some or all of the property declarations yourself:
record Student (string ID, string LastName, string GivenName) { public string ID { get; } = ID; }
In this case, we âtook overâ the ID
property definition, defining it as read-only (instead of init-only), preventing it from partaking in nondestructive mutation. If you never need to nondestructively mutate a particular property, making it read-only lets you store computed data in the record without having to code up a refresh mechanism.
Notice that we needed to include a property initializer (in boldface):
public string ID { get; } = ID;
When you âtake overâ a property declaration, you become responsible for initializing its value; the primary constructor no longer does this automatically. Note that the ID
in boldface refers to the primary constructor parameter, not the ID
property.
Note
With record structs, itâs legal to redefine a property as a field:
record struct Student (string ID) { public string ID = ID; }
A unique feature of primary constructors is that their parameters (ID
, LastName
, and GivenName
in this case) are magically visible to all field and property initializers. We can illustrate this by extending our example as follows:
record Student (string ID, string LastName, string FirstName) { public string ID { get; } = ID; readonly int _enrollmentYear = int.Parse (ID.Substring (0, 4)); }
Again, the ID
in boldface refers to the primary constructor parameter, not the property. (The reason for there not being an ambiguity is that itâs illegal to access properties from initializers.)
In this example, we calculated _enrollmentYear
from the first four digits of the ID
. While itâs safe to store this in a read-only field (because the ID
property is read-only and so cannot be nondestructively mutated), this code would not work so well in the real world. This is because without an explicit constructor, thereâs no central place in which to validate ID
and throw a meaningful exception should it be invalid (a common requirement).
Validation is also a good reason for needing to write explicit init-only accessors (as we discussed in âProperty Validationâ). Unfortunately, primary constructors do not play well in this scenario. To illustrate, consider the following record, where an init
accessor performs a null validation check:
record Person (string Name) { string _name = Name; public string Name { get => _name; init => _name = value ?? throw new ArgumentNullException ("Name"); } }
Because Name
is not an automatic property, it cannot define an initializer. The best we can do is put the initializer on the backing field (in boldface). Unfortunately, doing so bypasses the null check:
var p = new Person (null); // Succeeds! (bypasses the null check)
The difficulty is that thereâs no way to assign a primary constructor parameter to a property without writing the constructor ourselves. While there are workarounds (such as factoring the init
validation logic into a separate static method that we call twice), the simplest workaround is to avoid the parameter list altogether and write an ordinary constructor manually (and deconstructor, should you need it):
record Person { public Person (string name) => Name = name; // Assign to *PROPERTY* string _name; public string Name { get => _name; init => ... } }