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

发布于 2024-09-09 09:53:03 字数 1248 浏览 11 评论 0原文

我有一个关于值类型中的类型构造函数的问题。这个问题的灵感来自 Jeffrey Richter 通过 C# 第三版在 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,编译器以某种方式优化了构造函数调用 - 但这充其量只是一个模糊的猜测!

抛开最佳实践等不谈,我只是对它非常感兴趣,因为我希望能够亲眼看看为什么它没有被调用。

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

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

I have a question concerning type constructors within a Value type. This question was inspired by something that Jeffrey Richter wrote in CLR via C# 3rd ed, he says (on page 195 - chapter 8) that you should never actually define a type constructor within a value type as there are times when the CLR will not call it.

So, for example (well...Jeffrey Richters example actually), I can't work out, even by looking at the IL, why the type constructor is not being called in the following code:

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

So, applying the following rules for type constructors I just can't see why the value type constructor above is not called at all.

  1. I can define a static value type constructor to set the initial state of the type.
  2. A type can have no more than one constructor - there is no default one.
  3. Type constructors are implicitly private
  4. The JIT compiler checks whether the type's type constructor has already been executed in this AppDomain. If not it emits the call into native code, else it doesn't as it knows the type is already 'initialized'.

So...I just can't work out why I can't see this type array being constructed.

My best guess would be that it could be:

  1. The way that the CLR constructs a type array. I would have thought that the static constructor would be called when the first item was created
  2. The code in the constructor is not initializing any static fields so it is ignored. I have experimented with initializing private static fields within the constructor but the field remains the default 0 value - therefore the constructor is not called.
  3. Or...the compiler is somehow optimizing away the constructor call due to the public Int32 being set - but that is a fuzzy guess at best!!

Best practices etc asside, I am just super intrigued by it as I want to be able to see for myself why it doesn't get called.

EDIT: I added an answer to my own question below, just a quote of what Jeffrey Richter says about it.

If anyone has any ideas then that would be brilliant.
Many thanks,
James

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

玩心态 2024-09-16 09:53:03

Microsoft C#4 规范< /a> 与以前的版本相比略有变化,现在更准确地反映了我们在这里看到的行为:

11.3.10 静态构造函数

结构的静态构造函数如下
大部分规则与班级相同。
静态构造函数的执行
对于结构类型是由触发的
以下事件中最先发生的
在应用程序域内:

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

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

ECMA 规范 和 < a href="http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc" rel="noreferrer">Microsoft C#3 Spec 在该列表中都有一个额外的事件:“引用了结构类型的实例成员”。 所以看起来 C#3 在这里违反了它自己的规范。 C#4 规范已与 C#3 和 4 的实际行为更加一致。

编辑...

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

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

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

The Microsoft C#4 Spec has changed slightly from previous versions and now more accurately reflects the behaviour that we're seeing here:

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.)

The ECMA Spec and the Microsoft C#3 Spec both have an extra event in that list: "An instance member of the struct type is referenced". So it looks as if C#3 was in contravention of its own spec here. The C#4 Spec has been brought into closer alignment with the actual behaviour of C#3 and 4.

EDIT...

