实例构造与引用类型
之前的章节其实已经写过了引用类型的构造过程:
首先当然是,在堆中,为引用类型的实例对象分配内存,然后初始化对象的附加字段(即类型对象指针和同步块索引)。
这个时候为对象分配的内存都是直接被置为0的,所以如果所用到的构造器中没有对对象中的一些字段做处理,那么这些字段的初始值都应该为0或者null。
如果一个类,没有构造函数,那么这个类构造的时候就会定义一个默认无参构造器,它里面就简单调用基类的无参构造器。
极少数的情况下,会有不实用实例构造器就能创建类型的实例的情况,比如MemberwiseClone方法(深复制)和反序列化对象时。(反序列化会调用GetUninitializedObject或GetSafeUninitializedObject方法为对象分配内存,还是后面讲吧。)
还记得上一章节写的,内联初始化可能会导致性能问题吧:
因为每一次内联初始化实际上都会将这些初始化字段的操作,嵌入构造函数的代码中(注意会先进行这些操作,再进行真正的构造函数的操作)。如果只有一个构造器函数,那么不会有什么影响。然而如果有多个构造器函数,那么这几个构造器函数里都会插入这段初始化字段的代码。
所以当存在多个构造器参数,而代码里又有一大堆内联初始化,那么实际生成的代码中就会有大量的冗余代码。可以用以下方法解决:
public class Troy{ //不进行内联初始化 int _a; int _b; public Troy() { //将初始化过程放在某一构造参数内,一般就是无参构造函数中 this._a = 1; this._b = 2; } public Troy(int i):this() //在其它构造函数时,调用this() { } public Troy(int i,int j) : this() { } }
实例构造与值类型
值类型其实不需要定义构造器,C#编译器也根本就不会为值类型内联嵌入默认的无参构造器。
只有嵌套在引用类型的值类型才会被初始化为0或null,如果是基于栈的值类型,那么在读取之前,被要求强制初始化,否则报错。
值类型的实例构造器只有显式调用才有用,否则其字段都会被初始化为0或null。(也就是说,即时这个struct有无参构造函数,只要你没有显示调用,那么就不会自动调用无参构造函数。实际上C#编译器也不允许你在结构体里写一个无参构造函数,毕竟这个点来说,太容易误会了)。
由于C#不允许值类型定义无参构造函数,所以值类型同样不准内联参数化。(静态字段可以内联初始化,因为是在类型对象里面,而不是实例对象)
且值类型的任何构造函数在初始化的时候,必须对值类型的所有字段都赋值。
对于这么麻烦的设定,当然也有解决的办法:
public struct Troy { public int a; public int b; public Troy(int i) { this = new Troy();//先初始化所有的字段都为0或null //初始化自己想玩的字段 a = i; } }
(我不得不吐槽,我刚刚用VS自己写了个值参数初始化的小例子,然后被360给当做病毒删掉了。)
关于类型构造器
首先要了解到,类型构造器实际上就是构造CLR分配内存中的类型对象初始化时用的。
类型构造器是不允许有参数的,当然也就只能定义一个类型构造函数。
实际上类型构造器必须是私有的,甚至不允许显式写上private修饰符,这样做正是为了防止开发人员调用。它的调用总是由CLR负责的。
简单样例:
class Program { static void Main(string[] args) { Troy obj = new Troy(); } } public class Troy { static Troy() { Console.WriteLine("我就问你6不6?"); } }
构造过程:
JIT编译器编译一个方法时,会查看代码引用了哪些类型。任何一个类型定义了类型构造器,那么JIT编译器就会检查针对当前AppDomain是否执行了这个类型构造器。是就不调用,否就调用。因为CLR希望在每个AppDomain中一个类型构造器只执行一次,所以为了不使多个线程同时调用类型构造器,在第一个调用类型构造器的线程调用时,会获取一个互斥线程同步锁。这样一来,就只有一个线程可以调用了,后面的线程要用的时候发现已经调用过了,就不会再调用类型构造器了。(因为类型构造器线程安全,所以很适合在里面初始化任何单例对象。)
虽然可以在值类型中定义类型构造函数,然而实际上因为值类型根本就不会在堆中有类型对象,所以自然里面的代码都不会被调用。
关于操作符重载
实际上CLR对操作符重载一无所知,因为这是编程语言的语法。
当C#这种语言写的操作符重载语句被编译成IL代码时,其实已经变成了一个带有specialname标志的函数。
当编译器看到有+这种操作符时,就会看几个操作数的类型中是否有定义了名为op_Addition这个函数(被编译后的真正的函数名),而且该方法参数兼容于操作数的类型。
所以操作符重载函数中,一定要有一个参数的类型与定于这个重载方法的类型相同:
public class Troy { public static int operator +(Troy a, Troy b) { return 10; } }
关于转换操作符方法
class Program { static void Main(string[] args) { Troy obj = 3;//隐式转换成功 string a = obj;//由于是显示转换重载,所以这种写法会编译不过 string a =(String)obj;//显示转换成功 } } public class Troy { // 隐式转换操作符implicit重载 public static implicit operator Troy(Int32 num) { return new Troy(); } // 显式转换操作符explicit重载 public static explicit operator String(Troy troy) { return "怎么转都是我"; } }
和一般的+-这种操作符重载一样,实际上生成的IL代码中,换了一个名字,前缀加上了op_。
当C#编译器检测到代码中一个对象期望得到另一个类型不同的对象时,就回去找这两个类型中是否定义了隐式转换的op_Implicit方法,是就转。显示类似。
可以参考Decimal类的定义去理解。
关于扩展方法
先说一下我自己的认识吧,其实我不建议使用这个东西。
因为写得不规范的扩展方法,会增加了代码的阅读难度,增加维护成本。(我真的很确定有的人会把这个东西写得到处都是)
这个东西之前在写《重构》的学习笔记中提到过,主要用于解决别人封装的类库,没法增加自己想要的函数。
简单来讲,还是慎用,自己写的类就别用扩展方法。
另外扩展方法必须是顶级静态类中定义的静态方法,如果是嵌套类中的话,编译会出错。
实际上扩展方法在C#编译器编译过后也只是个一般的静态对象里的静态函数,只不过加了个[Extension]的特性。然而实际上这个ExtensionAttribute特性还不能在代码中用,都是C#编译器去自动生成的。
关于分部方法
分部方法和分部类很像,不过是方法前面加上partial修饰符。
这样的话,如果其它分部类实现了这个方法,那么就会加上这个方法,如果没有实现,那么这条代码在编译的时候就会被忽略。
但是分部方法只能在分部类和结构中用,且返回类型总是void,任何参数都不能用out来修饰。之所以会这样限制,是因为方法在运行时可能就并不存在,所以也就不会有返回。
分部方法总是private的,但是C#编译器禁止将private修饰符显式写在分部方法前面。(和类型构造器在这个点上类似)