38

我有一个关于Value type 中的类型构造函数的问题。这个问题的灵感来自 Jeffrey Richter 通过 C# 第 3 版在 CLR 中编写的内容,他说(在第 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 中执行。如果不是,它将调用发送到本机代码,否则它不会因为它知道类型已经“初始化”。

所以......我只是无法弄清楚为什么我看不到正在构造的这种类型数组。

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

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

除了最佳实践等,我只是对它非常感兴趣,因为我希望自己能够看到为什么它没有被调用。

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

如果有人有任何想法,那就太好了。非常感谢,詹姆斯

4

7 回答 7

19

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

11.3.10 静态构造函数

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

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

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

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

编辑...

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

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

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

于 2010-07-14T13:56:20.260 回答
11

从标准的第 18.3.10 节开始(另见C# 编程语言书):

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

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

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

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

经过测试,共识似乎是它始终如一地触发方法、属性、事件和索引器。这意味着它对于字段之外的所有显式实例成员都是正确的。因此,如果选择 Microsoft 的 C# 4 规则作为标准,这将使它们的实现从大部分正确变为大部分错误。

于 2010-07-14T12:40:36.813 回答
2

只是把它作为一个“答案”,这样我就可以分享里希特先生自己写的关于它的内容(顺便说一句,有没有人有最新 CLR 规范的链接,很容易获得 2006 版,但发现它有点难获取最新的):

对于这类东西,通常最好看 CLR 规范而不是 C# 规范。CLR 规范说:

4. 如果未标记 BeforeFieldInit,则该类型的初始化方法在以下位置执行(即由以下方式触发):

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

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

• 首次调用该类型的任何实例或虚方法,如果它是值类型或

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

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

于 2010-07-16T05:41:42.400 回答
1

更新:我的观察是,除非使用静态状态,否则永远不会触及静态构造函数——运行时似乎决定了这一点,并且不适用于引用类型。这引出了一个问题,如果它是一个遗留的错误,因为它几乎没有影响,它是设计使然,还是它是一个待处理的错误。

更新 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

于 2010-07-14T12:41:40.213 回答
1

另一个有趣的示例:

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

    static void Main() { new S().f(); }
于 2010-07-14T12:54:49.807 回答
0

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

说得有道理

SomeValType i;
i._x = 5;

在任何地方都没有 new 关键字,这基本上就是您在这里所做的。如果 SomeValType 是引用类型,则必须使用初始化数组的每个元素

array[i] = new SomeRefType();
于 2010-07-14T12:38:40.583 回答
0

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

由于我们在此处调用标准,因此 Partition I, 8.9.5 中的行是这样说的:

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

该部分实际上详细介绍了语言实现如何选择阻止您所描述的行为。C++/CLI 选择不这样做,而是允许程序员根据需要这样做。

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

您看到的是相同的行为,尽管使用的是不同的语言。

于 2010-07-14T12:45:44.313 回答