一、实例构造器和类(引用类型)

  类实例构造器是允许将类型的实例初始化为良好状态的一种特殊的方法。

  类实例构造器方法在"方法定义元数据表"中始终叫.ctor(代表constructor)。创建一个引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始化状态。

  构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。构造器将没有显式重写的所有字段保证都有一个0或null值。
 
  与其他方法不同,实例构造器永远不能被继承。也就是说,类只有类自己定义的实例构造器。
 
  如果你定义的类没有显式定义任何构造器,C#编译器将定义一个默认(无参)构造器。在它的实现中,只能简单地调用了基类的无参构造器。
public class SomeType {
}

它等下于以下的代码:

piblic class SomeType {
public SomeType () : base () { }
}

  

  如果类的修饰符是abstract,那么编译器生成的默认构造函数的可访问性就是protected;否则,构造器就被赋予public可访问性。

  如果基类没有提供默认的无参构造函数,那么派生类必须显示调用一个基类构造器,否则编译器会报错。

  如果类的修饰符为static,那么编译器根本不会再类的定义中生成默认的实例构造器。

  一个类可以定义多个实例构造器。每个构造器都必须有一个不同的签名,而且都可以有不同的可访问性。
 
  为了使代码"可验证"(verifiable),类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。
 
  在极少数情况下,可以在不调用实例构造器的前提下创建一个类型的实例。比如Object的MemberciseClone方法。
 
  C#语言提供了一个简单的语法,允许在构造引用类型的一个实例时,对类型中定义的字段进行初始化:
Internal sealed class SomeType {
private Int32 m_x =;
}
  我们来看下生成的IL代码吧,着重看下.ctor方法中的代码!
  明显可以看出,实例构造器把值5存到字段m_x,再调用基类的构造器。也就是说,C#编译器提供了一种简化的语法,允许以"内联"的方式初始化实例字段。编译器帮助我们调用构造器方法来执行初始化,但是我们要注意代码的"膨胀效应",如下类定义:
internal sealed class SomeType {
private Int32 m_x = ;
private String m_s = "Hi there";
private Double m_d = 3.14159;
private Byte m_b; // Here are some constructors.
public SomeType() { /* ... */ }
public SomeType(Int32 x) { /* ... */ }
public SomeType(String s) { /* ...; */ m_d = ; }
}
  使用反编译器会查看到有三个构造器.ctor方法。
  
  我们在看下每个构造器方法的IL代码。
  .ctor()
  
  .ctor(Int32)
  
  .ctor(String)
  
  有何发现呢?
  编译器为这三个构造器方法生成代码时,在每个方法的开始位置,都会包含用于初始化m_x,m_s和m_d的代码。在这些初始化代码之后,编译器会插入对基类构造器的调用。再然后,会插入构造器自己的代码。
  上面类中有三个构造器,所以编译器就生成的三次初始化的m_x,m_s和m_d的代码,由于两方面都会增多,所有就会造成"代码膨胀"的问题。
  那如何解决呢?
  可以考虑不是在定义字段的时候初始化,而是创建单个构造器来执行这些公共的初始化。然后,让其他构造器显示调用这个公共初始化构造器。
internal sealed class SomeType
{
// 不显示初始化下面的字段
private Int32 m_x;
private String m_s;
private Double m_d;
private Byte m_b; // 该构造器将所有的字段都设为默认值
// 其它所有构造器显式调用这个构造器
public SomeType()
{
m_x = ;
m_s = "Hi there";
m_d = 3.14159;
m_b = 0xff;
} // 该构造器将所有的字段都设为默认值,然后修改m_x
public SomeType(Int32 x)
: this()
{
m_x = x;
} // 该构造器将所有的字段都设为默认值,然后修改m_x
public SomeType(String s)
: this()
{
m_s = s;
} // 该构造器将所有的字段都设为默认值,然后修改m_x和m_s
public SomeType(Int32 x, String s)
: this()
{
m_x = x;
m_s = s;
}
}

二、实例构造器和结构(值类型)

  值类型(struct)构造器的工作方式与引用类型(class)的构造器截然不同。CLR总是允许创建值类型的实例,并且没有办法阻止值类型的实例化。所以,值类型其实并不需要定义构造器,C#编译器根本不会为值类型生成默认的无参构造器。如以下代码:

