Home > Back-end >  Static constructor order in inherited generic classes
Static constructor order in inherited generic classes

Time:03-16

Using the following code as an example ...

public class ClassA<T> where T : new()
{
    static ClassA()
    {
        Debug.WriteLine("static ctor A");
    }
    public static T TA = new T();
}
public class ClassB<T> : ClassA<T> where T : new()
{
    static ClassB()
    {
        Debug.WriteLine("static ctor B");
    }
    public static T TB = new T();
}
public class ClassC : ClassB<ClassC>
{
    static ClassC()
    {
        Debug.WriteLine("static ctor C");
    }
    public static ClassC TC = new ClassC();
}

public class Tests
{
    [Test]
    public void Test()
    {
        ClassC classC = ClassC.TC;
    }
}

... the output is

static ctor A
static ctor B
static ctor C

So, are the constructors always guaranteed to be called in order from base to most inherited, when classes are defined in the above pattern? And why is ClassB's constructor getting triggered? Does it have anything to do with the fact I'm passing a self-reference through the generic argument?

I see there are plenty of other answers related to static constructors:

The trouble I have is that this is a very specific pattern of generic inheritance with a self-referencing argument, and I can't find an example online that matches, so I don't know what behaviour to expect, or what order the constructors should get triggered in.


UPDATED TO ADD

If I remove the static fields in ClassA and ClassB, the output becomes ...

static ctor B
static ctor A
static ctor C

... which seems to make even less sense!

CodePudding user response:

The chapter 14 Classes / 14.12 Static constructors in the C# language specification:

The static constructor for a closed class executes at most once in a given application domain. The execution of a static constructor is triggered by the first of the following events to occur within an application domain:

  • An instance of the class is created.
  • Any of the static members of the class are referenced.

Let's make a test:

// Declarations (two distinct inheritance chains A1 <- A2, B1 <- B2)

class A1
{
    static A1()
    {
        Console.WriteLine(nameof(A1));
    }

    public static void M() { }
}

class A2 : A1
{
    static A2()
    {
        Console.WriteLine(nameof(A2));
    }

    public static new void M() { }
}

class B1
{
    static B1()
    {
        Console.WriteLine(nameof(B1));
    }

    public static void M() { }
}

class B2 : B1
{
    static B2()
    {
        Console.WriteLine(nameof(B2));
    }

    public static new void M() { }
}

Test:

public static void Test()
{
    A1.M();
    A2.M();

    B2.M();
    B1.M();
}

Output:

A1
A2
B2
B1

This shows: the order of execution of the static constructors is defined by the order in which static members of these classes are accessed for the first time.

Your example is different in that the classes create instances of the other classes. Remember, instance creation is also a reason for the invocation of a static constructor. I added instance constructors to your example. Note that TA and TB are creating a T instance which is of the concrete type ClassC. So, 3 ClassC instances are created. With all the static fields the output of your example now looks like this:

instance ctor A
instance ctor B
instance ctor C
static ctor A
instance ctor A
instance ctor B
instance ctor C
static ctor B
instance ctor A
instance ctor B
instance ctor C
static ctor C

Without the TA and TB fields, we get:

static ctor B
static ctor A
instance ctor A
instance ctor B
instance ctor C
static ctor C

If we delete the static field TC as well and instantiate new ClassC() directly, we get even a different order:

static ctor C
static ctor B
static ctor A
instance ctor A
instance ctor B
instance ctor C

This means that field initializers have an influence on this order. Especially when they create other instances. Without fields and recursive instance creations the static constructors are called in reverse order from the most derived to the less derived. The instance constructors are called afterwards (since they may call static fields) and are called starting with the base class.

