类型概述
类型是CLR程序的生成块(building block).
CLR类型(CLR type)是命名的可重用抽象体.
CLR类型定义由零个或多个成员(member)组成.类型的成员控制类型如何使用.以及类型如何工作.类型的每个成员都有自己的访问修饰符(access modifier)控制对于成员的访问.类型的可访问成员会被经常引用,组合在一起就是类型的合同(contract).
除了控制对给定成员的访问,开发人员还能够控制类型的实例是否需要访问该成员.多数成员能被定义为按实例(per instance)或按类型(per type)访问.按实例访问成员(per-instance member)需要通过这个类型的实例才能访问.按类型访问成员(per-type member)则没有这种要求.
CTS有三种基本类型的成员:字段,方法和嵌套类型.字段是一个命名的存储单元,它隶属于所声明的类型.方法是一个命名的操作,它可以被调用和执行.嵌套类型则是一种简单的辅助类型,它被定义为声明类型的实现的一部分.其他类型成员(例如:属性,事件)是以附加元数据的形式出现的方法(属性和事件实际上也是方法).
类型的字段控制内存如何分配.CLR使用类型的字段来决定分配多少内存给这个类型.CLR会给static字段分配一次内存:即在类型被首次加载的时候.CLR在每次分配类型实例时,都会为non-static(instance)[非静态(实例)]字段分配内存.在分配内存时,CLR初始化所有的static字段,并且为它们赋予默认值.对于数值类型,默认值是零,对于布尔类型,默认值是false.对于对象引用,默认值是null.CLR也会初始化堆分配的(heap-allocated)实例字段,同样赋予上述默认值.
CLR保证static字段和堆分配(heap-allocated)实例字段的初始化状态.CLR将把局部变量分配在堆栈中.
就customerCount来说,类型被首次使用之前内存会分配和初始化.对于其他字段,每当新的AcmCorp.LOB实例被分配在堆上时,内存都会被分配和初始化.
默认情况下,确切的内存布局是不透明的.CLR将使用虚拟的内存布局,并且经常会重新排序字段以优化访问和使用,如图3.1所示.注意,声明的顺序是:isGoodCustomer,lastName,banlance,extra和firstInitial.如果CLR以类型声明的顺序布局字段,它将不得不在字段间插入空间量(padding),以避免对个别字段的不对齐访问--这将会影响性能.为了避免这点,CLR对字段重新排序以便不再有不必要的空间量.因此,在作者的32位IA-32机器上,这意味着最终采用的顺序是:balance,lastName,firstInitial,isGoodCustomer和extra.这种布局的结果是取消不必要的空间量,并能很好地对齐数据.然而,CLR确切的布局策略并没有正式的文档,并且,对于不同版本的CLR也不可能只依赖某一种特定的策略.
CLR提供了两种将字段声明为常量值的方式.第一种方式所适用的字段,它的常量值是在编译时计算的--这是效率最高的:字段的静态值仅仅作为一个字面值存储在类型的元数据模块中,在运行时它并不是一个真正的字段.准确地说,编译器需要内联任何到字面字段的访问,从本质上讲,它是将字面值嵌入到指令流中.在C#中声明字面字段,必须使用const关键字.这还需要一个初始化表达式,使得它的值能够在编译时计算出来.
任何试图修改这个字段的做法都将作为编译时错误被捕获
对于第二种方式,CLR允许程序员将字段声明为不变的(immutable),它将一个字段声明为initonly,并动态地初始化.如果将initonly特性应用到一个字段,那么,一旦构造函数执行完毕,就不允许再对字段值修改.在C#种要指定一个initonly字段,就必须使用readonly关键字.
注意,这段代码动态地生成了created字段的初始化值,它是基于当前时间的.也就是说,在新的实例构造函数执行完毕后,假如created的值被设置,就不能再改变它
类型和初始化
在讨论类型成员之前,有两个方法需要引起特别关注.类型允许提供一个特别方法,在它首次被初始化时调用.这个类型初始化器是一个简单的静态方法,它有一个众所周知的名字(.cctor).一个类型最多只有一个类型初始化器,它没有参数和返回值,也不能被直接调用.它们是被CLR作为类型初始化的一部分自动调用的.
这段代码语义等价于下面的类型定义,它使用了C#字段初始化表达式,而不是显式的类型初始化器
对于这两种情况,作为结果的CLR类型都将有一个类型初始化器.在前一种情况下,你可以把任意语句放到初始化器中.而对于后一种情况.则只能用初始化表达式.但在这两种情况下,最后的结果类型都会有同样.cctor方法,并且,t字段在被访问之前就已被初始化了.
根据这个类型定义,字段将以这个顺序进行初始化:t2,t3,t1
至于类型初始化器实际运行的时机,CLR将灵活处理.类型初始化器总是保证在首次访问类型的静态字段之前执行.除此之外,CLR还支持两种策略:默认策略是在首次访问类型的任何成员之前,执行类型初始化器;第二种策略(通过beforefieldinit元数据特性标明)给予CLR更大的灵活性.标记为beforefieldinit的类型与没有标记的类型在两个方面是不同的.其一,在第一个成员被访问前,CLR将充分拥有调用类型初始化器的主动权;其二,CLR会推迟对于类型初始化器的调用,直到有一个静态字段被首次访问之时.这意味着在beforefieldinit类型上调用静态方法,并不保证类型初始化器会执行.它同时说明,在类型初始化器执行以前,就可以*地创建实例并使用它.也就是说,CLR将保证在任何方法使用到一个静态字段之前,执行类型初始化器
C#编译器会在所有缺乏显式类型初始化器方法的类型上设置一个beforefieldinit特性,而带有显式的类型初始化器方法的类型将不会被设置这个元数据特性.在静态字段声明中存在初始化表达式,将不会影响C#编译器是否使用beforefieldinit特性.
当类型的实例每次被分配时,CLR将自动调用另外一个不同的方法.这个方法被称为构造函数(constructor),并有一个截然不同的名字.ctor.构造函数不像类型初始化器,它可以接收它想要的参数.此外,它还能够使用方法重载的规则,即一个类型可以提供多个重载的构造函数方法.不带任何参数的构造函数被称为类型的默认构造函数.为了授予或禁止对个别成员的访问,构造函数方法还可以使用访问修饰符,它们与字段或者标准的方法使用的修饰符是一样的.这与类型初始化方法有很大不同,类型初始化方法总是private.
C#编译器将在所生成的.ctor方法中,在显示的方法体之前,插入non-static字段初始化表达式.就默认构造函数来说,t2和t3初始化语句会在t1的初始化语句之前.
C#编译器还支持链式(chaining)构造函数,允许一个构造函数调用另一个构造函数.
类型和接口
我们经常需要根据两个或更多的类型所设的公共假设将类型划分成不同的类别.这种归类相当于类型的附加文档,因为只有显示地声明属于这个类别的类型.才被认为是可以共享该类别中所隐含的假设.在CLR中,将这些类型的类别称为接口(interface).接口是整合到类型系统中的类型归类.因为接口代表的类别自身就是类型,所以,你可以声明字段(变量及方法参数)来获取类别的从属关系,而不是对要用到的实际的具体类型进行硬编码(hard-code).这种松散的要求允许在实现上的可替代性,它是多态(polymorphism)的基石
从结构上说,接口是CLR的另外一种类型.接口有类型名,可以有成员,其限制条件就是它既不能有实例字段,也不能有带实现的实例方法.从结构上说,接口与其他类型的真正区别是,在类型的元数据上是否存在interface特性.在CLR中使用接口的语义是特别规定的.
接口是形成分类或类型家族的抽象类型.对接口类型的变量,字段和参数进行声明是合法的.但实例化一个仅仅基于接口的对象是不合法的.更进一步地说,接口类型的变量,字段和参数必须引用一个具体类型的实例.而这个具体实例则必须显式地被声明与该接口兼容
一个类型声明兼容多个接口是合法的.当一个具体类型(例如,一个类)声明兼容多个接口时,就说明这个类型的实例可在多个上下文中切换.
接口对还能够将显式的要求强加于兼容它的类型上,特别是包含抽象方法声明(abstract method declaration)的接口.这些方法声明相当于对支持该接口的所有类型的要求.如果一个具体类型声明兼容接口I,那么,这个具体类型必须提供接口I中的所有抽象方法的实现
类型和基类型
除了用多重接口声明兼容性(也就是继承),一个类型还可以指定最多一个基类型.基类型不能是接口,而且严格来说,它所支持的接口集也不能被认为是声明类型的基类型.此外,接口本身没有基类型.准确地说,一个接口最多有一组所支持接口,这和具体类型一样.
没有指定基类型的非接口类型将使用System.Object作为它们的基类型.有时基类型将从CLR触发不同的运行时语义(例如,引用类型与值类型,按引用封送,委托).基类型也能用于将通用成员打包为单个类型,这样能够为多个类型所支持.当定义一个类型时,你可以控制该类型是否作为基类型使用.假如将类型声明为sealed,将会禁止将它作为基类型使用.另一方面,假如声明为abstract,那么不允许直接实例化该类型,它的用处仅限作为基类型.接口类型总是隐式的abstract.如果一个类型既不是abstract,也不是sealed,那么,程序员便可以把它当作基类型使用,也可以实例化为新的对象.不是abstract的类型经常作为具体(concrete)类型被引用.
就跨程序集可访问性而言,基类型的non-private成员成为派生类型的合同的一部分.派生类型的方法能够访问基类型的non-private成员,就如同它们是派生类型中被显示声明.派生类型中的成员名可能会与基类型中的non-private成员名发生冲突(不论是偶尔的还是精心设计的).如果发生这种情况,那么在派生类型中,这两种成员都会存在.如果这个成员是static,则可以用类型名区分.如果成员是non-static,就可以使用语言相关的关键字(如this或者base)进行限定,要么选择派生类型成员,要么选择基类型成员.
当基类型和派生类型存在同名的方法时,CLR支持两种基本的策略:按名字的隐藏(hide-by-name)和按签名隐藏(hide-by-signature).通过在派生类型的方法上添加或者不添加hidebysig元数据特性,从而指明方法的声明将采取那种策略.当使用按签名隐藏(hide-by-signature)声明方法时,只有名字相同和签名相同的基类型的方法将被隐藏.对于基类型中的其他同名方法,则在派生类型的合同中是可见的.相比之下,当使用按名字隐藏(hide-by-name)声明方法时,派生类型的方法隐藏了基类型的所有同名方法,而不在乎它们的签名.用C++定义的类型默认情况是按名字隐藏的,因为这是C++语言最初定义的方式.用C#定义的类型却不同,它们总是使用按签名隐藏.用VB.NET定义的类型可以采用这两种策略中的任何一个,具体取决于该方法是使用了Overloads(按签名隐藏)关键字,还是Shadows(按名字隐藏)关键字
关于派生类型的构造函数和基类型的构造函数是如何协同工作的,C#语言有着自己的解决之道,如图3.5所示.在面对带初始化表达式的实例字段的声明时,编译器产生的.ctor会首先以声明的顺序调用所有的字段初始化器.一旦派生类型的字段初始化器被调用,派生类型构造函数将使用程序员提供的参数调用基类型构造函数(如果使用base构件).如果基类型构造函数完成执行,派生类型构造函数会继续执行构造函数的主体(例如,花括号中构造函数的部分).这意味着当基类型的构造函数执行时,派生类型的构造函数的主体还没有开始执行.