为什么不总是调用值types的构造函数

我有一个值types的types构造函数的问题。 这个问题的启发是杰弗里·里希特(Jeffrey Richter)通过C#第3版在CLR中写的东西,他说(在第195页 – 第8章)你永远不应该在值types中实际定义一个types构造函数,因为有时候CLR不会调用它。

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

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

所以,为types构造函数应用下面的规则,我不明白为什么上面的值types构造函数根本不被调用。

  1. 我可以定义一个静态值types的构造函数来设置types的初始状态。
  2. 一个types只能有一个构造函数 – 没有默认构造函数。
  3. types构造函数是隐式私有的
  4. JIT编译器检查这个AppDomain中是否已经执行了types的types构造函数。 如果不是,则会将调用发送到本地代码,否则不会按照它知道该types已被“初始化”。

所以…我只是不能解决为什么我看不到这种types的数组正在构build。

我最好的猜测是,它可能是:

  1. CLR构造一个types数组的方式。 我会想到,静态构造函数将在第一个项目被创build时被调用
  2. 构造函数中的代码不会初始化任何静态字段,因此将被忽略。 我已经尝试在构造函数中初始化私有静态字段,但该字段仍然是默认的0值 – 因此构造函数不会被调用。
  3. 或者…编译器以某种方式优化了构造函数的调用,因为公共Int32被设置 – 但这是一个模糊的猜测!

最好的做法等,我只是超级好奇,因为我希望能够看到自己为什么不被调用。

编辑:我在下面添加了一个我自己的问题的答案,只是Jeffrey Richter对此所说的一句话。

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

Microsoft C#4 Spec已经从以前的版本略有改变,现在更准确地反映了我们在这里看到的行为:

11.3.10静态构造函数

结构的静态构造函数遵循大部分与类相同的规则。 结构types的静态构造函数的执行是由应用程序域内发生的以下第一个事件触发的:

  • 引用了一个结构types的静态成员。
  • 一个显式声明的structtypes的构造函数被调用。

创build结构types的默认值(第11.3.4节)不会触发静态构造函数。 (一个例子是数组中元素的初始值。)

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

编辑…

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

因此,当前实现与ECMA和C#3规范中给出的规则比C#4规范中的规则更紧密相关:在访问字段之外的所有实例成员时,C#3规则正确实现; C#4规则适用于字段访问。

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

从标准的§18.3.10开始(另请参阅C#编程语言手册):

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

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

[ 注意 :创build结构types的默认值(第18.3.4节)不会触发静态构造函数。 (这是一个数组中元素的初始值。) end note ]

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

经过testing,共识似乎是始终触发方法,属性,事件和索引器。 这意味着字段以外的所有显式实例成员都是正确的。 所以如果微软的C#4规则被选为标准,这将使他们的实施从大多数正确的大部分错误。

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

既然我们在这里调用这个标准,那么8.9.9的Partition I就是这样说的:

如果标记为BeforeFieldInit,那么types的初始化方法在第一次访问为该types定义的任何静态字段时或之前执行。

该部分实际上详细介绍了语言实现如何select来防止您描述的行为。 C ++ / CLI不select,而是允许程序员如果愿意的话。

基本上,由于下面的代码绝对没有静态字段,所以JIT在完全正确的情况下不需要调用静态类的构造函数。

尽pipe使用不同的语言,但您所看到的行为也是一样的。

另一个有趣的例子

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

更新:我的观察是,除非使用静态状态,静态构造函数将永远不会被触及 – 运行时似乎决定,不适用于引用types。 这引出了一个问题,那就是它是一个漏洞,因为它没有什么影响,它是devise的,或者是一个悬而未决的bug。

更新2:个人来说,除非你在构造函数中做了一些奇怪的事情,否则这个来自运行时的行为永远不会引起问题。 只要你访问静态,它的行为是正确的。

更新3:进一步由LukeH发表评论,并引用Matthew Flaschen的回答,在结构中实现和调用你自己的构造函数也会触发被调用的静态构造函数。 这意味着在三种情况中的一种情况下,行为并不是锡上所说的。

我只是添加一个静态属性的types,并访问该静态属性 – 它称为静态构造函数。 如果没有静态属性的访问,只是创build一个新的types实例,静态构造函数不会被调用。

 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

只要把它作为一个“答案”,以便我可以分享里奇先生自己写的关于它(没有人有最新的CLR规范的链接,容易得到2006年版,但发现有点困难得到最新的一个):

对于这种types的东西,通常比C#规范更好看一下CLR规范。 CLR规范说:

4.如果没有标记为BeforeFieldInit,那么该types的初始化方法在(即由其触发)时被执行:

•首先访问任何types的静态字段,或者

•首先调用该types的静态方法或

•如果是types值或types,则首先调用该types的任何实例或虚方法

•首先调用该types的任何构造函数。

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

我猜你正在创build一个你的值types的ARRAY。 所以新的关键字将被用来初始化数组的内存。

它有效的说

 SomeValType i; i._x = 5; 

没有任何新的关键字,这基本上是你在这里做的。 SomeValType是一个引用types,你必须用数组初始化你的数组的每个元素

 array[i] = new SomeRefType();