Introduction
Primary constructors is a new feature of C# 12 that allows us to declare parameters in a class or struct declaration that their scope is the entire class (or struct). It is important to understand the distinction between parameters and fields. Although the scope of the primary constructors is the entire type, they are not field members of the type.
Primary constructors, allow us to avoid writing boilerplate code, when we need a constructor, help us initialize a field or a property and help with the initialization of the classes that have inheritance dependencies.
Because primary constructors are parameters and not fields, sometimes they need to be captured. This means that the language in those cases will create hidden fields so that it can have access to these parameters. For 95% of the use cases, this won’t make any difference for us, but there are edge cases where the distinction between parameters and private fields matter.
Let’s start with a simple usage and then expand into more corner cases.
public class Example(int i, string s)
{
public int PlusOne { get; } = i + 1;
public string Message { get; } = s + " World";
}
Example classExample = new(1, "Hello");
Console.WriteLine(classExample.PlusOne); // prints 2
Console.WriteLine(classExample.Message); // prints Hello World
With the above code, we don’t need anymore to create a constructor, assign the parameters to our own fields inside the constructor and then use these parameters in our properties, everything is done for us automatically. The same is true for structs.
public struct StructExample(int i, string s)
{
public int PlusOne { get; } = i + 1;
public string Message { get; } = s + " World";
}
Parameters Are Not Always Captured, So They Don’t Always Exist As An Object’s Data
Let’s try to get the size of the following struct:
public struct StructExample(int i)
{
public readonly int PlusOne = i + 1;
}
Console.WriteLine(System.Runtime.InteropServices.Marshal.SizeOf(typeof(StructExample))); // prints 4
The above type has a size of 4 bytes, which is the size of the PlusOne
field which is an int. No memory is used for the i
integer as the i
, is not needed at runtime. For this reason no hidden backing field for the i
is created. In contrast:
public struct StructExample(int i)
{
public readonly int PlusOne = i + 1;
public int ReturnPlusOne(int methodParam)
{
return i + 1;
}
}
Console.WriteLine(System.Runtime.InteropServices.Marshal.SizeOf(typeof(StructExample))); // prints 8
Now the size of our struct is 8 bytes. The reason is that the compiler now, needs to generate a private field that captures the value of the i
parameter, because it is needed for our ReturnPlusOne
method.
Multiple constructors
If there is a need for multiple constructors, each constructor can use the primary constructor’s parameters with the this
keyword
public class Example(int i, string s)
{
public int PlusOne { get; } = i + 1;
public string Message { get; } = s + " World";
public Example() : this(0, string.Empty)
{ }
public Example(int i) : this(i, string.Empty)
{ }
}
Primary Constructors And Inheritance
For Inheritance, we can use primary constructors for one of the classes or both of them:
public class Base
{
private readonly int _i;
public Base(int i)
{
_i = i;
}
public override string ToString() => $"{_i} from base";
}
public class Child(int childI) : Base(childI)
{
public override string ToString() => $"{childI} from child";
}
or
public class Base(int i)
{
public override string ToString() => $"{i} from base";
}
public class Child(int childI) : Base(childI)
{
public override string ToString() => $"{childI} from child";
}
A minor problem here, is that the primary parameter childI
is captured in the Child
class because it is being used in a method, but is also passed as a parameter to the Base
class which in turn, because it is also used in a method, will be recaptured by the base class. In this example, this problem doesn’t affect us, but this might:
public class Person
{
public int Age;
}
public abstract class Base(Person person)
{
public Person MyPersonProperty = person;
public abstract void ChangePerson();
}
public class Child(Person childPerson) : Base(childPerson)
{
public override void ChangePerson() => childPerson = new Person { Age = 69 };
}
Person aPerson = new Person{ Age = 10 };
Child child = new Child(aPerson);
child.ChangePerson();
Console.WriteLine(child.MyPersonProperty.Age); // prints 10
Because the childPerson
parameter is captured in a hidden field, any modifications won’t be seen by the parent class, as its parameter has also been captured in a hidden field inside the base. Effectively, we now have two copies that are independent of each other, the copy of the childPerson
inside the Child
class and the copy of the person
inside the Base
class.
For reference types, this is unintuitive, as we would expect that any changes would be reflected in the parent class. Fortunately the compiler issues a warning if there is a possibility that the parameter can be captured by the parent class. We have to remember that primary constructors parameters are actual parameters that their scope is the entire class and not class fields.
Double Capture In The Same Class
The same problem can happen in the body of one class. We can, by mistake, capture the parameter two times. Consider the following:
public class Enemy(int hp)
{
public int HitPoints { get; set; } = hp;
public string ShowHitPoints => $"The enemy's hit points are {hp}";
}
Here the hp
parameter is captured by the backing field of the property because the hidden field of the parameter is assigned to the hidden field of the property and then the same happens with the ShowHitPoints
property. See the following code:
Enemy enemy = new Enemy(100);
Console.WriteLine(enemy.HitPoints); // prints 100
Console.WriteLine(enemy.ShowHitPoints); // prints The enemy's hit points are 100
enemy.HitPoints = 10;
Console.WriteLine(enemy.HitPoints); // prints 10
Console.WriteLine(enemy.ShowHitPoints); // prints The enemy's hit points are 100
This would have happened even if we had used a method:
public class Enemy(int hp)
{
public int HitPoints { get; set; } = hp;
public string ShowHitPoints() => $"The enemy's hit points are {hp}";
}
The hidden hp
field that is created is assigned to the hidden backing field of the ShowHitPoints
property but then, the ShowHitPoints
method is using the hidden hp
field and the property its own backing field.
We can avoid these problems by remembering that there is a hidden field behind each parameter and either create a mechanism that synchronizes each change, or by using only one capture, for example:
public class Enemy(int hp)
{
public int HitPoints
{
get => hp;
set => hp = value;
}
public string ShowHitPoints() => $"The enemy's hit points are {hp}";
}
Or
public class Enemy(int hp)
{
public int HitPoints { get; set; } = hp;
public string ShowHitPoints() => $"The enemy's hit points are {HitPoints}";
}
The same solution can be done a little differently, if we are afraid that we might forget about the double capture. We can create a private field with the same name as our parameter, this field will shadow the parameter:
public class Enemy(int hp)
{
private int hp = hp; // shadows the hp parameter
public int HitPoints
{
get => hp;
set => hp = value;
}
public string ShowHitPoints => $"The enemy's hit points are {hp}";
}
This is also useful if we want the parameter to be used as a readonly field:
public class Enemy(int hp)
{
private readonly int hp = hp;
public string ShowHitPoints => $"The enemy's hit points are {hp}";
}
The only problem with the last two techniques is that they may violate naming conventions that we may have for our private fields (for example we may have a convention that our parameters are camelCase
and our private fields _camelCase
).
Instantiation, Initialization and Execution Order
The creation of objects from our types in C#, follows a certain order, which is a whole subject on its own, but can also affect the way primary constructor parameters vs normal constructors are executed. Let’s suppose that we have the following code:
public static class Helper
{
public static int GetAndRemoveFirstElement(List<int> aList)
{
int temp = aList[0];
aList.RemoveAt(0);
return temp;
}
}
It is a static helper class that has a method that returns the first element of a List<int>
and then removes it.
Now we create a base and a child class with normal constructors:
public class BaseWithConstructor
{
public readonly int ListElementBaseClassic;
public BaseWithConstructor(List<int> aList)
{
ListElementBaseClassic = Helper.GetAndRemoveFirstElement(aList);
}
}
public class ChildWithConstructor : BaseWithConstructor
{
public readonly int ListElementChildClassic;
public ChildWithConstructor(List<int> aList) : base(aList)
{
ListElementChildClassic = Helper.GetAndRemoveFirstElement(aList);
}
}
Both of those at construction, they populate their respective public fields with the first element of their provided list and then remove the element from the list. Then if we execute:
List<int> myList = [1, 2];
ChildWithConstructor childWithConstructor = new(myList);
Console.WriteLine(childWithConstructor.ListElementBaseClassic);
Console.WriteLine(childWithConstructor.ListElementChildClassic);
The result will be:
1
2
If we do the same thing with classes that use a primary constructor:
public class BaseWithParameters(List<int> aList)
{
public readonly int ListElementBaseWithParameter = Helper.GetAndRemoveFirstElement(aList);
}
public class ChildWithParameters(List<int> aList) : BaseWithParameters(aList)
{
public readonly int ListElementChildWithParameter = Helper.GetAndRemoveFirstElement(aList);
}
and we execute the same code:
List<int> myList = [1, 2];
ChildWithParameters childWithParameters = new ChildWithParameters(myList);
Console.WriteLine(childWithParameters.ListElementBaseWithParameter);
Console.WriteLine(childWithParameters.ListElementChildWithParameter);
the result will be the opposite:
2
1
Why The Reverse Result Happens
In C#, when an object is created, first the fields are initialized (among other things) from the bottom to the top, this means from the children to the parents. After that the constructors run in the opposite order, from the parent to the children. How the order of instantiation and initialization happens probably deserves a post on its own, but in the primary constructors vs normal constructors case, the following is happening:
When we have normal constructors, the instance field of the child (the ListElementChildClassic
) is initialized with its default value (0 for integers), then the instance field of the parent (the ListElementBaseClassic
) is initialized with its default value (0). Then the base constructor executes, gets the first element of the list (the 1
), removes it and then assigns it to its field (the ListElementBaseClassic
), then the child constructor runs, gets the first element of the list (the 2
, because the 1
has been removed) and assigns it to its field (the ListElementChildClassic
).
When we have the primary constructors, the fields are initialized with the same order (from child to parent), but this time not with their default value, but with the value provided. The child will get the first element of the list and assign it to its field (the ListElementChildWithParameter
) and then the base class will do the same with the ListElementBaseWithParameter
field.
This is an extreme use case and happens because of the way the C# executes the order of initialization (which is different for Java), but it is good to know why this happens as we may find ourselves in a similar situation, especially if we are in the process of refactoring code from normal constructors to primary constructors.
Conclusion
This is all I could think of about primary constructors and some edge cases. If you happen to know any other edge cases, I would be happy to hear about them and update this post. Primary constructors are a new feature in C# 12, and probably we are not done with them, as there are proposals of adding a readonly modifier for primary constructors. For those interested you can read about it here: C# Language Design Meeting for July 31st, 2023
Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.