Since C#9 we have the convenient primary constructor syntax for class record
(or just record
) and struct record
:
1 2 3 4 5 |
var p = new Person("Seth", "Gecko"); Assert.IsTrue($"{p.FirstName} {p.LastName}" == "Seth Gecko"); // p.FirstName = "Alan"; ERROR record are immutable by default public record Person(string FirstName, string LastName); |
C#12 introduces primary constructor for non-record class
and struct
but beware, it is very different! This is because the underlying motivation is different:
record
primary constructor represents a concise way to generate public read-only properties. This is because a record is a simple immutable object designed to hold some states.class
andstruct
primary constructor represents a concise way to generate private fields. This is becauseclass
andstruct
are implementations with internal logic that uses internal states often initialized at construction time.
For example here is a situation where quite a few lines of code can be saved:
A valid criticism to consider is that the absence of a primary constructor body could result in the creation of classes that lack proper parameter validation, thereby encouraging the development of lazy code. This potential issue is already present in records’ primary constructors.
No Property Generated
Let’s make clear that unlike record
primary constructor, properties are not generated:
Since no properties are generated, another difference with record
primary constructor is that the with
keyword cannot be used:
Usage of private fields generated by Primary Constructor
Let’s illustrate how generated private fields can be used:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var p = new Person(1970); Assert.IsTrue(p.YearOfBirth1 == 1970); p.YearOfBirth1 = 1975; // Modify the property backing field Assert.IsTrue(p.YearOfBirth1 == 1975); Assert.IsTrue(p.YearOfBirth2 == 1970); p.YearOfBirth2 = 1968; // Modify the field generated by the primay constructor Assert.IsTrue(p.YearOfBirth2 == 1968); public class Person(int yearOfBirth) { // Assign the primary constructor value of yearOfBirth to the property backing field public int YearOfBirth1 { get; set; } = yearOfBirth; // Read and assign the field generated for yearOfBirth public int YearOfBirth2 { get { return yearOfBirth; } set { yearOfBirth = value; } } } |
Actually in this code sample above, the C# compiler generates 2 private fields, one for the primary constructor parameter yearOfBirth
and one for the property YearOfBirth1
backing fields:
Naming Conflict
Primary constructor parameter naming conflict is authorized by the compiler with both field and property. This can lead to odd situations like here:
The property or field then hides the primary constructor parameter everywhere except in property initialization:
However parameters should be Camel case (yearOfBirth
), property should be Pascal case (YearOfBirth
) and by convention private fields should be underscore Camel case (_yearOfBirth
) so naming conflict should be naturally avoided.
Primary Constructor and Others Constructors
class
and struct
primary constructors can save quite some code but the downside is that it forces (eventual) other constructors to call the primary constructor through the keyword this
:
1 2 3 4 5 6 7 8 9 10 |
var p1 = new Person("Seth", "Gecko"); Assert.IsTrue(p1.FullName == "Seth Gecko"); var p2 = new Person("Seth"); Assert.IsTrue(p2.FullName == "Seth Smith"); public class Person(string firstName, string lastName) { public Person(string firstName) : this(firstName, "Smith") { } public string FullName => $"{firstName} {lastName}"; } |
Also a derived class cannot have a primary constructor as long as its base class doesn’t have a constructor with no parameters (which is named a default constructor):
Finally let’s notice that a struct
with a primary constructor still has a default constructor.
1 2 3 4 5 6 7 8 9 10 11 12 |
var p1 = new PersonStruct(); Assert.IsTrue(p1.Name == null); Assert.IsTrue(p1.YearOfBirth == 0); var p2 = default(PersonStruct); Assert.IsTrue(p2.Name == null); Assert.IsTrue(p2.YearOfBirth == 0); public struct PersonStruct(string name, int yearOfBirth) { public string Name { get; } = name; public int YearOfBirth { get; } = yearOfBirth; } |
This is because in C# a struct
has value-type semantic. This implies that a default(TStruct)
instance can be created through new TStruct()
with all states initialized with their default values, 0
for value types and null
for references.
Conclusion
C#12 class
and struct
primary constructor is a great new syntax to write more concise code. However it can be misleading since it is quite different than the existing record
primary constructor syntax. This is why in this post we insisted on the different underlying motivations.
I wish a breakpoint could be set on class
and struct
primary constructors. For now I can only break on primary constructor call when a parameter is used in a property initialization. (I checked that record
primary constructors don’t have that also). This could be useful the same way one can break upon a get;
or set;
expression. I submitted a request for that.