I'm designing a domain-specific CLI, and for the last couple weeks I've been investigating various corner cases to make sure I have a firm understanding of what's required.
Right now I'm looking into type construction. I'm considering the following scenario, which I can't find much information on:
class C
{
public static int Field = D.Field;
}
class D
{
public static int Field = C.Field;
}
class TestProg
{
static void Main()
{
Console.WriteLine( D.Field );
}
}
Both of the classes are marked beforefieldinit
.
Interestingly, this program actually compiles, and runs on the MSCLR, yielding:
0
So it seems what's happening in practice is that the trigger point in C..cctor
to construct D
is ignored because D
's construction has already started. However, to me, this program looks invalid, in the sense that C..cctor
is using something before it is fully constructed.
Many will point out that the above scenario is pointless, but it is a concern to me as an implementor of the CLI because I need to know how much latitude I have with regard to circular-references in type initializers.
All I can find in ECMA-335 about this is:
If marked BeforeFieldInit then the type’s initializer method is executed at, or sometime before, first access to any static field defined for that type.
The words "executed at" leave some ambiguity in this case because they don't specify whether the entire initializer has to execute or whether execution has to simply begin.
I ran into a comment alluding to specific rules in the CLI specs about the circular reference case, but so far I have not been able to find any mention of the issue at all in ECMA-335.
So, my questions are:
Is the above program relying on undefined behaviour? Or unspecified behaviour?
If my CLR refused to load the above program, would it still be compliant?
If not, what are the exact rules about circular references in type constructors?
Are there any valid, useful design patterns that could lead to cycles in the directed graph of a program's type initializers when flow control is discounted?
This is an answer:
II.10.5.3.1 - II.10.5.3.3 of ECMA 335 answers this fairly explicitly and explains most of the below behavior.
This is a non-answer, but far to big for a comment.
This seems to me to be illustrative:
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(D.Field);
Console.WriteLine(C.Field);
Console.WriteLine(C.FieldX);
Console.WriteLine(D.FieldY);
Console.ReadLine();
}
}
}
class D
{
public static int Field = C.FieldX;
public static int FieldY = C.Field;
}
class C
{
public static int FieldX = 5;
public static int Field = D.Field;
}
Which reliably outputs: 5 0 5 0
With analagous results for a reference type.
I believe your 'So it seems what's happening in practice is that the trigger point in C..cctor to construct D is ignored because D's construction has already started.' is false. The actual intent of 'If marked BeforeFieldInit then the type’s initializer method is executed at, or sometime before, first access to any static field defined for that type.' seems to be to me, and indeed in pracice (in this example) is:
CLR recognises Main uses D, wants to initialise D, recognises D uses C, wants to intialise C and does prior to D's intialisation really starting. (see II.10.5.3.3)
ie C is strictly initialised prior to D and any refs to D will be null / default.
In my example swap the first two main lines
static void Main(string[] args)
{
Console.WriteLine(C.Field);
Console.WriteLine(D.Field);
And D will get strictly initialised first, output
0 0 5 0
Now my question is what is your real quesion! ie do you have an example of a circular dependency closer to what you 'care about'.
Also note the behavior here is entirely analogous to
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(D.Field);
Console.WriteLine(D.FieldY);
Console.ReadLine();
}
}
}
class D
{
public static int Field = FieldY;
public static int FieldY = 5;
}
Which is 0 5
I feel that's the 'same' example but within a single types initialisation ie you can rely on the 'existance' of the fields but not the initialisation and if you want more deterministic ordering then write static constructors (which IIRC stops beforefieldinit) and creates the more deterministic approach in II.10.5.3.3.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With