【C#进阶系列】08 方法

实例构造与引用类型

之前的章节其实已经写过了引用类型的构造过程:

首先当然是,在堆中,为引用类型的实例对象分配内存,然后初始化对象的附加字段(即类型对象指针和同步块索引)。

这个时候为对象分配的内存都是直接被置为0的,所以如果所用到的构造器中没有对对象中的一些字段做处理,那么这些字段的初始值都应该为0或者null。

如果一个类,没有构造函数,那么这个类构造的时候就会定义一个默认无参构造器,它里面就简单调用基类的无参构造器。

极少数的情况下,会有不实用实例构造器就能创建类型的实例的情况,比如MemberwiseClone方法(深复制)和反序列化对象时。(反序列化会调用GetUninitializedObject或GetSafeUninitializedObject方法为对象分配内存,还是后面讲吧。)

还记得上一章节写的,内联初始化可能会导致性能问题吧:

因为每一次内联初始化实际上都会将这些初始化字段的操作,嵌入构造函数的代码中(注意会先进行这些操作,再进行真正的构造函数的操作)。如果只有一个构造器函数,那么不会有什么影响。然而如果有多个构造器函数,那么这几个构造器函数里都会插入这段初始化字段的代码。

所以当存在多个构造器参数,而代码里又有一大堆内联初始化,那么实际生成的代码中就会有大量的冗余代码。可以用以下方法解决:

  public class Troy{
//不进行内联初始化
int _a;
int _b;
public Troy() {
//将初始化过程放在某一构造参数内,一般就是无参构造函数中
this._a = ;
this._b = ;
}
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 ;
}
}

关于转换操作符方法

  class Program
{
static void Main(string[] args)
{
Troy obj = ;//隐式转换成功
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类的定义去理解。

关于扩展方法

先说一下我自己的认识吧,其实我不建议使用这个东西。

因为写得不规范的扩展方法,会增加了代码的阅读难度,增加维护成本。(我真的很确定有的人会把这个东西写得到处都是)

这个东西之前在写《重构》的学习笔记中提到过,主要用于解决别人封装的类库,没法增加自己想要的函数。

第19点.不完美的类库讲到的

简单来讲,还是慎用,自己写的类就别用扩展方法。

另外扩展方法必须是*静态类中定义的静态方法,如果是嵌套类中的话,编译会出错。

实际上扩展方法在C#编译器编译过后也只是个一般的静态对象里的静态函数,只不过加了个[Extension]的特性。然而实际上这个ExtensionAttribute特性还不能在代码中用,都是C#编译器去自动生成的。

关于分部方法

分部方法和分部类很像,不过是方法前面加上partial修饰符。

这样的话,如果其它分部类实现了这个方法,那么就会加上这个方法,如果没有实现,那么这条代码在编译的时候就会被忽略。

但是分部方法只能在分部类和结构中用,且返回类型总是void,任何参数都不能用out来修饰。之所以会这样限制,是因为方法在运行时可能就并不存在,所以也就不会有返回。

分部方法总是private的,但是C#编译器禁止将private修饰符显式写在分部方法前面。(和类型构造器在这个点上类似)

上一篇:大部分人都会做错的经典JS闭包面试题


下一篇:Tomcat服务器顶层结构和启动过程【转】