[你必须知道的.NET]第二十三回:品味细节,深入.NET的类型构造器
今天Artech兄在《关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释》一文中让我们认识了一个关于类型构造器调用执行的有趣示例,其中也相应提出了一些关于beforefieldinit对于类型构造器调用时机的探讨,对于我们很好的理解类型构造器给出了一个很好的应用实践体验。
作为补充,本文希望从基础开始再层层深入,把《关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释》一文中没有解释的概念和原理,进行必要的补充,例如更全面的认识类型构造器,认识BeforeFieldInit。并在此基础上,探讨一点关于类型构造器的实践应用,同时期望能够回答其中示例运行的结果。
废话少说,我们开始。
2 认识对象构造器和类型构造器
在.NET中,一个类的初始化过程是在构造器中进行的。并且根据构造成员的类型,分为类型构造器(.cctor)和对象构造器(.ctor), 其中.cctor和.ctor为二者在IL代码中的指令表示。.cctor不能被直接调用,其调用规则正是本文欲加阐述的重点,详见后文的分析;而.ctor会在类型实例化时被自动调用。
基于对类型构造器的探讨,我们有必要首先实现一个简单的类定义,其中包括普通的构造器和静态构造器,例如
<span style="color:black"><span style="color:black"> <span style="color:#008000">// Release : code01, 2008/11/02 </span></span></span>
// Author : Anytao, http://www.anytao.com
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> User</span></span>
{
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">static</span> User()</span></span>
{
<span style="color:black"><span style="color:black"> message = <span style="color:#006080">"Initialize in static constructor."</span>;</span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
public User()
<span style="color:black"><span style="color:black"> {</span></span>
message = "Initialize in normal construcotr.";
<span style="color:black"><span style="color:black"> }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> User(<span style="color:#0000ff">string</span> name, <span style="color:#0000ff">int</span> age)</span></span>
{
<span style="color:black"><span style="color:black"> Name = name;</span></span>
Age = age;
<span style="color:black"><span style="color:black"> }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">string</span> Name { get; set; }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">int</span> Age { get; set; }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">static</span> <span style="color:#0000ff">string</span> message = <span style="color:#006080">"Initialize when defined."</span>;</span></span>
我们将上述代码使用ILDasm.exe工具反编译为IL代码,可以很方便的找到相应的类型构造器和对象构造器的影子,如图
然后,我们简单的来了解一下对象构造器和类型构造器的概念。
- 对象构造器(.ctor)
在生成的IL代码中将可以看到对应的ctor,类型实例化时会执行对应的构造器进行类型初始化的操作。
关于实例化的过程,设计到比较复杂的执行顺序,按照类型基础层次进行初始化的过程可以参阅《你必须知道的.NET》7.8节 “动静之间:静态和非静态”一文中有详细的介绍和分析,本文中将不做过多探讨。
本文的重点以考察类型构造器为主,所以在此不进行过多探讨。
- 类型构造器(.cctor)
用于执行对静态成员的初始化,在.NET中,类型在两种情况下会发生对.cctor的调用:
- 为静态成员指定初始值,例如上例中只有静态成员初始化,而没有静态构造函数时,.cctor的IL代码实现为:
<span style="color:black"><span style="color:black">.method <span style="color:#0000ff">private</span> hidebysig specialname rtspecialname <span style="color:#0000ff">static</span> </span></span>
void .cctor() cil managed
<span style="color:black"><span style="color:black">{</span></span>
// Code size 11 (0xb)
<span style="color:black"><span style="color:black"> .maxstack 8</span></span>
IL_0000: ldstr "Initialize when defined."
<span style="color:black"><span style="color:black"> IL_0005: stsfld <span style="color:#0000ff">string</span> Anytao.Write.TypeInit.User::message</span></span>
IL_000a: ret
<span style="color:black"><span style="color:black">} <span style="color:#008000">// end of method User::.cctor</span></span></span>
- 实现显式的静态构造函数,例如上例中有静态构造函数存在时,将首先执行静态成员的初始化过程,再执行静态构造函数初始化过程,.cctor的IL代码实现为:
<span style="color:black"><span style="color:black">.method <span style="color:#0000ff">private</span> hidebysig specialname rtspecialname <span style="color:#0000ff">static</span> </span></span>
void .cctor() cil managed
<span style="color:black"><span style="color:black">{</span></span>
// Code size 23 (0x17)
<span style="color:black"><span style="color:black"> .maxstack 8</span></span>
IL_0000: ldstr "Initialize when defined."
<span style="color:black"><span style="color:black"> IL_0005: stsfld <span style="color:#0000ff">string</span> Anytao.Write.TypeInit.User::message</span></span>
IL_000a: nop
<span style="color:black"><span style="color:black"> IL_000b: ldstr <span style="color:#006080">"Initialize in static constructor."</span></span></span>
IL_0010: stsfld string Anytao.Write.TypeInit.User::message
<span style="color:black"><span style="color:black"> IL_0015: nop</span></span>
IL_0016: ret
<span style="color:black"><span style="color:black">} <span style="color:#008000">// end of method User::.cctor</span></span></span>
同时,我们必须明确一些静态构造函数的基本规则,包括:
- 必须为静态无参构造函数,并且一个类只能有一个。
- 只能对静态成员进行初始化。
- 静态无参构造函数可以和非静态无参构造函数共存,区别在于二者的执行时间,详见《你必须知道的.NET》7.8节 “动静之间:静态和非静态”的论述,其他更多的区别和差异也详见本节的描述。
3 深入执行过程
因为类型构造器本身的特点,在一定程度上决定了.cctor的调用时机并非是一个确定的概念。因为类型构造器都是private的,用户不能显式调用类型构造器。所以关于类型构造器的执行时机问题在.NET中主要包括两种方案:
- precise方式
- beforefieldinit方式
二者的执行差别主要体现在是否为类型实现了显式的静态构造函数,如果实现了显式的静态构造函数,则按照precise方式执行;如果没有实现显式的静态构造函数,则按照beforefieldinit方式执行。
为了说清楚类型构造器的执行情况,我们首先在概念上必须明确一个前提,那就是precise的语义明确了.cctor的调用和调用存取静态成员的时机存在精确的关系,所以换句话说,类型构造器的执行时机在语义上决定于是否显式的声明了静态构造函数,以及存取静态成员的时机,这两个因素。
我们还是从User类的实现说起,一一过招分析这两种方式的执行过程。
3.1 precise方式
首先实现显式的静态构造函数方案,为:
<span style="color:black"><span style="color:black"> <span style="color:#008000">// Release : code02, 2008/11/02 </span></span></span>
// Author : Anytao, http://www.anytao.com
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> User</span></span>
{
<span style="color:black"><span style="color:black"> <span style="color:#008000">//Explicit Constructor</span></span></span>
static User()
<span style="color:black"><span style="color:black"> {</span></span>
message = "Initialize in static constructor.";
<span style="color:black"><span style="color:black"> }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">static</span> <span style="color:#0000ff">string</span> message = <span style="color:#006080">"Initialize when defined."</span>;</span></span>
}
对应的IL代码为:
<span style="color:black"><span style="color:black">.<span style="color:#0000ff">class</span> <span style="color:#0000ff">public</span> auto ansi User</span></span>
extends [mscorlib]System.Object
<span style="color:black"><span style="color:black">{</span></span>
.method private hidebysig specialname rtspecialname static void .cctor() cil managed
<span style="color:black"><span style="color:black"> {</span></span>
.maxstack 8
<span style="color:black"><span style="color:black"> L_0000: ldstr <span style="color:#006080">"Initialize when defined."</span></span></span>
L_0005: stsfld string Anytao.Write.TypeInit.User::message
<span style="color:black"><span style="color:black"> L_000a: nop </span></span>
L_000b: ldstr "Initialize in static constructor."
<span style="color:black"><span style="color:black"> L_0010: stsfld <span style="color:#0000ff">string</span> Anytao.Write.TypeInit.User::message</span></span>
L_0015: nop
<span style="color:black"><span style="color:black"> L_0016: ret </span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
<span style="color:black"><span style="color:black"> {</span></span>
.maxstack 8
<span style="color:black"><span style="color:black"> L_0000: ldarg.0 </span></span>
L_0001: call instance void [mscorlib]System.Object::.ctor()
<span style="color:black"><span style="color:black"> L_0006: ret </span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
.field public static string message
<span style="color:black"><span style="color:black">}</span></span>
为了进行对比分析,我们需要首先分析beforefieldinit方式的执行情况,所以接着继续。。。
3.2 beforefieldinit方式
为User类型,不实现显式的静态构造函数方案,为:
<span style="color:black"><span style="color:black"> <span style="color:#008000">// Release : code03, 2008/11/02 </span></span></span>
// Author : Anytao, http://www.anytao.com
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> User</span></span>
{
<span style="color:black"><span style="color:black"> <span style="color:#008000">//Implicit Constructor</span></span></span>
public static string message = "Initialize when defined.";
<span style="color:black"><span style="color:black"> }</span></span>
对应的IL代码为:
<span style="color:black"><span style="color:black">.<span style="color:#0000ff">class</span> <span style="color:#0000ff">public</span> auto ansi beforefieldinit User</span></span>
extends [mscorlib]System.Object
<span style="color:black"><span style="color:black">{</span></span>
.method private hidebysig specialname rtspecialname static void .cctor() cil managed
<span style="color:black"><span style="color:black"> {</span></span>
.maxstack 8
<span style="color:black"><span style="color:black"> L_0000: ldstr <span style="color:#006080">"Initialize when defined."</span></span></span>
L_0005: stsfld string Anytao.Write.TypeInit.User::message
<span style="color:black"><span style="color:black"> L_000a: ret </span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
<span style="color:black"><span style="color:black"> {</span></span>
.maxstack 8
<span style="color:black"><span style="color:black"> L_0000: ldarg.0 </span></span>
L_0001: call instance void [mscorlib]System.Object::.ctor()
<span style="color:black"><span style="color:black"> L_0006: ret </span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
.field public static string message
<span style="color:black"><span style="color:black">}</span></span>
3.3 分析差别
从IL代码的执行过程而言,我们首先可以了解的是在显式和隐式实现类型构造函数的内部,除了添加新的初始化操作之外,二者的实现是基本相同的。所以要找出两种方式的差别,我们最终将着眼点锁定在二者元数据的声明上,隐式方式多了一个称为beforefieldinit标记的指令。
那么,beforefieldinit究竟表示什么样的语义呢?Scott Allen对此进行了详细的解释:beforefieldinit为CLR提供了在任何时候执行.cctor的授权,只要该方法在第一次访问类型的静态字段之前执行即可。
所以,如果对precise方式和beforefieldinit方式进行比较时,二者的差别就在于是否在元数据声明时标记了beforefieldinit指令。precise方式下,CLR必须在第一次访问该类型的静态成员或者实例成员之前执行类型构造器,也就是说必须刚好在存取静态成员或者创建实例成员之前完成类型构造器的调用;beforefieldinit方式下,CLR可以在任何时候执行类型构造器,一定程度上实现了对执行性能的优化,因此较precise方式更加高效。
值得注意的是,当有多个beforefieldinit构造器存在时,CLR无法保证这多个构造器之间的执行顺序,因此我们在实际的编码时应该尽量避免这种情况的发生。
4 回归问题,必要的小结
本文源于Artech兄的一个问题,希望通过上文的分析可以给出一点值得参考的背景。现在就关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释一文中的几个示例进行一些继续的分析:
- 在蒋兄的开始的示例实现中,可以很容易的来确定对于显式实现了静态构造函数的情况,类型构造器的调用在刚好引用静态成员之前发生,所以不管是否在Main中声明
<span style="color:black"><span style="color:black"><span style="color:#0000ff">string</span> field = Foo.Field;</span></span>
执行的结果不受影响。
- 而在没有显式实现静态构造函数的情况下,beforefieldinit优化了类型构造器的执行不在确定的时间执行,只要实在静态成员引用或者类型实例发生之前即可,所以在Debug环境下调用的时机变得不按常理。然而在Release优化模式下,beforefieldinit的执行顺序并不受
<span style="color:black"><span style="color:black"><span style="color:#0000ff">string</span> field = Foo.Field;</span></span>
的影响,完全符合beforefieldinit优化执行的语义定义。
- 关于最后一个静态成员继承情况的结果,正像本文开始描述的逻辑一样,类型构造器是在静态成员被调用或者创建实例时发生,所以示例的结果是完全遵守规范的。不过,我并不建议子类最好不要调用父类静态成员,原因是作为继承机制而言,子承父业是继承的基本规范,除了强制为private之外,所有的成员或者方法都应在子类中可见。而对于存在的潜在问题,更好的以规范来约束可能会更好。其中,静态方法一定程度上是一种结构化的实现机制,在面向对象的继承关系中,本质上就存在一定的不足。
- 在c#规范中,关于beforefieldinit的控制已经引起很多的关注和非议,一方面beforefieldinit方式可以有效的优化调用性能,但是以显式和或者隐式实现静态构造函数的方式不能更有直观的让程序开发者来控制,因此在以后版本的c#中,能实现基于特性的声明方式来控制,是值得期待的。
- 另一方面,在有两个类型的类型构造器相互引用的情况下,CLR无法保证类型构造器的调用顺序,对程序开发者而言,我同样强调了对于类型构造器而言,我们应该尽量避免要求顺序相关的业务逻辑,因为很多时候执行的顺序并非声明的顺序,这是值得关注的。
5 结论
除了补充Artech老兄的问题,本文算是继续了关于类型构造器在《你必须知道的.NET》7.8节 “动静之间:静态和非静态”中的探讨,以更全面的视角来进一步阐释这个问题。在最后,关于beforefieldinit标记引起的类型构造器调用优化的问题,虽然没有完全100%的了解在Debug模式下的CLR调用行为,但是深入细节我们可以掌控对于语言之内更多的理解,从这点而言,本文是个开始。
Worktile,新一代简单好用、体验极致的团队协同、项目管理工具,让你和你的团队随时随地一起工作。完全免费,现在就去了解一下吧。
https://worktile.com
参考文献
- 《你必须知道的.NET》7.8节 “动静之间:静态和非静态”
- Artech,关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释
- 通过七个关键编程技巧得益于静态内容
#53楼 2009-10-27 22:25 fisea
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class
Program
{
static
void
Main()
{
Console.WriteLine(
"Start ..."
);
Foo.GetString(
"Manually invoke the static GetString() method!"
);
}
}
class
Foo
{
public
static
string
Field = GetString(
"Initialize the static field!"
);
public
static
string
GetString(
string
s)
{
Console.WriteLine(s);
return
s;
}
}
的运行结果如下:
Start ...
Initialize the static field!
Manually invoke the static GetString() method!1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class
Program
{
static
void
Main()
{
Console.WriteLine(
"Main execute!"
);
Console.WriteLine(
"int: "
+ MyClass<
int
>.Time);
Thread.Sleep(3000);
Console.WriteLine(
"string: "
+ MyClass<
string
>.Time);
Console.ReadLine();
}
}
public
static
class
MyClass<T>
{
public
static
readonly
DateTime Time = GetNow();
private
static
DateTime GetNow()
{
Console.WriteLine(
"GetNow execute!"
);
return
DateTime.Now;
}
}
LZ能否回答一下53楼的问题,我也感觉很迷茫,O(∩_∩)O谢谢了~
#57楼2013-07-27 11:50 String.Trim()
[你必须知道的.NET]第二十三回:品味细节,深入.NET的类型构造器相关推荐
- [你必须知道的.NET]第二十一回:认识全面的null
<你必须知道的.NET>网站 | Anytao技术博客 [你必须知道的.NET]第二十一回:认识全面的null 发布日期:2008.7.31 作者:Anytao © 2008 Anyta ...
- [你必须知道的.NET]第二十七回:interface到底继承于object吗?
<你必须知道的.NET>网站 | Anytao技术博客 [你必须知道的.NET]第二十七回:interface到底继承于object吗? 发布日期:2009.03.05 作者:Anyta ...
- [你必须知道的.NET]第二十回:学习方法论
本文,源自我回答刚毕业朋友关于.NET学习疑惑的回复邮件. 本文,其实早计划在<你必须知道的.NET>写作之初的后记部分,但是因为个中原因未能如愿,算是补上本书的遗憾之一. 本文,作为[& ...
- 【转】[你必须知道的.NET]第二十一回:认识全面的null
引用自:http://www.cnblogs.com/anytao/category/155694.html 作者:Anytao . 说在,开篇之前 null. nullable.??运算符.null ...
- [你必须知道的.NET]第十三回:从Hello, world开始认识IL
发布日期:2007.7.22 作者:Anytao ©2007 Anytao.com ,原创作品,转贴请注明作者和出处. 本文将介绍以下内容: IL代码分析方法 Hello, world历史 .NET学 ...
- [你必须知道的.NET]第二十四回:认识元数据和IL(上)
说在,开篇之前 很早就有说说Metadata(元数据)和IL(中间语言)的想法了,一直在这篇开始才算脚踏实地的对这两个阶级兄弟投去些细关怀,虽然来得没有<第一回:恩怨情仇:is和as>那么 ...
- [你必须知道的css系列]第一回:丰富的利器2:CSS选择符之子选择符、相邻选择符...
对了,接下来要讲的属性选择符,相邻选择符,子对象选择符可能大家稍微有点陌生了,这当然也是有原因的,因为IE6及以下的浏览器并不支持这几个选择符,而 大多数从事这方面工作的技术人员,多数时候还是主要考虑 ...
- [你必须知道的.NET] 第五回:深入浅出关键字---把new说透(转载)
[你必须知道的.NET] 第五回:深入浅出关键字---把new说透 作者:Anytao 本文将介绍以下内容: 面向对象基本概念 new关键字深入浅出 对象创建的内存管理 1. 引言 园子里好像没有或者 ...
- [你必须知道的.NET]第二十八回:说说Name这回事儿
1 缘起 老赵在谈表达式树的缓存(2):由表达式树生成字符串中提到,在描述Type信息时讨论FullName或者AssemblyQualifiedName提供完整的Type信息,虽是小话题,但却是值得 ...
最新文章
- goland 报错 Main file has non-main package or doesn’t contain main function
- Selenium Webdriver原理终于搞清楚了
- bzoj千题计划128:bzoj4552: [Tjoi2016Heoi2016]排序
- [导入]连连看.NET 1.41全部源码
- 去掉字符串中的单引号和双引号_同时搞定Android和iOS的Dart语言(4):字符串类型...
- 普通视图和物化视图区别
- SAP Cloud for Customer My settings按钮被disable的原因分析
- [Luogu1891]疯狂LCM[辗转相减法]
- linux curl命令验证服务器断点续传支持
- Java Sokect编程之HTTP请求
- 新泽西州男子因无牌运营比特币交易所或面临5年监禁
- 自动驾驶即将迎来下一个飞跃?
- HALCON学习笔记 1
- 炉石兄弟 修复图腾师问题 by大神beebee102, 还有阴燃电鳗
- 迷你博客“废话”转移
- UnityWebGL引用4399的Api
- 6个Python数据分析神器~
- Linux 远程复制命令scp命令的使用
- 腾讯云CFS挂载问题
- 大数据分析对电子商务行业有哪些改变?
热门文章
- WebStorm 预览时把浏览器地址localhost 改成IP
- 简明Python3教程 16.标准库
- 关于application title一直是untitled的问题
- [小技巧][Java]Arrays.fill() 初始化 二维数组
- [Leetcode][第96题][JAVA][不同的二叉搜索树][动态规划][数学]
- [剑指offer][JAVA]面试题[51][数组中的逆序对][归并排序]
- linux tcp header更改,Linux Netfilter中修改TCP/UDP Payload的方法
- php后端mysql,【后端开发】PHP如何处理MySQL死连接
- 北京交通大学计算机系2018年录取情况,北京交通大学2018年高招录取分数线汇总...
- matlab宏参赛,MATLAB杯无人机大赛 | 决赛通知!