.NET本质论 类型基础
类型概述
类型是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构件).如果基类型构造函数完成执行,派生类型构造函数会继续执行构造函数的主体(例如,花括号中构造函数的部分).这意味着当基类型的构造函数执行时,派生类型的构造函数的主体还没有开始执行.
.NET本质论 类型基础的更多相关文章
- C#学习笔记——面向对象、面向组件以及类型基础
C#学习笔记——面向对象.面向组件以及类型基础 目录 一 面向对象与面向组件 二 基元类型与 new 操作 三 值类型与引用类型 四 类型转换 五 相等性与同一性 六 对象哈希码 一 面向对象与面向组 ...
- [CLR via C#]4. 类型基础及类型、对象、栈和堆运行时的相互联系
原文:[CLR via C#]4. 类型基础及类型.对象.栈和堆运行时的相互联系 CLR要求所有类型最终都要从System.Object派生.也就是所,下面的两个定义是完全相同的, //隐式派生自Sy ...
- 《C#从现象到本质》读书笔记(三)第3章C#类型基础(下)
<C#从现象到本质>读书笔记第3章C#类型基础(下) 常量以关键字const修饰.C#支持静态字段(类型字段)和实例字段. 无参属性的get方法不支持参数,而有参属性的get方法支持传入一 ...
- [No0000B9]C# 类型基础 值类型和引用类型 及其 对象复制 浅度复制vs深度复制 深入研究2
接上[No0000B5]C# 类型基础 值类型和引用类型 及其 对象判等 深入研究1 对象复制 有的时候,创建一个对象可能会非常耗时,比如对象需要从远程数据库中获取数据来填充,又或者创建对象需要读取硬 ...
- [No0000B5]C# 类型基础 值类型和引用类型 及其 对象判等 深入研究1
引言 本文之初的目的是讲述设计模式中的 Prototype(原型)模式,但是如果想较清楚地弄明白这个模式,需要了解对象克隆(Object Clone),Clone其实也就是对象复制.复制又分为了浅度复 ...
- 《C#从现象到本质》读书笔记(二)第2章 C#类型基础(上)
<C#从现象到本质>读书笔记第二篇 第2章 C#类型基础(上) 类型指的是集合{类,结构,接口,枚举,委托}中的任意一个成员.任何拥有某类型的值(value)称为某类型的一个实例(inst ...
- [Clr via C#读书笔记]Cp4类型基础
Cp4类型基础 Object类型 Object是所有类型的基类,有Equals,GetHashCode,ToString,GetType四个公共方法,其中GetHashCode,ToString可以o ...
- CLR-基元类型以及溢出检查 (CLR-Via-C#) 类型基础
CLR-基元类型以及溢出检查 =========(CLR via C#阅读笔记)======== 基元类型(primitive type): 基元类型也不做过多的解释,举个例子即可清晰的辨别 在j ...
- c#基础-类型基础深入了解
对象类型需要动态内存,基础类型需要静态内存 动态内存分配在堆上,静态内存分配在栈上. 静态内存保存着简单的变量,如 int a=0; 值类型:把一个值类型赋值给另外一个值类型,改变其中一个另外一个不会 ...
随机推荐
- centos7下nginx安全配置
Linux服务器下nginx的安全配置 1.一些常识 linux下,要读取一个文件,首先需要具有对文件所在文件夹的执行权限,然后需要对文件的读取权限. php文件的执行不需要文件的执行权限,只需要 ...
- <Spark><Spark Streaming>
Overview Spark Streaming为用户提供了一套与batch jobs十分相似的API,以编写streaming应用 与Spark的基本概念RDDs类似,Spark Streaming ...
- Python pandas快速入门
Python pandas快速入门2017年03月14日 17:17:52 青盏 阅读数:14292 标签: python numpy 数据分析 更多 个人分类: machine learning 来 ...
- HDU 1004 Let the Balloon Rise(map应用)
Problem Description Contest time again! How excited it is to see balloons floating around. But to te ...
- java连接操作数据库
Connection 类prepareStatement(String sql) 创建一个 PreparedStatement 对象来将参数化的 SQL 语句发送到数据库. PreparedState ...
- 第二十六课 典型问题分析(Bugfix)
问题1: glibc中的strdup实现如下: 没有对参数s进行空指针判断. 我们的Exception.cpp中应做改进: 在第12行进行判断空指针操作. 问题2: t1在析构时会抛出异常,我们在re ...
- VMware网络连接IP设置
网络配置(仅主机模式) 一.改变虚拟机IP地址达到联网目的 仅主机模式,第一步,打开我的电脑属性,查看VMt1网卡IP设置,设置一个区段:192.168.xx.aa xx.aa自由设置,简 ...
- Java中的面向对象II
既然要创建一个对象那么就需要有一个类,下面介绍类的构建. 一.类的两个元素: 1.字段 字段也就是类变量,每一个类变量都是类的成员. <1.>类变量访问指定通常是私有的(private)或 ...
- Linux系统下curl命令上传文件,文件名包含逗号无法上传
使用curl命令,将备份好的图片全部重新导入到seaweedfs,图片全部以存储在seaweedfs中的fid命令, fid中间有一个逗号,使用curl命令时报错: curl: (26) couldn ...
- Java中类的构造方法
constructor:构造函数. 在创建对象的时候,对象成员可以由构造函数方法进行初始化. new对象时,都是用构造方法进行实例化的: 例如:Test test = new Test("a ...