为什么CLR不总是调用值类型构造函数

时间:2010-07-14 12:33:03

标签: c# struct clr static-constructor typeinitializer

我对值类型中的类型构造函数有疑问。这个问题的灵感来自Jeffrey Richter在CLR中通过C#3rd ed编写的内容,他说(在第195页 - 第8章)你不应该在值类型中实际定义类型构造函数,因为有时候CLR不会调用它

所以,例如(嗯......实际上是Jeffrey Richters的例子),我无法解决,即使通过查看IL,为什么在以下代码中没有调用类型构造函数:

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
    }
}

因此,对类型构造函数应用以下规则,我无法理解为什么根本没有调用上面的值类型构造函数。

  1. 我可以定义静态值类型构造函数来设置类型的初始状态。
  2. 一个类型只能有一个构造函数 - 没有默认构造函数。
  3. 类型构造函数是隐式私有的
  4. JIT编译器检查此AppDomain中是否已执行类型的类型构造函数。如果不是它将调用发送到本机代码,否则它不会,因为它知道类型已经“初始化”。
  5. 所以......我无法理解为什么我看不到这个类型的数组正在构建。

    我最好的猜测可能是:

    1. CLR构造类型数组的方式。我原以为在创建第一个项目时会调用静态构造函数
    2. 构造函数中的代码未初始化任何静态字段,因此会被忽略。我已经尝试在构造函数中初始化私有静态字段,但该字段仍然是默认值0 - 因此不会调用构造函数。
    3. 或者......由于设置了公共Int32,编译器以某种方式优化了构造函数调用 - 但这是一个模糊的猜测!
    4. 最佳实践等帮助,我只是对它非常感兴趣,因为我希望能够自己看到它为什么不被调用。

      编辑:我在下面添加了对自己问题的回答,只是引用了Jeffrey Richter所说的内容。

      如果有人有任何想法,那将是辉煌的。 非常感谢, 詹姆斯

7 个答案:

答案 0 :(得分:17)

Microsoft C#4 Spec与之前版本略有不同,现在更准确地反映了我们在此处看到的行为:

  

11.3.10静态构造函数

     

结构的静态构造函数如下   大多数与课程相同的规则。   执行静态构造函数   对于结构类型是由。触发的   发生以下事件中的第一个   在应用程序域中:

     
      
  • 引用了struct类型的静态成员。
  •   
  • 调用结构类型的显式声明构造函数。
  •   
     

创建默认值   结构类型的(第11.3.4节)没有   触发静态构造函数。 (一个   这个例子就是初始值   数组中的元素。)

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

修改...

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

因此,当前的实现与ECMA和C#3规范中给出的规则比C#4规范中的规则更紧密相关:当访问所有实例成员时,C#3规则正确实现,除了< / em> fields; C#4规则正确实现了字段访问。

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

答案 1 :(得分:11)

从标准的§18.3.10开始(另见The C# programming language书):

  

结构的静态构造函数的执行由应用程序域中发生的以下第一个事件触发:

     
      
  • 结构的实例成员是   引用。
  •   
  • 静态成员   结构被引用。
  •   
  • 显式声明的构造函数   结构被调用。
  •   
     

[注意:创作   结构的默认值(第18.3.4节)   类型不会触发静态   构造函数。 (这方面的一个例子是   一个元素的初始值   数组。)结束记录]

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

经过测试,共识似乎是它始终触发方法,属性,事件和索引器。这意味着它对于除字段之外的所有显式实例成员都是正确的。因此,如果为标准选择了微软的C#4规则,那么这将使他们的实施从大多数情况变为大多数错误。

答案 2 :(得分:1)

更新:我的观察是,除非使用静态状态,否则永远不会触及静态构造函数 - 运行时似乎决定并且不适用于引用类型。这就引出了一个问题,如果它是一个错误,因为它没有什么影响,它是设计的,或者它是一个悬而未决的bug。

个人

更新2:,除非你在构造函数中做了一些时髦的事情,否则运行时的这种行为永远不会导致问题。一旦访问静态状态,它就会正常运行。

Update3:继续LukeH的评论,并引用Matthew Flaschen的回答,在结构中实现和调用自己的构造函数也会触发静态构造函数的调用。这意味着在三种情景中的一种情况下,行为不是它在锡上的说法。

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

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#default

答案 3 :(得分:1)

这是MSIL中“beforefieldinit”属性的疯狂设计行为。它也影响了C ++ / CLI,我提交了一个错误报告,微软非常好地解释了为什么行为是这样的,我指出了语言标准中的多个部分,它们不同意/需要更新来描述实际行为。但它不是公开可见的。无论如何,这是微软的最后一句话(讨论C ++ / CLI中的类似情况):

  

因为我们正在调用标准   这里是分区I的行,8.9.5   这样说:

     

如果标记为BeforeFieldInit则   执行type的初始化方法   在第一次访问时或之前的某个时间   到为此定义的任何静态字段   类型。

     

该部分实际上是详细的   关于如何实现语言   可以选择防止这种行为   你在描述。 C ++ / CLI选择不   而且,他们允许程序员   如果他们愿意,可以这样做。

     

基本上,因为下面的代码有   绝对没有静态字段,JIT   完全正确,根本不是   调用静态类构造函数。

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

答案 4 :(得分:1)

另一个有趣的样本:

   struct S
    {
        public int x;
        static S()
        {
            Console.WriteLine("static S()");
        }
        public void f() { }
    }

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

答案 5 :(得分:1)

只是把它作为一个'答案',以便我可以分享Richter先生自己写的关于它的内容(顺便提一下,有没有关于最新CLR规范的链接,很容易获得2006年版本但是找到了它更难获得最新的一个):

对于这种东西,通常比C#规范更好地查看CLR规范。 CLR规范说:

4。如果没有标记为BeforeFieldInit,那么该类型的初始化方法将在(即被触发)执行:

•首次访问该类型的任何静态字段,或

首先调用该类型的任何静态方法或

•如果是值类型或

,则首先调用该类型的任何实例或虚拟方法

首先调用该类型的任何构造函数。

由于没有满足这些条件,因此静态构造函数被调用。唯一需要注意的棘手部分是“_x”是一个实例字段而不是静态字段,并且构造一个结构数组 not 会调用数组元素上的任何实例构造函数。

答案 6 :(得分:0)

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

有效说

SomeValType i;
i._x = 5;

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

初始化数组的每个元素
array[i] = new SomeRefType();