类型基础
所有类型都从System.Object派生
CLR要求所有对象都用new 操作符来创建。
Employee e = new Employee("Constructor Parameters");
以下是 new 操作符所做的事情:
#1, 计算类型及所有基类型(一直到System.Object, 虽然它没有定义自己的实例字段)中定义的所有实例字段需要的字节数。
堆上的每个对象还需要一些额外(overhead 开销成员)的成员 -- 即“类型对象指针”(type object pointer)和“同步块索引”(sync block index)。这些成员由CLR用于管理对象。这些额外成员的字节数会计入对象大小。
#2, 它从托管堆中分配指定类型要求的字节数,从而分配对象的内存,分配的所有字节都设为0。
#3, 它初始化对象的“类型对象指针”和“同步块索引”成员。
#4, 调用类型的实例构造器,向其传入在对new 的调用中指定的任何实参。大多数编译器都在构造器中自动生成代码来调用一个基类构造器。每个类型的构造器在调用时,都要负责初始化由这个类型定义的实例字段。最总调用的是System.Object的构造器,该构造器只是简单地返回,不会做其他任何事情。
new执行了所有操作之后,会返回指向新建对象一个引用(或指针)。在前面的示例代码中,这个引用会保存到变量e中,后者具有Employee类型。
类的“新实例”和“实例成员”:两种不同的“实例”。一种是类的实例,也就是具体的对象。另一种是类中定义的实例字段。所谓“实例字段”,就是指非静态字段,有时也称为“实例成员”。简单地说,实例成员是属于类的对象的,
而静态成员是属于类的。
类型安全
CLR总是知道一个对象(某个类型的实例)是什么类型。所有表达式都解析成某个类型的实例,在编译器生成的代码中,只会执行对这个类型来说有效的操作。与非类型安全的语言相比,类型安全的语言的优势在于:程序员会犯的许多错误能在编译时检测到,确保代码在你的尝试执行它之前是正确的。除此之外,编译时语言通常能生成更小、更快的代码,因为他们能在编译时进行更多的假设,并在生成的IL和元数据中落实那些假设。
类型转换
CLR最重要的特性之一就是类型安全性。在运行时,CLR总是知道一个对象是什么类型。调用GetType() 方法,总是知道一个对象确切的类型是什么。
开发中,开发人员会经常将一个对象从一种类型转换为其他各种类型。CLR允许将一个对象转换为它的(实际)类型或者它的任何基类型。
#1: 向基类型的转换被认为是一种安全的隐式转换。
#2: 将对象转换为它的某个派生类型时,C#要求只能进行显式转换,因为这样的转换有可能在运行时失败。在运行时,CLR检查类型操作,确定总是转换为对象的实际类型或者它的任何基类型。
这就是类型安全的设计。如果CLR允许这样的转型,就无类型安全性可言了,将出现难以预料的结果 -- 其中包括应用程序崩溃,以及安全漏洞的出现(因为一种类型能轻松地伪装成另一种类型)。
类型伪装是许多安全漏洞的根源,它还会破坏应用程序的稳定性和健壮性。类型安全是CLR一个重要的目标。
is 操作符,检查一个对象是否兼容于指定的类型,并返回一个Boolean值;true或false。注意 is 操作符永远不会抛出异常。如果对象引用是null,is操作符总是返回false。
Object o = new Object();
if (o is Employee)
{
Employee e = (Employee)o;
}
这段代码中,CLR实际上会检查两次对象的类型。
CLR 的类型检查增强了安全性,但无疑也会对性能造成一定影响。因为CLR首先必须判断变量引用的对象的实际类型。然后,CLR必须遍历继承层次结构,用每个基类型去核对指定的类型(如Employee)。
上面这个事一个相当常用的编程模式,所以C#专门提供了as操作符,目的就是简化这种代码的写法,同时提升性能。
Employee e = o as Employee;
if(e != null)
{
//.....
}
as 操作符的工作方式与强制类型转换一样,只是它永远不会抛出一个异常 -- 相反,如果对象不能转型,结果就是null。所以,正确做法也就是检查最终生成的引用是否为null。应该不要直接使用最终生成的引用,否则可能会抛出一个System.NullReferenceException 异常。
注意:C#允许在一个类型中定义转换操作符方法。只有在使用一个转型表达式时,才会调用这些方法;使用C#的as或者is操作符时,永远不会调用他们。
命名空间 (namespace)
用于对相关的类型进行逻辑性分组,开发人员可以使用命名空间方便地定位一个类型。例如,System.Text命名空间定义了一组执行字符串处理的类型。
using 指令指示编译器为每一个类型附加不同的前缀,直到找到一个匹配项。using的使用,不仅极大地减少打字量,还有助于增强代码的可读性。
using指令还支持另一种形式,允许为一个类型或者命名空间创建别名。如果只想使用一个命名空间中的少数几个类型,不希望它的所有类型都跑出来“污染”全局命名空间,别名就显得十分方便。
using System;
using jack = CSI.Widget;
重要提示:CLR并不知道命名空间的任何事情。访问一个类型时,CLR需要直到类型的完整名称(可能是一个相当长的、包含句点符号的名称)以及该类型的定义具体在哪一个程序集中。这样一来,“运行时”才能加载正确的程序集,找到目标类型,并对其进行操作。
编译器会扫描引用的所有程序集,在其中查找类型的定义。一旦找到正确的程序集,程序集信息和类型信息就会嵌入最终生成的托管模块的元数据中。为了获取程序集信息,必须将定义了“引用的类型”的程序集传给编译器。
默认情况下,C#编译器会自动在MSCorLib.dll 程序集中查找 “引用的类型”,即使你没有显式告诉它这样做。MSCorLib.dll程序集中包含了所有核心Framework类库(FCL)类型的定义,比如Object, Int32, String等。
命名空间和程序集(实现了一个类型的文件)不一定是相关的。特别是,同一个命名空间中的各个类型可能是在不同的程序集中实现的。在一个程序集中,也可能包含不同命名空间中的类型。
运行时的相互关系
类型、对象、线程栈和托管堆在运行时的相互关系。调用静态方法、实例方法和虚方法的区别。
已经加载了CLR的一个Microsoft Windows 进程。这个进程中,可能存在多个线程。一个线程的创建时,会分配到一个1MB大小的栈。这个栈的空间用于向方法传递实参,并用于方法内部定义的局部变量。栈是从高位内存地址向低位内存地址构建的。
栈帧(stack frame)代表的是当前线程的调用栈中的一个方法调用。在执行线程的过程中进行的每个方法的调用都会在调用栈中创建并压入一个stack frame
至此我们讨论了源代码、IL和JIT编译的代码之间的关系,还讨论了线程栈、实参、局部变量以及这些实参和变量如何引用托管堆上的对象。我知道了,对象中包含一个指针,它指向对象的类型对象(类型对象中包含静态字段和方发表)。
还讨论了JIT编译器如何决定静态方法、非虚实例方法以及虚实例方法的调用方式。这一切的理解,可以帮助深刻地认识CLR的工作方式。
注意,Employee和Manager类型对象都包含“类型对象指针”成员。这是由于类型对象本质上也是对象。CLR创建类型对象时,必须初始化这些成员。初始化成什么呢?CLR开始在一个进程中运行时,会立即为MSCorLib.dll中定义的System.Type类型创建一个特殊的类型对象。Employee和Manager类型对象都是该类型的“实例”。因此,它们的类型对象指针成员会初始化成对System.Type类型对象的引用,如下面所示。
-----------------------------------------------------------