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:
- Why aren't these static constructors called in generic inherited types? ... this doesn't mention the order they are called in, and by all accounts that answer suggests that the inherited class's static constructor is triggered before the first instance is created or any static members are referenced, which I am not doing in the example above, I only reference a property in
ClassC
(which istypeof(ClassC)
) and yetClassB
still has its constructor triggered - https://stackoverflow.com/a/5629446/5040941 ... again the order is not discussed
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