为什么 CLR 不总是调用值类型构造函数
我有一个关于值类型中的类型构造函数的问题。这个问题的灵感来自 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
}
}
因此,对类型应用以下规则我只是不明白为什么上面的值类型构造函数根本没有被调用。
- 我可以定义一个静态值类型构造函数来设置类型的初始状态。
- 一种类型只能有一个构造函数 - 没有默认构造函数。
- 类型构造函数是隐式私有的
- JIT 编译器检查该类型的类型构造函数是否已在此 AppDomain 中执行。如果不是,它会将调用发出到本机代码,否则它不会发出调用,因为它知道类型已经“初始化”。
所以...我只是不明白为什么我看不到正在构造的这种类型数组。
我最好的猜测是它可能是:
- CLR 构造类型数组的方式。我本以为创建第一个项目时会调用静态构造函数。
- 构造函数中的代码没有初始化任何静态字段,因此它被忽略。我尝试过在构造函数中初始化私有静态字段,但该字段仍保留默认 0 值 - 因此不会调用构造函数。
- 或者...由于设置了公共 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.
- I can define a static value type constructor to set the initial state of the type.
- A type can have no more than one constructor - there is no default one.
- Type constructors are implicitly private
- 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:
- 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
- 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.
- 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
Microsoft C#4 规范< /a> 与以前的版本相比略有变化,现在更准确地反映了我们在这里看到的行为:
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:
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.)
来自标准的 §18.3.10(另请参阅C# 编程语言 书):
所以我同意你的观点,程序的最后两行应该分别触发第一条规则。
经过测试,共识似乎是它始终触发方法、属性、事件和索引器。这意味着它对于除字段之外的所有显式实例成员都是正确的。因此,如果选择 Microsoft 的 C# 4 规则作为标准,那么它们的实现就会从大部分正确变为大部分错误。
From §18.3.10 of the standard (see also The C# programming language book):
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.
只是将其作为“答案”,以便我可以分享 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.
另一个有趣的示例:
Another interesting sample:
更新:我的观察是,除非使用静态,否则静态构造函数永远不会被触及 - 这是运行时似乎决定的并且不适用于引用类型。这就引出了一个问题:它是否是一个遗留的错误,因为它几乎没有影响,它是设计使然的,或者它是一个悬而未决的错误。
更新 2: 就个人而言,除非您在构造函数中做了一些奇怪的事情,否则运行时的这种行为永远不会导致问题。一旦您访问静态,它就会正确运行。
更新3:进一步参考LukeH的评论,并引用Matthew Flaschen的答案,在结构中实现和调用您自己的构造函数也会触发调用静态构造函数。这意味着在这三种情况中的一种情况下,行为并不像罐头上所说的那样。
我刚刚向该类型添加了一个静态属性并访问该静态属性 - 它称为静态构造函数。如果没有静态属性的访问,只是创建该类型的新实例,则不会调用静态构造函数。
此链接中的注释指定仅访问实例时不调用静态构造函数:
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.
A note in this link specifies that static constructors are not called when simply accessing instances:
http://www.jaggersoft.com/pubs/StructsVsClasses.htm#default
我猜你正在创建一个值类型的数组。因此 new 关键字将用于初始化数组的内存。
可以说
在任何地方都没有 new 关键字,这本质上就是您在这里所做的。如果 SomeValType 是引用类型,则必须使用以下命令初始化数组的每个元素
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
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
这是 MSIL 中“beforefieldinit”属性的疯狂设计行为。它也会影响 C++/CLI,我提交了一份错误报告,其中 Microsoft 很好地解释了为什么行为是这样的,并且我指出了语言标准中不同意/需要更新以描述实际行为的多个部分。但它不公开可见。无论如何,这是 Microsoft 的最终结论(讨论 C++/CLI 中的类似情况):
尽管使用不同的语言,但您所看到的行为是相同的。
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):
The same behavior is what you're seeing, although in a different language.
这里有两件事在起作用: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 thenewobj
instruction is used which calls the method. This triggers the static constructor.