The Remarks section of Static Constructors (C# Programming Guide) says:

[...] If static field variable initializers are present in the class of the static constructor, they're executed in the textual order in which they appear in the class declaration. The initializers run immediately prior to the execution of the static constructor. [...]

In your second example the static constructor C is executed after the field initializer was executed. This explains the order B, A, C.

Your first example is dominated by the order of execution of the field initializers and is very convoluted, as those create other instances recursively.


Summary

The order of execution of static constructors can be hard to understand and can change at any time by adding or removing code. Therefore don't rely on it. C# still ensures that every member you are allowed to call is initialized.

Constructors should be used to initialize things, not to execute business logic. Module Initializers introduced in C# 9.0 can be a better place for such things.

CodePudding user response:

Since you have static constructor (type-initializer) and static fields, your type can not be marked with the beforefieldinit. Therefore, CLI specification (ECMA 335) states in section 8.9.5 dictates that:

If not marked BeforeFieldInit then that type's initializer method is executed at (i.e., is triggered by):
① first access to any static or instance field of that type, or
② first invocation of any static, instance or virtual method of that type

The following proof is not required but makes it easier to debug the code

According to the C# specifications, the following classes are equal: The readers are encourage to look at the created IL Code

class ProofClass {
    public static int StaticMember = 5;
    static ProofClass() { }
}

class ProofClass2 {
    public static int StaticMember;

    static ProofClass2() {
        StaticMember = 5;
    }
}

Now let me rewrite your code slightly!

public class ClassC : ClassB<ClassC> {
    static ClassC() {
        TC = new ClassC()
        Debug.WriteLine("static ctor C");
    }
    public static ClassC TC;
}


public class ClassB<T> : ClassA<T> where T : new() {
    static ClassB(){
       TB = new T();
       Debug.WriteLine("static ctor B");
    }
    public static T TB;
}

public class ClassA<T> where T : new() {
    static ClassA(){
        TA = new T();
        Debug.WriteLine("static ctor A");
    }
    public static T TA;
}


//Trigger point, Entry point etc.
ClassC classC = ClassC.TC;

By accessing ClassC.TC static member, according to the standard ①, you will trigger Static Constructor of type C, which will start the following chain reaction:

In the static ctor of type C you have the following line

TC = new ClassC();

which will call the base ctor for ClassB<ClassC>. According to the standard ②, this will trigger static ctor for ClassB.

In the static ctor for ClassB, you have the following assignment:

TB = new T();

Please keep in mind that T here is ClassC, basically you have TB = new ClassC();

According to the standard, ClassC ctor will call ClassB<ClassC> ctor call which will call the base ctor for ClassA<ClassC>. Trying to access ClassA<ClassC> will trigger static ctor for ClassA.

In the static ctor of ClassA you have the following line

TA = new T();

Please keep in mind that T here is ClassC, basically you have TA = new ClassC();

According to the standard, ClassC ctor will call ClassB<ClassC> ctor call which will call the base ctor for ClassA<ClassC>.

Since each static ctor is guaranteed to run only once, we do not have recursive static ctor calling.

Summary until now

We are inside static ctor for ClassC, which triggered static ctor for ClassB, which triggered static ctor for ClassA. And we are trying to assign a ClassC instance to TA variable inside static ctor for ClassA.

From here on it is pretty straight forward!!
※TA is assigned a ClassC instance
※Debug.WriteLine("static ctor A") is called
※Static Ctor for ClassA is finished

Return to static ctor for ClassB!!
※TB is assigned a ClassC instance
※Debug.WriteLine("static ctor B") is called
※Static Ctor for ClassB is finished

Return to static ctor for ClassC!!
※TC is assigned a ClassC instance
※Debug.WriteLine("static ctor C") is called
※Static Ctor for ClassC is finished


In the second part of your question, you mention that you remove the static fields in ClassA and ClassB. You can easily apply the same logic I have written above.

This time I will just summarize:

ClassC.TC;                    →Trigger Static Ctor for ClassC
  TC = new ClassC();          →Trigger Public Ctor for ClassC
  ctor for ClassC             →Trigger Public Ctor for ClassB
    ctor for ClassB           →Trigger static Ctor for ClassB
      static ctor for ClassB
      ctor for ClassA         →Trigger static Ctor for ClassA
        static ctor for ClassA 
  static ctor for ClassC returns

Appendix A

  .method private hidebysig static specialname rtspecialname void
    .cctor() cil managed
  {
    .maxstack 8

    // [56 5 - 56 40]
    IL_0000: ldc.i4.5
    IL_0001: stsfld       int32 ProofClass::StaticMember

    // [61 5 - 61 6]
    IL_0006: ret

  } // end of method ProofClass::.cctor

  .method private hidebysig static specialname rtspecialname void
    .cctor() cil managed
  {
    .maxstack 8

    // [68 9 - 68 26]
    IL_0000: ldc.i4.5
    IL_0001: stsfld       int32 ProofClass2::StaticMember

    // [69 5 - 69 6]
    IL_0006: ret

  } // end of method ProofClass2::.cctor

  • Related