Why do primary constructors in C# 12 do execute in opposite order?
That's kind of a breaking change to say the least...
Example:
namespace Whatever;
[TestClass]
public class UnitTestTemp
{
[TestMethod]
public void TestMethod1() // PASS // is expected, 1st is 1, 2nd is 2
{
using var stream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
var classicDerived = new ClassicDerived(stream);
Console.WriteLine(classicDerived.Value1);
Console.WriteLine(classicDerived.Value2);
Assert.AreEqual(1, classicDerived.Value1);
Assert.AreEqual(2, classicDerived.Value2);
}
[TestMethod]
public void TestMethod2() // FAIL // is opposite, 1st is 2, 2nd is 1
{
using var stream = new MemoryStream(new byte[] { 1, 2, 3, 4 });
var primaryDerived = new PrimaryDerived(stream);
Console.WriteLine(primaryDerived.Value1);
Console.WriteLine(primaryDerived.Value2);
Assert.AreEqual(1, primaryDerived.Value1);
Assert.AreEqual(2, primaryDerived.Value2);
}
}
Classic constructor:
public class ClassicBase
{
public readonly int Value1;
protected ClassicBase(Stream stream)
{
Value1 = stream.ReadByte();
}
}
public class ClassicDerived : ClassicBase
{
public readonly int Value2;
public ClassicDerived(Stream stream) : base(stream)
{
Value2 = stream.ReadByte();
}
}
Primary constructor:
public class PrimaryBase(Stream stream)
{
public readonly int Value1 = stream.ReadByte();
}
public class PrimaryDerived(Stream stream) : PrimaryBase(stream)
{
public readonly int Value2 = stream.ReadByte();
}
1st test outcome:
TestMethod1
Source: UnitTestTemp.cs line 7
Duration: 4 ms
Standard Output:
1
2
2nd test outcome:
TestMethod2
Source: UnitTestTemp.cs line 21
Duration: 26 ms
Message:
Assert.AreEqual failed. Expected:<1>. Actual:<2>.
Stack Trace:
UnitTestTemp.TestMethod2() line 30
RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Standard Output:
2
1
As you can see, it's a bit problematic if for instance you use a stream from within constructors.
Question:
Is there another way to address that beside reverting to classic constructors?
(was thinking of maybe something like SetsRequiredMembers for the new required modifier)
TL;DR
Your primary ctor case is using field initialization and from the Fields (C# Programming Guide):
So all fields initialization happens before the constructors being called resulting in first derived class fields initialized and then the base (I assume this happens due to ability to call overloaded methods in the base ctor and those overloaded methods might use some initialized fields - see for example this).
And from the Primary constructors specification:
Details
If you check the decompilation of your classes with primary ctors you will see something like the following:
Hence the fields will be initialized before ctor call and subsequently before initialization of base class.
And the following IL for ctors:
Derived:
Base:
So basically from IL (and "lower") standpoint of view the derived class ctor will actually always be called first, it will initialize the derived fields, call the base ctor (which in it's turn will initialize fields and call code defined in C# base class ctor) and then call code defined in C# derived class ctor.
The minimal reproducible example to compare the behaviours can be arguably simplified to something like the following:
And execution:
Which leads to the following output:
As you can see both "classic" with fields init and primary ctor classes have the same order.
Demo @sharplab