CLR via C#读后感2-类型基础

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类型对象是在代码编译后利用元数据创建出的数据结构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值