7

I've been through the relevant section of C# Language Spec (v5.0) but I can't find the piece that's relevant to what I'm seeing.

If you have a run of the code below, you'll see the output below, which is what I expect:

using System;

class Test {
   static int count = 0;
   static void Main() {
      Console.WriteLine("In Main(), A.X=" + A.X);
   }

   public static int F(string message) {
      Console.WriteLine(message);
      A.X = ++count;

      Console.WriteLine("\tA.X has been set to " + A.X);
      B.Y = ++count;

      Console.WriteLine("\tB.Y has been set to " + B.Y);
      return 999;
   }
}
class A {
   static A() { }
   public static int U = Test.F("Init A.U");
   public static int X = Test.F("Init A.X");
}

class B {
   static B() { }
   public static int R = Test.F("Init B.R");
   public static int Y = Test.F("Init B.Y");
}

The output is:

Init A.U
    A.X has been set to 1
Init B.R
    A.X has been set to 3
    B.Y has been set to 4
Init B.Y
    A.X has been set to 5
    B.Y has been set to 6
    B.Y has been set to 2
Init A.X
    A.X has been set to 7
    B.Y has been set to 8
In Main(), A.X=999

This is exactly the output I'd expect. In particular, notice that even though method F() is executing with the parameter, "Init A.U", it is called again (interrupted, if you like) once the reference to B.Y is encountered, causing B's static initializers to execute. Once B's static constructor completes, we return again to the A.U invocation of F(), which accounts for B.Y being set to 6 and then to 2. So, hopefully this output makes sense to everyone.

Here's what I'm not understanding: If you comment out B's static constructor, this is the output you see:

Init B.R
        A.X has been set to 1
        B.Y has been set to 2
Init B.Y
        A.X has been set to 3
        B.Y has been set to 4
Init A.U
        A.X has been set to 5
        B.Y has been set to 6
Init A.X
        A.X has been set to 7
        B.Y has been set to 8
In Main(), A.X=999

Sections 10.5.5.1 and 10.12 of the C# Spec (v5.0) indicate A's static constructor (and its static initializers) are triggered to execute when "any of the static members of the class are referenced." Yet here we have A.X referenced from within F() and A's static constructor is not triggered (since its static initializers are not running).

Since A has a static constructor I would expect those initializers to run (and interrupt) the "Init B.R" call to F(), just as B's static constructor interrupted A's call to F() in the "Init A.U" call that I showed at the beginning.

Can anyone explain? Af face value it looks like a violation of the spec, unless there's some other part of the spec that allows this.

Thanks

Tom Baxter
  • 2,110
  • 2
  • 19
  • 38
  • 2
    Once you remove the static constructor all bets are off when the type initializer gets run. It can get run at *any* time prior to access of the static fields. This is described in section 10.5.5.1. It is being run before you access the fields so I see no violation. Why it runs before A's static initializers run is an interesting question. I believe, in .NET 4.5, beforefieldinit classes run their type initializer when the JIT loads the type information but if there is static constructor it is run when the members are actually accessed. Here JITting `Test.F` requires B's type info to be loaded. – Mike Zboray Apr 17 '15 at 23:46
  • In addition to Mike's comments, it's worth noting that this behavior has actually changed over time. As an example, take a look a Jon Skeet's observations in his article [Type initialization changes in .NET 4.0](http://codeblog.jonskeet.uk/2010/01/26/type-initialization-changes-in-net-4-0/). The bottom line is that with the exception of some specific scenarios, the exact order static initialization is done is not guaranteed and your code should not count on any specific order. Fortunately, the spec _does_ guarantee init order when you'd expect it to, e.g.when one type depends on another, etc. – Peter Duniho Apr 18 '15 at 00:31
  • @mike z - Keep in mind I removed the static c'tor from B, not from A. The runtime is certainly free to run B's static initializers at its leisure. However, because A has a static c'tor (and therefore, has no beforefieldinit), A's static c'tor must, according to the spec (sections 10.5.5.1 and 10.12), be triggered by the reference to A.X within F(), but that's not what's happening. A.X is being referenced w/in F(), yet A's static c'tor is not executing. Maybe I'm missing something,but from what I see, it seems to violate the specification. – Tom Baxter Apr 18 '15 at 02:52
  • @Peter Duniho - The specification does make some guarantees as to the timing of static initializers if the class contains a static constructor: Please see sections 10.5.5.1 and 10.12 of the version 5.0 spec. Let me just quote sec. 10.12: 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 type is created. • Any of the static members of the class type are referenced. Since A.X is referenced in Test.F(), it should be the case, per the spec, that A's static initializers run. – Tom Baxter Apr 18 '15 at 02:58
  • @TomBaxter: _"The specification does make some guarantees as to the timing of static initializers if the class contains a static constructor"_ -- Yes, I am well aware. Hence the words "...with the exception of some specific scenarios" in my comment. – Peter Duniho Apr 18 '15 at 03:45
  • @TomBaxter No it's not getting triggered by the reference to A.X within because F() because it is in the process of running A's type initializer already by the access to A.X in Main. You can see this if you print the stack trace at the start of F. Alternatively insert another layer of indirection so that the method where the fields are accessed is not the same one as the that prints the message. – Mike Zboray Apr 18 '15 at 06:01
  • @mike z Hi Mike, What I was trying to point out was that even though F() is executing in the context of class B's static initializers it can and should be interrupted when it (that is, F()) encounters the assignment to A.X since A has a static c'tor. This interruption of F() is what we see when both A and B have static c'tors. To allow the assignment of A.X within F(), when there exists a static c'tor for A() is a violation (it seems) of 10.12, Again, it's probably me not interpreting something "correctly". – Tom Baxter Apr 18 '15 at 15:23