internal struct Point{
pblic Int32 m_x, m_y;
}
internal sealed class Rectangle {
public Point m_topLeft, m_bottomRight;
}

  为了构造一个类Rectangle,必须使用new操作符,而且必须指定一个构造器。在这个例子中,调用的是C#自动生成的默认实例构造器,为Rectangle 分配内存时,内存中包含Point值类型的两个实例。处于对性能的考虑,CLR不会为包含在引用类型中的每个值类型字段都调用一个构造器。但是,如前面提到的,值类型的字段会被初始化为0或null。

  CLR是允许为值类型定义构造器。但执行这种构造器的唯一方式就是写代码来显示地调用它们。如下:
internal struct Point {
// 在C#中,向一个值类型应用关键字new,
// 可以调用构造器来初始化值类型的字段。
public Int32 m_x, m_y;
public Point(Inte32 x, Int32 y) {
m_x = x;
m_y = y;
}
} internal sealed class Rectangle {
public Point m_topLeft, m_bottomRight; public Rectangle() {
m_topLeft = new Point (, );
m_bottomRight = new Point (, );
}
}

  好了,让我们来改变下上面的没有定义默认的无参构造器。

internal struct Point {
public Int32 m_x, m_y;
public Point() {
m_x =m_y = ;
}
} internal sealed class Rectangle {
public Point m_topLeft, m_bottomRight; public Rectangle() { }
}

  现在我们来想象,m_x和m_y值是多少呢?是5吗?还是0?

  正确答案是0,为什么呢?因为代码中没有任何地方显式调用过Point的构造器,即使值类型提供了无参构造器。更明显的是,C#编译器会报错:error CS0568:结构不能包含显式地无参构造器。
 
  C#编译器故意不允许值类型带有无参构造器,旨在避免开发人员对这种构造器在什么时候调用产生迷茫。没有无参构造器,值类型的字段总是被初始化为0或null。
 
  严格的说,只有当值类型的字段嵌套到引用类型中,才保证会被初始化0或null。基于栈的值类型字段不保证为0或null。
 
  注意,虽然C#编译器不允许值类型带有无参构造器,但是CLR允许。你可以使用其他语言(如IL汇编语言)定义带有无参构造器的值类型。
 
  由于C#是不允许为值类型定义无参构造器,所以编译一下类型是,会报错。
internal struct SomeValType {
//不能再值类型中内联实例字段的初始化
private Int32 m_x = ;
}
 
  为了生成"可验证"的代码,在访问值类型的任何一个字段前,都需要对全部字段进行赋值,所以,值类型的任何构造器必须初始化值类型的全部字段。
internal struct SomeValType {
private Int32 m_x,m_y; //C#允许为值类型定义有参构造器
public SomeValType(Int32 x){
m_x = x;
}
}
  编译这段代码时,C#编译器会报错,说字段"SomeValType.m_y"必须赋值。
  为了修正这个问题,需要在构造器中为y赋一个值(通常为0)。
  下面是对值类型的全部字段进行赋值的一个替代方案:
//C#允许为之类定义有参构造器
public SomeValType(Int32 x){
//this代表本身实例,使用new操作符会将所有的字段初始化为0/null
this = new SomeValType();
//使用x覆盖m_x的0
m_x = x;
//现在y已经初始化为0了
}

  注意,在引用类型的构造器中,this被认为是只读的,所以不能对它赋值。

