Why do C# 12 primary constructors execute in opposite order?

537 views Asked by At

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)

2

There are 2 answers

2
Guru Stron On

TL;DR

Your primary ctor case is using field initialization and from the Fields (C# Programming Guide):

Fields are initialized immediately before the constructor for the object instance is called. If the constructor assigns the value of a field, it overwrites any value given during field declaration.

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:

Primary Constructor will do the following sequence of operations:

  • Parameter values are stored in capture fields, if any.
  • Instance initializers are executed
  • Base constructor initializer is called

Details

If you check the decompilation of your classes with primary ctors you will see something like the following:

public class PrimaryBase
{
    public readonly int Value1 = stream.ReadByte();

    public PrimaryBase(Stream stream)
    {
    }
}

public class PrimaryDerived : PrimaryBase
{
    public readonly int Value2 = stream.ReadByte();

    public PrimaryDerived(Stream stream)
        : base(stream)
    {
    }
}

Hence the fields will be initialized before ctor call and subsequently before initialization of base class.

And the following IL for ctors:

Derived:

.method public hidebysig specialname rtspecialname 
    instance void .ctor (
class [System.Runtime]System.IO.Stream 'stream'
    ) cil managed 
{
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
        01 00 01 00 00
        )
        // Method begins at RVA 0x20b1
        // Code size 20 (0x14)
        .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: callvirt instance int32 [System.Runtime]System.IO.Stream::ReadByte()
    IL_0007: stfld int32 PrimaryDerived::Value2
    IL_000c: ldarg.0
    IL_000d: ldarg.1
    IL_000e: call instance void PrimaryBase::.ctor(class [System.Runtime]System.IO.Stream)
    IL_0013: ret
} // end of method PrimaryDerived::.ctor

Base:

.method public hidebysig specialname rtspecialname 
    instance void .ctor (
class [System.Runtime]System.IO.Stream 'stream'
    ) cil managed 
{
    // Method begins at RVA 0x209d
    // Code size 19 (0x13)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: callvirt instance int32 [System.Runtime]System.IO.Stream::ReadByte()
    IL_0007: stfld int32 PrimaryBase::Value1
    IL_000c: ldarg.0
    IL_000d: call instance void [System.Runtime]System.Object::.ctor()
    IL_0012: ret
} // end of method PrimaryBase::.ctor

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:

public static class Helper
{
    private static int Counter = 0;

    public static int GetValue(string callerName)
    {
        Console.WriteLine($"{callerName}: {Counter}");
        return Counter++;
    }
}
// "Classic" 
public class ClassicCtorBase
{
    public readonly int Value1;
    protected ClassicCtorBase() => Value1 = Helper.GetValue(nameof(ClassicCtorBase));
}

public class ClassicCtorDerived : ClassicCtorBase
{
    public readonly int Value2;
    public ClassicCtorDerived() => Value2 = Helper.GetValue(nameof(ClassicCtorDerived));
}

// Classic witout ctor but with fields initialization
public class ClassicFieldInitBase
{
    public readonly int Value1 = Helper.GetValue(nameof(ClassicFieldInitBase));
}

public class ClassicFieldInitDerived : ClassicFieldInitBase
{
    public readonly int Value2 = Helper.GetValue(nameof(ClassicFieldInitDerived));
}

// Primary ctor
public class PrimaryBase(int i = 1)
{
    public readonly int Value1 = Helper.GetValue(nameof(PrimaryBase));
}

public class PrimaryDerived(int i = 2) : PrimaryBase(i)
{
    public readonly int Value2 = Helper.GetValue(nameof(PrimaryDerived));
}

And execution:

new ClassicCtorDerived();
new ClassicFieldInitDerived();
new PrimaryDerived();

Which leads to the following output:

ClassicCtorBase: 0
ClassicCtorDerived: 1
ClassicFieldInitDerived: 2
ClassicFieldInitBase: 3
PrimaryDerived: 4
PrimaryBase: 5

As you can see both "classic" with fields init and primary ctor classes have the same order.

Demo @sharplab

4
Olivier Jacot-Descombes On

I think that you are comparing apples with oranges.

In the Classic classes you are initializing in the constructor, while in the Primary classes you are initializing in initializers. And the order of execution of constructors and initializers is indeed different. This has nothing to do with primary constructors.

You get the reverse order if you are using initializers without primary constructors being involved.

I have a sightly different setup. This is my helper class:

static class Values
{
    private static int n = 0;

    public static int GetNext(string name)
    {
        n++;
        Console.WriteLine($"{name}: {n}");
        return n;
    }
}

The classic class hierarchy:

public class ClassicBase
{
    public readonly int InitValue1 = Values.GetNext(nameof(InitValue1));
    public readonly int CtorValue1;

    public ClassicBase()
    {
        CtorValue1 = Values.GetNext(nameof(CtorValue1));
    }
}

public class ClassicDerived : ClassicBase
{
    public readonly int InitValue2 = Values.GetNext(nameof(InitValue2));
    public readonly int CtorValue2;

    public ClassicDerived()
    {
        CtorValue2 = Values.GetNext(nameof(CtorValue2));
    }
}

The test

_ = new ClassicDerived();
Console.ReadKey();

It prints:

InitValue2: 1
InitValue1: 2
CtorValue1: 3
CtorValue2: 4

See also: