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. (This exactly matches the behavior when defining primary constructors on classes or structs.) Also 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; }
In keeping with the semantics of primary constructors on classes and structs (see “Primary Constructors”), the primary constructor 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. The reason is that 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 => ... } }