关于c#:为什么CLR不总是调用值类型构造函数

Why doesn't the CLR always call value type constructors

我有一个关于值类型中类型构造函数的问题。这个问题的灵感来源于Jeffrey Richter通过第三版在clr中所写的东西,他说(在第195页第8章)你不应该在一个值类型中定义类型构造函数,因为有时clr不会调用它。

因此,例如(实际上是Jeffrey Richters的例子),我无法计算出为什么在下面的代码中没有调用类型构造函数,即使通过查看IL也是如此:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
internal struct SomeValType
{
    static SomeValType()
    {
        Console.WriteLine("This never gets displayed");
    }
    public Int32 _x;
}
public sealed class Program
{
    static void Main(string[] args)
    {
        SomeValType[] a = new SomeValType[10];
        a[0]._x = 123;
        Console.WriteLine(a[0]._x);     //Displays 123
    }
}

所以,对类型构造函数应用以下规则,我就是不明白为什么上面的值类型构造函数根本没有被调用。

  • 我可以定义一个静态值类型的构造函数来设置类型的初始状态。
  • 类型不能有多个构造函数-没有默认的构造函数。
  • 类型构造函数是隐式私有的
  • JIT编译器检查该类型的类型构造函数是否已在此AppDomain中执行。如果不是,它会发出对本机代码的调用,否则它就不会知道类型已经"初始化"。
  • 所以…我就是不明白为什么我看不到这个类型数组被构造。

    我的最佳猜测是:

  • clr构造类型数组的方式。我本以为在创建第一个项时会调用静态构造函数。
  • 构造函数中的代码未初始化任何静态字段,因此将忽略它。我已经尝试在构造函数中初始化私有静态字段,但该字段仍然是默认的0值,因此不会调用构造函数。
  • 或者……由于设置了公共int32,编译器正在以某种方式优化构造函数调用,但这充其量只是一个模糊的猜测!!
  • 最佳实践等辅助,我只是超级感兴趣,因为我想自己能够看到它为什么不被调用。

    编辑:我在下面添加了我自己的问题的答案,只是引用杰弗里·里克特的话。

    如果有人有什么想法,那就太棒了。多谢,詹姆斯


    Microsoft C_4规范与以前的版本略有不同,现在更准确地反映了我们在这里看到的行为:

    11.3.10 Static constructors

    Static constructors for structs follow
    most of the same rules as for classes.
    The execution of a static constructor
    for a struct type is triggered by the
    first of the following events to occur
    within an application domain:

    • A static member of the struct type is referenced.
    • An explicitly declared constructor of the struct type is called.

    The creation of default values
    (§11.3.4) of struct types does not
    trigger the static constructor. (An
    example of this is the initial value
    of elements in an array.)

    ECMA规范和Microsoft C 3规范在该列表中都有一个额外的事件:"引用了结构类型的实例成员"。所以看起来C 3违反了它自己的规范。C 4规范已与C 3和4的实际行为更为一致。

    编辑…

    经过进一步调查,几乎所有实例成员访问(除了直接字段访问)都会触发静态构造函数(至少在当前的Microsoft C 3和4实现中)。

    因此,当前的实现与ECMA和C 3规范中给出的规则比C 4规范中给出的规则更为密切相关:访问除字段之外的所有实例成员时,C 3规则都是正确实现的;C 4规则只对字段访问正确实现。

    (当涉及到与静态成员访问和显式声明的构造函数相关的规则时,不同的规范都是一致的——并且显然是正确实现的。)


    本标准第18.3.10节(另见C编程语言手册):

    The execution of a static constructor for a struct is triggered by the first of the following events to occur within an application domain:

    • An instance member of the struct is
      referenced.
    • A static member of
      the struct is referenced.
    • An explicitly declared constructor of the
      struct is called.

    [Note: The creation
    of default values (§18.3.4) of struct
    types does not trigger the static
    constructor. (An example of this is
    the initial value of elements in an
    array.) end note]

    所以我同意你的程序最后两行应该触发第一条规则。

    在测试之后,人们的共识似乎是,它总是触发方法、属性、事件和索引器。这意味着它对于除字段之外的所有显式实例成员都是正确的。因此,如果为标准选择了微软的C 4规则,这将使其实现从基本正确到基本错误。


    把这个作为一个"答案",这样我就可以分享里克特先生自己写的关于它的文章(顺便问一下,是否有人有一个最新的clr规范的链接,它很容易得到2006年的版本,但发现要得到最新的版本有点困难):

    对于这类东西,查看clr规范通常比查看c规范要好。clr规范说:

    4。如果未标记beforefieldinit,则该类型的初始值设定项方法将在执行(即,由以下项触发):

    ?第一次访问该类型的任何静态字段,或

    ?第一次调用该类型的任何静态方法或

    ?第一次调用该类型的任何实例或虚拟方法(如果它是值类型)或

    ?第一次调用该类型的任何构造函数。

    由于这些条件都不满足,所以不会调用静态构造函数。唯一需要注意的是,"x"是一个实例字段而不是静态字段,并且构造一个结构数组不会在数组元素上调用任何实例构造函数。


    另一个有趣的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
       struct S
        {
            public int x;
            static S()
            {
                Console.WriteLine("static S()");
            }
            public void f() { }
        }

        static void Main() { new S().f(); }

    这是疯狂的设计行为的"beforefieldinit"属性在msil。它也影响了C++/CLI,我提交了一个bug报告,其中微软很好地解释了为什么行为是这样的,并且我指出了语言标准中不同意/需要更新以描述实际行为的多个部分。但这是不公开的。总之,这里是微软的最后一个字(讨论C++中类似的情况):

    Since we're invoking the standard
    here, the line from Partition I, 8.9.5
    says this:

    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.

    That section actually goes into detail
    about how a language implementation
    can choose to prevent the behavior
    you're describing. C++/CLI chooses not
    to, rather they allow the programmer
    to do so if they wish.

    Basically, since the code below has
    absolutely no static fields, the JIT
    is completely correct in simply not
    invoking static class constructors.

    同样的行为就是你所看到的,尽管用的是不同的语言。


    更新:我的观察是,除非使用静态状态,否则将永远不会接触静态构造函数——运行时似乎决定了这一点,并且不适用于引用类型。这就回避了这样一个问题:它是因为影响很小而留下的一个bug,还是因为设计原因,或者它是一个悬而未决的bug。

    更新2:就个人而言,除非您在构造函数中做了一些奇怪的事情,否则运行时的这种行为永远不会引起问题。一旦您访问静态状态,它就会正常工作。

    更新3:除了Lukeh的注释之外,参考Matthew Flaschen的答案,在结构中实现和调用自己的构造函数也会触发调用静态构造函数。这意味着,在三种情况中,有一种情况下,行为并不是它在锡上所说的。

    我刚向类型添加了一个静态属性,并访问了该静态属性——它称为静态构造函数。如果不访问静态属性,只创建类型的新实例,则不会调用静态构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    internal struct SomeValType
        {
            public static int foo = 0;
            public int bar;

            static SomeValType()
            {
                Console.WriteLine("This never gets displayed");
            }
        }

        static class Program
        {
            /// <summary>
            /// The main entry point for the application.
            /// </summary>
            [STAThread]
            static void Main()
            {
                // Doesn't hit static constructor
                SomeValType v = new SomeValType();
                v.bar = 1;

                // Hits static constructor
                SomeValType.foo = 3;
            }
        }

    此链接中的一个注释指定,仅访问实例时不调用静态构造函数:

    http://www.jaggersoft.com/pubs/structsvsclasses.htm默认


    我猜您正在创建一个值类型的数组。因此,新的关键字将用于初始化数组的内存。

    可以这么说

    1
    2
    SomeValType i;
    i._x = 5;

    任何地方都没有新的关键字,这基本上就是你在这里所做的。如果somevaltype是引用类型,则必须使用

    1
    array[i] = new SomeRefType();