After further investigation, it appears that pretty much all instance member access except direct field access will trigger the static constructor (at least in the current Microsoft implementations of C#3 and 4).

So the current implementations are more closely correlated with the rules given in the ECMA and C#3 specs than those in the C#4 spec: the C#3 rules are implemented correctly when accessing all instance members except fields; the C#4 rules are only implemented correctly for field access.

(The different specs are all in agreement -- and apparently correctly implemented -- when it comes to the rules relating to static member access and explicitly declared constructors.)

孤星 2024-09-16 09:53:03

来自标准的 §18.3.10(另请参阅C# 编程语言 书):

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

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

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

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

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

From §18.3.10 of the standard (see also The C# programming language book):

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]

So I would agree with you that the last two lines of your program should each trigger the first rule.

After testing, the consensus seems to be that it consistently triggers for methods, properties, events, and indexers. That means it's correct for all explicit instance members except fields. So if Microsoft's C# 4 rules were chosen for the standard, that would make their implementation go from mostly right to mostly wrong.

不打扰别人 2024-09-16 09:53:03

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

对于此类内容,通常最好查看 CLR 规范而不是 C# 规范。 CLR 规范规定:

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

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

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

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

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

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

Just putting this in as an 'answer' so that I could share what Mr Richter himself wrote about it (does anyone have a link for the latest CLR spec by the way, its easy to get the 2006 edition but finding it a bit harder to get the latest one):

For this kind of stuff, it is usually better to look at the CLR spec than the C# spec. The CLR spec says:

4. If not marked BeforeFieldInit then that type’s initializer method is executed at (i.e., is triggered by):

• first access to any static field of that type, or

• first invocation of any static method of that type or

• first invocation of any instance or virtual method of that type if it is a value type or

• first invocation of any constructor for that type.

Since none of those conditions are satisfied, the static constructor is not invoked. The only tricky parts to note are that “_x” is an instance field not a static field, and constructing an array of structs does not invoke any instance constructors on the array elements.

巴黎盛开的樱花 2024-09-16 09:53:03

另一个有趣的示例:

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

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

Another interesting sample:

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

    static void Main() { new S().f(); }
凉城 2024-09-16 09:53:03

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

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

更新3:进一步参考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

Update: my observation is that unless static state is used, the static constructor will never be touched - something the runtime seems to decide and doesn't apply to reference types. This begs the question if it's a bug left because it has little impact, it's by design, or it's a pending bug.

Update 2: personally, unless you are doing something funky in the constructor, this behaviour from the runtime should never cause a problem. As soon as you access static state, it behaves correctly.

Update3: further to a comment by LukeH, and referencing Matthew Flaschen's answer, implementing and calling your own constructor in the struct also triggers the static constructor to be called. Meaning that in one out of the three scenarios, the behaviour is not what it says on the tin.

I just added a static property to the type and accessed that static property - it called the static constructor. Without the access of the static property, just creating a new instance of the type, the static constructor wasn't called.

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

A note in this link specifies that static constructors are not called when simply accessing instances:

http://www.jaggersoft.com/pubs/StructsVsClasses.htm#default

梦纸 2024-09-16 09:53:03

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

可以说

SomeValType i;
i._x = 5;

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

array[i] = new SomeRefType();

I would guess that you're creating an ARRAY of your value type. So the new keyword would be used to initialize memory for the array.

Its valid to say

SomeValType i;
i._x = 5;

with no new keyword anywhere, which is essentially what you're doing here. Were SomeValType a reference type, you'd have to initialize each element of your array with

array[i] = new SomeRefType();
朕就是辣么酷 2024-09-16 09:53:03

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

由于我们正在调用标准
这里是来自分区 I 的行,8.9.5
说的是:

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

该部分实际上很详细
关于语言如何实现
可以选择阻止该行为
你正在描述。 C++/CLI 选择不
相反,他们允许程序员
如果他们愿意的话,就可以这样做。

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

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

This is crazy-by-design behavior of "beforefieldinit" attribute in MSIL. It affects C++/CLI too, I filed a bug report where Microsoft very nicely explained why the behavior is the way it is and I pointed out multiple sections in the language standard that didn't agree / need to be updated to describe the actual behavior. But it's not publicly viewable. Anyway, here's the final word on it from Microsoft (discussing a similar situation in C++/CLI):

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.

The same behavior is what you're seeing, although in a different language.

断肠人 2024-09-16 09:53:03

这里有两件事在起作用:structs-vs-classes 和 beforefieldinit 类型标志。

如果我们在 C# 中未显式定义静态构造函数,编译器将发出 beforefieldinit 类型标志。
这对结构和类的作用是,仅在访问静态字段之前才会调用隐式定义的静态构造函数。

如果我们显式定义静态构造函数(如OP 示例),则未设置 beforefieldinit 标志,并且行为是每当调用ANY 方法(构造函数、静态、虚拟、实例)时,都会调用静态构造函数。

示例中的数组是使用 newarr 指令初始化的,该指令不会调用结构上的任何方法(也不会调用类)。

对于通过 initobj 初始化的结构实例也会发生同样的情况,它不会调用构造函数方法。但是,如果我们为该结构显式定义了一个构造函数,则使用 newobj 指令来调用该方法。这会触发静态构造函数。

There are two things at play here structs-vs-classes and the beforefieldinit type flag.

If we don't explicitly define a static constructor in C# the compiler will emit the beforefieldinit type flag.
What this does for both structs and classes is that the implicitly defined static constructor will be called ONLY before a static field is accessed.

If we explicitly define a static constructor (as in the OP example), then the beforefieldinit flag is not set and the behavior is that whenever ANY method is invoked (constructor, static, virtual, instance) the static constructor is called.

The array in the example is initialized using the newarr instruction which does not invoke any method on the struct (won't for a class either).

The same would happen for an instance of a struct that is initialized via the initobj which won't call the constructor method. However, if we have defined explicitly a constructor for the struct the newobj instruction is used which calls the method. This triggers the static constructor.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文