1 Answers1

2

I think I see what's going on here, although I don't have a good explanation for why it is this way.

The test program is a little too coarse to see what's happening. Let's make a minor adjustment:

class Test {
   static int count = 0;
   static void Main() {
      Console.WriteLine("In Main(), A.X=" + A.X);
   }

   public static int F(string message) {
       Console.WriteLine("Before " + message);
       return FInternal(message);
   }

   private static int FInternal(string message) {
      Console.WriteLine("Inside " + message);
      A.X = ++count;

      Console.WriteLine("\tA.X has been set to " + A.X);
      B.Y = ++count;

      Console.WriteLine("\tB.Y has been set to " + B.Y);
      return 999;
   }
}
class A {
   static A() { }
   public static int U = Test.F("Init A.U");
   public static int X = Test.F("Init A.X");
}

class B {
   static B() { }
   public static int R = Test.F("Init B.R");
   public static int Y = Test.F("Init B.Y");
}

The output is similar to that in the question, but with more detail:

Before Init A.U  
Inside Init A.U  
    A.X has been set to 1  
Before Init B.R  
Inside Init B.R  
    A.X has been set to 3  
    B.Y has been set to 4  
Before Init B.Y  
Inside Init B.Y  
    A.X has been set to 5  
    B.Y has been set to 6  
    B.Y has been set to 2  
Before Init A.X  
Inside Init A.X  
    A.X has been set to 7  
    B.Y has been set to 8  
In Main(), A.X=999

Nothing surprising here. Remove B's static constructor and this is what you get:

Before Init A.U  
Before Init B.R  
Inside Init B.R  
    A.X has been set to 1  
    B.Y has been set to 2  
Before Init B.Y  
Inside Init B.Y  
    A.X has been set to 3  
    B.Y has been set to 4  
Inside Init A.U  
    A.X has been set to 5  
    B.Y has been set to 6  
Before Init A.X  
Inside Init A.X  
    A.X has been set to 7  
    B.Y has been set to 8  
In Main(), A.X=999

Now this is interesting. We can see that the original output was misleading. We actually start by trying to initialize A.U. That is not surprising because A should be initialized first because A.X is accessed in Main. The next part is interesting. It looks like when B does not have a static constructor the CLR interrupts the method that is going to access B's fields (FInternal) before it enters the method. Contrast this with the other case. There the initialization of B was delayed until we actually accessed the fields of B.

I'm not entirely sure why things are done in this particular order, but you can see that the reason the initialization of B is not interrupted to initialize A is that the initialization of A has already started.

Mike Zboray
  • 39,828
  • 3
  • 90
  • 122