CLR要求所有的类型都从System.Object派生,也就是说Object是所有类型的父类。这样就标明所有类型都应该具有Object类的以下方法。
Equal:两个对象如果有相同的值,就返回True。
GetHashCode:返回对象值的一个哈希码。
ToString:该方法默认返回对象完整名,即this.GetType().FullName,我们经常重写该方法,使返回String对象。
GetType:返回一个对象的实例,指出调用GetType对象的类型。
CLR要求所有对象都用new操作符来创建:Employee e = new Employee();
以下是new操作符所做的事情:
1.计算该类型及基类型(一直到System.Object)中定义的所有实例字段需要的字节数。堆(Heap)上的每一个对象都需要一些额外成员-即“类型对象指针”和“同步索引块”。这些成员由CLR用于管理对象。这些额外成员的字节数会计入对象大小。
2.它从托管堆中分配指定类型要求的字节数,从而分配对象的内存,分配的所有字节都设为零(0)。
3.初始化对象的“类型对象指针”和“同步索引块”。
4.调用构造函数,传入new操作时指定的实参(可有可无)。
new在执行了以上的操作后,会返回一个指向新建对象的引用(或指针),上例中该引用地址保存在e中,e具有Employee类型。
另外,在.NET中没有与new操作对应的delete操作,也就是无法显示的释放一个对象的内存空间。CLR采用的垃圾回收机制,能自动检测到一个对象不再使用或访问时,自动释放对象的内存。
CLR最重要的一个特点就是类型安全。调用对象的GetType()方法就可以知道该对象的具体类型,所以一个类型不可能伪装成另一个类型。开发人员经常需要进行类型间的转换。C#默认不用任何特殊语法即可将子类对象转换为父类对象(隐式转换,因为该操作被认为是安全的),然而将一个对象转换为其派生对象(父类对象转换为子类对象)时,C#就要求需要显示转换,因为这样转换可能存在失败。
以下代码演示了基类(父类)和派生类(子类)之间的转换:
当在方法中进行传参时,CLR会自动检查类型是否匹配,如果构造方法时参数传入是(Object o),传入任何类,虽然编译时不会有问题,但当真正运行时,传入的参数类型与方法内部需要的不是同一个类型时,就会抛出一个InvalidCastException异常。这就是CLR的自动检查类型是否匹配,它不允许一个类型伪装成另一种类型,否则会破坏程序的安全性和健壮性,所以正确的写法是传入确定的类型参数。
使用C#的as和is操作符来转型。
is操作符用来检查一个对象是否兼容于指定的类型(是否类型匹配),并返回一个Boolean值。
Object o = new Object();
Boolean b1 = (o is Object); //b1=true;
Boolean b2 = (o is Employee); //b2=false;
如果引用对象是null,is操作会返回false,因为没有可检查其类型的对象。is操作符永远不会抛出异常。is操作符通常这么使用:
if(o is Employee)
{
Employee e = (Employee) o;
...//剩余的操作用对象o
}
上述代码中,CLR会检查两次对象类型,第一次is操作符先检查o是否兼容于类型Employee,如果true,转入if语句内部,进行类型转换的时候,CLR第二次核实o是否引用一个Employee。CLR的类型检查增强了安全性,但无疑损失了一定性能。这是因为CLR必须先判断变量o引用对象的实际类型,然后CLR必须遍历继承层次结构,用每个基类型去核对指定的类型(Employee)。所以C#提供了一个as操作符,目的就是简化类型转换的写法,并且提升性能。
Employee e = o as Employee;
if(e != null)
{
//使用e进行操作
}
在这段代码中,CLR只会检查o是否兼容Employee类型,如果是,返回对同一个对象的非null引用。如果o不兼容,as操作符就会返回null,而if只需要判断e是否为null,这就使程序快很多,因为只有as操作符进行一次对象的类型检验。
as操作符的工作方式与强制转换是一样的,只是它永远不会抛出异常,如果对象不能转换,就返回一个null。所以正确的做法是操作前检查最终生成的引用是否为null。否则试图引用时会抛出一个NullReferenceException的异常。
内存栈(Stack)上的操作:
CLR加载一个Windows进程后。在这个进程中,可能有多个线程,一个线程创建时,还会有1MB大小的线程栈。这个栈的空间用于向方法传递实参,并用于方法内部定义的局部变量。栈是从高位地址向低位地址构建的,一个方法执行时,为局部变量与新调用方法参数按顺序在线程栈上申请地址,并且压入栈,新调用方法时,还要压入返回地址,
CLR运行时堆(Heap)与栈(Stack)上的操作:
假设有以下两个类:
现在调用方法M3
当JIT编译器将M3的IL代码转换为本地CPU指令时,会注意到M3内部引用的所有类型:Employee,Int32,Manager以及String(因为"Joe")。这时候,CLR要确保定义了这些类型的应用程序集已加载。然后,利用程序集元数据,CLR提取与这些类型有关的信息,并创建数据结构来表示类型本身。下图表示了Employee和Manager类型对象使用的数据结构:
前面讲过,堆上所有对象都包含两个额外成员,“类型对象指针”和“同步块索引”,数据结构表中还保存了静态字段以及类型定义的方法,现在,当CLR确定方法需要的所有类型对象都已经创建,而且M3的代码已经编译好后,就允许线程开始执行M3的本地代码。M3的“序幕”代码先执行做一些准备工作,在线程栈(Stack)中为局部变量分配内存,并自动将局部变量初始化为0(值型)或null(引用型)。然后,M3执行他的代码构造一个Manager对象。这造成在托管堆中创建Manager类型的一个实例(对象),并返回内存地址,保存在变量e上。
如下图4-9:
接着调用对象的方法:
注意:堆中的对象与类型对象是两个概念,Manager对象是new操作符创建出的类的实例,在内存中的地址保存在变量e上,而Manager类型对象是在代码编译后利用元数据创建出的数据结构。