三、类型构造器

  除了实例构造器,CLR还支持类型构造器(type constructor),也称为静态构造器(static constructor)、类构造器(class constructor)等。

  类型构造器可用于接口(C#不允许)、引用类型和值类型。
 
  实例构造器的作用是设置类型的实例的初始状态。类型构造器的作用是设置类型的初始状态。
  类型默认没有定义类型构造器,如果定义,也只能定义一个,此外,类型构造器永远没有参数。
  比如:
internal sealed class SomeRefType {
static SomeRefType () {
//SomeRefType 被首次访问时,执行这里的代码。
}
}
internal struct SomeValType {
//C#允许值类型定义无参构造器
static SomeValType () {
//SomeValType 被首次访问时,执行这里的代码。
}
}

  类型构造器总是私有的,C#编译器会自动把它们标记成private。之所以私有,是为了阻止任何有开发人员写的代码调用它,对它的调用总是由CLR负责的。

  类型构造器的调用比较麻烦,JIT编译器在编译一个方法时,会检查代码中都引用了那些类型。任何一个类型定义了类型构造器,JIT编译器都会检查针对当前AppDomain,是否已经执行了这个类型构造器。CLR总要确保一个类型构造器只执行一次。为了保证这一点,在调用类型构造器时,调用线程要获取一个互斥线程同步锁。这样一来,如果多个线程试图调用某一个类型的类型构造器,只有一个线程可以获得锁,其他线程会被阻塞。第一个线程会执行静态构造器中的代码。当第一个线程离开构造器后,正在等待的线程将被唤醒,然后发现构造器中的代码已经被执行过。因此,这些线程不会再次执行代码,将直接从构造器方法返回。另外,如果再次调用这样的一个方法,CLR知道该方法所在类型的类型构造器已经被执行过了,从而确保构造器不会再被调用。
 
  虽然在值类型中能定义一个类型构造器,但不要这么做,因为CLR有时不会调用值类型的类型构造器。
 
  由于CLR保证了一个类型构造器在每个AppDomian中只执行一次,而且这种执行时线程安全的,所以非常适合在类型构造器中初始化类型需要的任何单实例(Singleton)对象。
 
  类型构造器中的代码只能访问类型的静态字段,并且它的常规用途就是初始化这些字段。和实例构造器一样,C#提供了简单的原发来初始化类型的静态字段。如;
internal sealed class SomeType{
public static Int32 s_x = ;
}

  虽然C#不允许值类型为它的实例字段使用内联字段初始化语法,但可以为静态字段使用。

  类型初始化的性能:
  在编译一个方法时,JIT编译器要决定是否在方法中生成一个对类型构造器的调用。如果JIT编译器决定生成这个调用,它还必须决定将这个调用添加到什么位置。具体什么位置,有以下两种可能:
    1)JIT编译器可以刚好在创建类型的第一个实例前,或者刚好在访问类的一个非继承的字段或成员之前生成这个调用。这称为"精确"(precise)语义,因为CLR调用类型构造器的时机拿捏恰到好处。
    2)JIT编译器可能在首次访问一个静态字段或一个静态/实例方法之前,或者在调用一个实例构造器之前,随便找个时间生成。这称为"字段初始化前"(before-field-init)语义,因为CLR只保证访问成员之前会运行类型构造器,可能提前很早就允许了。
  "字段初始化前"语义是首选的,因为它是CLR能够自由选择调用类型构造器的时间,而CLR会尽可能地利用这一点来生成运行得更快的代码。
  默认情况下,语言的编译器会选择对你定义的类型来说最恰当的一种语义,并在类型定义元数据表的行中设置beforefieldinit标识,从而告诉CLR这个选择。
  现在重点关注下C#编译器具体如何让选择,以及这些选择会对性能产生什么样的影响,如下代码:
