In this article, we’ll take a look at a new feature of C# 12 and .NET 8 for classes and structs called primary constructors that is still in preview at the time of writing the article.
Let’s dive in!
Primary Constructors for Records
Records are immutable reference types that offer value-based equality. Primary constructors for record
types were introduced in C# 9:
public record Point(double X, double Y);
With this line, we define a record
type called Point
that has two properties – X
and Y
. When we initialize a new Point
, the compiler will automatically assign the two properties:
[Fact] public void WhenConstructorIsInvoked_ThenAllPropertiesAreAssigned() { // Arrange var x = 5.0; var y = 6.5; // Act var point = new Point(x, y); // Assert point.X.Should().Be(x); point.Y.Should().Be(y); }
Here we verify that when the constructor is called with x
and y
variables, the corresponding values are assigned to the created point object’s properties X
and Y
. We also use the FluentAssertions
library to perform the assertions, ensuring that point.X
and point.Y
equal the expected values of our variables x
and y
, respectively.
With this simple but powerful feature, we can save a lot of time and needles lines of code. Next, we’ll see how the feature looks for classes and structs.
Installing the Prerequisites
Before the official release of the primary constructors for non-record classes and structs features, we must install certain components in order to utilize them. First, we need to start by installing the latest .NET 8 preview version. Then we also need to install the latest preview version of Visual Studio.
After we are ready, we can start experimenting with the new features.
Primary Constructors for Classes and Structs
Let’s first look at a simple class
which we call Doctor
, but everything applies to struct
types as well:
public class Doctor { private List<string> _patients; public string Name { get; set; } public bool IsOverworked => _patients.Count >= 5; public Doctor(string name) { Name = name; _patients = new List<string>(); } public void AddPatient(string patient) => _patients.Add(patient); }
We define a Doctor
class with properties Name
and IsOverworked
. The Name
property is a string
representing the doctor’s name, and IsOverworked
is a computed property returning true
if the doctor has at least five patients. The constructor takes a name
parameter and initializes the Name
property while also creating an empty _patients
list to store patient names. The class has an AddPatient()
method that allows adding patient names to the private list _patients
.
Now, let’s utilize a primary constructor for the same class:
public class Doctor(string name, List<string> patients) { public string Name { get; set; } = name; public bool IsOverworked => patients.Count >= 5; public Doctor(string name) : this(name, new List<string>()) { } public void AddPatient(string patient) => patients.Add(patient); }
We update our Doctor
class by adding a primary constructor that accepts name
and patients
parameters. The main constructor still takes the name
parameter but it calls the primary constructor, passing the name
and an empty List<string>
. The name
and patients
parameters will be available to us throughout the body of the class, much like private fields. We can use them to initialize properties, which we do for the Name
property. Another thing we can do is to use them inside methods or while computing values for properties as we do with the IsOverworked
property.
Key Points From Primary Constructors for Classes and Structs
One very important thing we should remember is that properties will not be automatically created for primary constructor parameters. This helps us only expose the properties we really need to. Classes and structs are complex data structures with underlying logic and not all constructor parameters should be accessible to the outside world.
Another crucial thing is the fact that all constructors in a class should call the primary one using the this
keyword. Calling the primary constructor is a requirement to ensure that all essential data is available for the creation of a class or struct. Failure to do so will result in errors and inconsistencies that can jeopardize the integrity of our code. It is crucial that you prioritize this step to ensure the optimal functionality and efficiency of your code.
One final thing to remember is that classes and structs with primary constructors still have reference-based equality and don’t get the ==
(equality) and !=
(inequality) operators by default. If you want to use them, you will have to explicitly override them.
Conclusion
In conclusion, primary constructors in C# 12 and .NET 8 offer a powerful feature for classes and structs, allowing concise and efficient initialization of properties. While they were initially introduced for records in C# 9, the extension to classes and structs provides similar benefits. With primary constructors, we can streamline object creation, eliminating the need for manual property assignments and reducing unnecessary code. By ensuring that all constructors call the primary one, we guarantee the availability of essential data, leading to more robust and maintainable code.