public sealed class TypeConstructorPerformance {
public static void Go() {
const Int32 iterations = * * ;
PerfTest1(iterations);
PerfTest2(iterations);
} // 由于这个类没有显示定义类型构造器,所以C#在元数据中
// 用BeforeFieldInit来标记类型定义
internal sealed class BeforeFieldInit {
public static Int32 s_x = ;
} // 由于这个类显示定义了类型构造器,所以C#在元数据中
// 没有用BeforeFieldInit来标记类型定义
internal sealed class Precise {
public static Int32 s_x;
static Precise() { s_x = ; }
} // 这个方法被JIT编译时, BeforeFieldInit 和Precise 类
// 的类型构造器还没有被执行,所以这些构造器的调用将嵌入
// 这个方法的代码中,使它允许较慢
private static void PerfTest1(Int32 iterations) {
Stopwatch sw = Stopwatch.StartNew();
for (Int32 x = ; x < iterations; x++) {
// JIT编译器优化调用BeforeFieldInit 的
// 类型构造器的代码,是他在循环开始之前执行
BeforeFieldInit.s_x = ;
}
Console.WriteLine("PerfTest1: {0} BeforeFieldInit", sw.Elapsed); sw = Stopwatch.StartNew();
for (Int32 x = ; x < iterations; x++) {
// JIT编译器在这里生成调用Precise 类的类型构造器的代码
// 所以每次循环迭代,它都要核实一遍是否需要调用构造器
Precise.s_x = ;
}
Console.WriteLine("PerfTest1: {0} Precise", sw.Elapsed);
} // 这个方法被JIT编译时, BeforeFieldInit 和Precise 类的
// 类型构造器已经执行过了。所以,在这个方法的代码中,不会
// 在对这些构造器的调用,是它运行得更快
private static void PerfTest2(Int32 iterations) {
Stopwatch sw = Stopwatch.StartNew();
for (Int32 x = ; x < iterations; x++) {
BeforeFieldInit.s_x = ;
}
Console.WriteLine("PerfTest2: {0} BeforeFieldInit", sw.Elapsed); sw = Stopwatch.StartNew();
for (Int32 x = ; x < iterations; x++) {
Precise.s_x = ;
}
Console.WriteLine("PerfTest2: {0} Precise", sw.Elapsed);
}
}
  运行上述代码的结果
  
  C#编译器如果看到一个类(BeforeFieldInit)包含进行了内联初始化的静态字段,就会在类的类型定义表中生成一个添加了BeforeFieldInit元数据标记的记录项。
  C#编译器如果看到一个类(Precise)包含显式地类型构造器,就不会添加BeforeFieldInit元数据标记。
  它的基本原理是:静态字段只要在访问之前初始化就可以了,具体什么时间无所谓。而显式类型构造器中包含可能具有副作用的代码,所以需要精确拿捏运行的时间。
 
 四、扩展方法
  扩展方法的原则:
  1)C#只支持扩展方法,不支持扩展属性、扩展事件、扩展操作符等。
  2)扩展方法(第一个参数前面有this的方法)必须在非泛型的静态类中声明。然而,类名没声明限制。扩展方法至少要有一个参数,而且只有第一个参数能用this关键字标记
  3)C#编译器查找静态类中定义的扩展方法时,要求这些具体文件作用域。也就是说,该静态类不能嵌套在另一个类中。
  4)因为定义扩展方法的静态类可以取任意名,所在在使用时,必须在相同命名空间下或引用该静态类所在的命名空间。
  5)扩展方法有潜在的版本控制问题
  在C#中,一旦用this关键字标记了某个静态方法的第一个参数,编译器就会在内部向该方法应用一个定制的attribute。这个attribute会在最终生成的文件的元数据中持久性地存储下来。这个attribute是在System.Core.dll程序集中定义的。只要在方法中定义了扩展方法,在类和程序集上都会被标注attribute。这样可以更快的找到和编译。
 
五、分部方法
  分部方法的原则;
  1)它们只能在分部类或结构中声明
  2)分部方法的返回值始终是void,任何参数都不能用out修饰符来标记。之所以有这两个限制,因为分部方法在运行时可能不存在,所以不能将一个变量初始化为方法也许会返回的东西。同样的,不能有out参数,是因为方法必须初始化它,而这个方法可能不存在。分部方法可以使用ref参数,可以是泛型方法,可以是实例或静态方法。
  3)分部方法的声明和实现必须具有安全一致的签名。如果两者都引用了attribute,编译器会将两个方法的attitude合并到一起。应用于参数的任何attribute也会合并。
  4)如果没有对应的实现部分,便不能在代码中创建一个委托来引用这个分部方法。
  5)分部方法总是被视为private方法。但是,C#禁止你在分部方法声明前添加private关键字。
 

[CLR via C#]8. 方法的更多相关文章

  1. MSSQLSERVER添加c# clr程序集的使用方法

    前言 MSSQLSERVER提供程序集,无疑可以让编程人员更加便捷的操作数据库数据,比如c#写的函数,可以在数据库当作sql的函数使用,你想想他对不熟悉数据库的程序员来说是有多么的嗨.这么好的机制,大 ...

  2. 【Clr in c#】方法

    1. 引用类型(class)与值类型(strust)的构造函数(实例构造器) 1,  创建一个引用类型的实例时,首先为实例的数据字段分配内存,然后初始对象的附加字段,最后调用实例构造器来设置对象的初始 ...

  3. CLR如何调用虚方法、属性和事件

    方法代表在类型或类型的实例上执行某些操作的代码.在类型上执行操作,称为静态方法:在类型的实例上执行操作,称为非静态方法.任何方法都有一个名称.一个签名和一个返回值(可以是void). CLR允许一个类 ...

  4. CLR类型设计之方法与构造器

    无论学习那门语言都要学习函数体,C#,JAVA,PHP,都会涉及到函数体,而C#的函数体成员并不少,方法和构造器就是函数体成员之一,函数体成员还包括但不限于:方法,属性,构造器,终结器,运算符及索引器 ...

  5. 重温CLR(六)方法和参数

    实例构造器和类(引用类型) 构造器是将类型的实例初始化为良好状态的特殊方法.构造器方法在“方法定义元数据表”中始终叫做.ctor(constructor的简称).创建引用类型的实例时,首先为实例的数据 ...

  6. <NET CLR via c# 第4版>笔记 第8章 方法

    8.1 实例构造器和类(引用类型) 构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零 .没有被构造器显式重写的所有字段都保证获得 0 或 null 值. 构造器不能被继承 ...

  7. ASP.NET Core 中文文档 第二章 指南(4.10)检查自动生成的Detail方法和Delete方法

    原文 Examining the Details and Delete methods 作者 Rick Anderson 翻译 谢炀(Kiler) 校对 许登洋(Seay).姚阿勇(Mr.Yao) 打 ...

  8. CLR via C# 摘要一:托管程序的执行模型

    托管程序的执行模型大致如下: 编译源代码为程序集(dll或exe文件),程序集包括了记录相关信息的元数据和IL代码 执行程序集文件时,启动CLR,JIT负责把IL编译为本地代码并执行 IL是微软推出的 ...

  9. Clr Via C#读书笔记---垃圾回收机制

    #1 垃圾回收平台的基本工作原理: 访问一个资源所需的具体步骤: 1)调用IL指令newobj,为代表资源的类型分配内存.在C#中使用new操作符,编译器就会自动生成该指令.2)初始化内存,设置资源的 ...

随机推荐

  1. highcharts插件使用总结和开发中遇到的问题及解决办法

    这里使用的highchart是2014-01-09从官网下载的版本,版本号是3.0.8, 当过了几天后,发现版本号变成了3.0.9,不由得的感叹highchart的版本更新之快. 在jsp中使用hig ...

  2. jQuery File Upload跨域上传

    最近在做一个一手粮互联网项目,方案为前后端分离,自己负责前端框架,采用了Requirejs+avalonjs+jquery三个框架完成. 前后端通过跨域实现接口调用,中间也发现了不少问题,尤其是在富文 ...

  3. websocket for python

    https://github.com/aaugustin/websockets server.py #!/usr/bin/env python import asyncioimport websock ...

  4. [Core Javascirpt] Basic Metaprogramming: Dynamic Method

    Somehow it looks like reflect in Java. For example: We define an mothod on the Object, it called def ...

  5. 替换GitBlit的证书为域证书

    GitBlit(当前版本1.6.2,http://gitblit.org/) 是一个Git版本控制的服务端,使用java编写,功能上足够满足基本的版本控制要求,而且部署很简单方便,比如windows上 ...

  6. Redis和Memcached的区别

    From: https://www.biaodianfu.com/redis-vs-memcached.html Redis的作者Salvatore Sanfilippo曾经对这两种基于内存的数据存储 ...

  7. 同时使用Twitter nlp 和stanford parser的解决方法

    因为Twitter nlp中使用了较老版本的stanford parser,导致不能同时使用 解决方法是使用未集成其它jar包的Twitter nlp,关于这点Stanford FAQ中也有说明(在F ...

  8. linux_脚本应用

    linux下三个有用的 Python 脚本 2010年4月29日   import os, sys  def pyc_clean(dir):      findcmd = 'find %s -name ...

  9. Android View自动生成插件

    在ButterKnife这样强大的注入库出来之后,使用注入进行UI开发已经非常普遍.但考虑到效率.学习成本等问题,findViewById方式仍然是不错的选择. 但是当页面UI变得复杂后,我们从Lay ...

  10. ListView具有多种item布局——实现微信对话列

    这篇文章的效果也是大家常见的,各种通讯应用的对话列表都是这种方式,像微信.whatsapp.易信.米聊等.我们这篇文章也权当为回忆,形成简单的笔记.这篇文章参考了2009年Google IO中的< ...