运行时的类型

类型本身并不是万能的.类型真正有意思的地方在于,程序员使用类型的实例,并让它们相互作用.类型的实例(instance)既可以是对象,也可以是值,这取决于类型如何定义的.基本数据类型(primitive type)的实例是值,而绝大多数用户定义类型的实例是对象,尽管也存在能够产生值的类型.

每一个对象和每一个值都是一个确切类型的实例.实例和类型的从属关系通常是隐式的.例如,声明一个System.Int32类型的变量或字段,其结果是分配一块符合该类型的内存,它的存在与否取决于操纵它的执行代码.CLR(和基于CLR的编译器)只允许对那些属于System.Int32类型定义的值进行操作.实施这种值和System.Int32类型的从属关系并不需要额外的开销,因为编译器会在编译时去实施,并且CLR验证器会确保在代码加载后维护这种从属关系

每个对象也属于一个类型.然而,因为对象总是通过对象引用来访问的,所以,被引用对象的实际类型可能不匹配该引用的声明类型.当对象引用的是一个abstract类型时,就是这样的情形.显然,我们需要某种机制来明确对象与其类型的从属关系,以处理这种情形.接下来让我们分析CLR的对象头(object header)

CLR的每个对象都以一个固定大小的对象头开始,如图4.1所示.对象头不能通过程序直接访问,但它确实存在.对象头的确切格式没有正式的文档说明.因而,下面的描述是在IA-32体系结构上对CLR1.0版本进行实验分析推断而来的.CLI的其他几种实现在某种程度上也像是由这个格式衍生而来的.

对象头有两个字段:它的第一个字段是同步块(sync block)索引.你可以使用这个字段推迟该对象与附加资源的关联(例如,锁,COM对象);对象头的第二个字段是一个句柄(handle),它指向一个不透明的数据结构,用于表示该对象的类型.尽管这个句柄的位置也没有正式文档说明,但通过System.RuntimeTypeHandle类型可以对它显示地支持.有趣的是,在CLR的当前实现中,对象引用总是指向对象头的类型句柄字段.第一个用户自定义类型的字段总是sizeof(void*)字节,不管对象引用指向哪里.

给定类型的每个实例在对象头中都会有相同的类型句柄值.类型句柄(type handle)简而言之是指向一个非透明的,无正式文档说明的数据结构的指针.这个数据结构包含了类型的完整描述,以及一个指向类型元数据的内存表示的指针.数据结构的内容对于各种关键性能的操作(例如,虚方法的分发,对象分配)在某种意义上做优化处理,使之尽可能地块.有关该数据结构的第一个应用就使动态类型强制转换(dynamic type coercion)

当从一个对象引用的类型转换到另一个对象引用的类型时,必须考虑两个类型之间的关系.如果初始引用的类型被认定与新引用的类型兼容,那么,CLR要做的转换只是一个简单的IA-32 mov指令.这通常出现于这样的赋值情形中:当从一个派生类型的引用到一个直接或间接基类型的引用,或者到一个已知兼容的接口的引用(这就是向上类型转换,up-casting).另一方面,如果初始引用的类型与新引用的类型之间的兼容性不是已知的,那么,CLR必须执行一个运行时测试,以确定对象的类型与所需要的类型是否是兼容的.当赋值是从一个基类型或接口引用到一个深度派生的类型的引用时(这就是向下类型转换,down-casting),或者到一个互不相关的类型的引用时(平行类型转换,side-casting),这种测试总是必需的

为了支持向下类型转换和平行类型转换,CIL(公共中间语言,Common Intermediate Language)定义了两个操作码:isinst和castclass.这两个操作码分别带有两个参数:一个对象引用和一个用于表示所期望的引用类型的的元数据标记.两个操作码都会生成代码,用于检查对象的类型句柄,以决定对象的类型是否与请求的类型相兼容.两个操作码的不同之处在于它们报告测试结果的方式.如果测试成功,两个操作码只是简单地将对象引用保留在用于测试计算的堆栈上:如果测试失败,则两个操作码的表现各不相同.如果请求的类型不被支持,castclass操作码会抛出一个System.InvalidCastException类型的异常.相反,如果测试失败,isinst操作码只是简单地把一个空引用放到用于测试计算的堆栈上.由于异常的开销相对较高,所以只有当转换总是期望成功时,才使用产生castclass操作码的构件.相似地,如果两种结果都是预期的,则应该使用产生isinst操作码的构件.有趣的是,JIT编译器将CIL的isinst和castclass指令分别翻译为对内部的JIT_IsInstanceof函数和JIT_ChkCast函数的调用,这两个函数都没有文档说明.

isinst和castclass都是在利用被RunTimeTypeHandle引用的数据结构,尽管这个数据结构(在内部被称为CORINFO_CLASS_STRUCT)没有正式的文档说明,但它包含了许多关键的信息.如图4.2所示,每个类型都有一张接口表(interface table).它包含了类型所兼容的每个接口的入口项.在类型的接口表中的每个入口项包含了用于支持该接口的类型句柄.对接口类型的转换将通过这张表进行匹配.为了支持向直接或间接基类型的转换,这个数据结构还包含了一个指针,指向类型元数据的内存表示,而它则包括一个指向该类型的基类型元数据的指针.对于直接或间接基类型的强制转换,将会使用该数据结构的这个部分进行匹配.对于这两种情况,类型兼容性测试只是通过接口表进行简单的线性检查(linear search),接着通过元数据结构链表进行线性遍历(linear traversal).这意味着,对于支持大量接口或者多级基类型(或者两者都有)的类型,类型兼容性的运行时测试将比只支持少量接口或者扁平的类型层次结构(或者两者都有)的简单类型要慢的多

每种编程语言都以自己的方式公开isinst和castclass操作码.在C#中,isinst操作码通过as和is关键字公开.as关键字是一个二元操作符,它接收一个变量和一个类型名.C#接着发射(emit)适合的isinst指令,并且将返回的引用作为操作符的结果

C#的is操作符的工作方式与as相似,只是最后结果的引用变成了一个布尔值,它的值取决于引用是否为空.下面是使用is操作符的同样代码:

这段代码在语义上等价于前面的实例.不同的是在第二个示例中,IBillee引用是不可用的.

C#通过其强制类型转换操作符公开castclass操作码.C#强制类型转换使用和C同样的语法.考虑下面的示例

注意,在这个示例中,有一个异常处理程序将被用于处理潜在的失败.再次考虑到异常相对较高的开销,如果强制转换不能保证总是成功的,那么,采用isinst操作码的构件要更合适一些

尽管对于基于CLR工作的程序员来说,类型句柄和其引用的数据结构根本是不透明的,但存储在这个数据结构(以及该类型的下级元数据)上的大多数信息可以由程序员通过System.Type类型访问到.System.Type基于底层优化后的类型信息,向程序员提供了易于使用的外观(facade).你可以通过调用System.Type的静态方法GetTypeFromHandle,从类型句柄获取一个System.Type对象,也可以通过System.Type的TypeHandle属性还原类型句柄

每种编程语言都提供了一种自己的机制,用于将符号化类型名转换为System.Type对象.在C#中可以用typeof操作符.typeof操作符接收一个符号化的类型名,得到的结果是一个该类型的System.Type对象的引用.下面演示了typeof操作符的使用:

对于一个给定的类型,CLR保证在内存中恰好只有一个System.Type对象存在.这意味着在这个示例中,type和t2被保证引用的是同一个System.Type对象

上述例子假定所需要的类型名在编译阶段时是有效的.此外,所请求类型的程序集会变成该模块和程序集的静态依赖项.为了不用静态依赖项便支持类型的动态加载,首先需要使用Assembly.Load或Assembly.LoadFrom动态加载该类型的程序集.在该程序集被加载后,你就可以使用Assembly对象的GetType方法提取想要得到的类型.下面的代码与前面的示例在语义上是等价的:

这个版本的UseType方法与前面示例的不同之处在于:该版本显式地加载了包含该类型的程序集,而不是假定程序集依赖项会被自动解析。

前面的两个示例演示了如何获取一个基于类型名的System.Type对象.你还可以获取内存中的任何对象或值的System.Type对象.为此,你可以调用System.Object.GetType方法.前面我们已经谈到System.Object是通用类型,并且所有的类型都与System.Object兼容.GetType就是System.Object的方法之一.当GetType在一个值上被调用时,它只是简单地返回隐式属于该值的类型对象;当GetType方法在一个对象引用上被调用时,它将使用存储在对象头上的System.RuntimeTypeHandle,并且调用GetTypeFromHandle方法

在对象或值上调用GetType方法,你可以发现对象或值的类型在运行时的情形.GetType方法最简单的应用是检查两个对象引用是否指向相同类型的实例.

只有当o1和o2引用相同类型的实例时,该测试才会返回真.System.Type还能够通过IsSubclassOf方法和IsAssignableFrom方法支持类型兼容性测试.下面的代码测试了一个对象是否是另一个对象类型的子类的实例.

注意,在这个示例中,只有当其中一个对象是另一个对象的类型的直接或者间接基类型的实例时,测试才会返回真.也就是说,如果t1和t2引用相同的类型,System.Type.IsSubclassOf方法返回假.此外,如果t1和t2中引用了接口类型时,System.Type.IsSubclassOf方法将返回假.当然,这在该示例中是不可能发生的,因为System.Object.GetType方法保证决不会返回接口类型的引用.

由于System.Type.IsSubClassOf方法用处不大,因此,该方法有一个更有用一些的变体System.Type.IsAssignableFrom方法,它专门用于测试类型之间的兼容性.如果两个类型是相同的,其结果就为真.如果指定的类型派生于当前类型,那么结果为真;如果当前类型是一个接口,而指定的类型与当前类型兼容,那么结果也为真.考虑下面的示例:

在这个示例中,IsCompatible方法的工作方式类似于先前的IsRelatedType方法.区别在于:如果o1和o2引用相同类型的实例,现在的测试结果将会是真.我们还可能需要列举一个给定类型的基类型或者接口(或者两者都有).为了列举一个类型的接口,你可以调用System.Type.GetInterfaces方法,它返回一个Type对象的数组,每个被支持的接口对应一个Type对象.为了枚举一个类型的基类型,你可以递归地查看System.Type.BaseType属性.下面的代码将打印出与对象兼容的各个类型的列表:

这个例子使用AssemblyQualifiedName属性来获取类型的完全限定名.如果你想要一个更为友好的版本,那么可以使用FullName属性来获取命名空间的限定名,或者使用Name属性返回没有命名空间前缀的类型名称.这个例子还说明了CLR在基类型上和接口处理上的差别

用元数据编程

反射使得程序能够触及类型定义的所有方面,不管是在开发时还是在运行时。

CLR提供了丰富的实用部件用于轻松地生成代码.System.Reflection.Emit库允许基于CLR的程序发射类型,模块和程序集.IMetaDataEmit接口向C++/COM程序提供了相同的功能;最后,System.CodeDom提供了用于在内存中构造更高级的C#或VB.NET程序的功能,并在执行前把它们编译到模块和程序集.

图4.3显示了反射对象模型.注意,这个对象模型反映了下列事实,即类型属于模块,而模块又属于程序集.此外这个模型还反映了类型包含诸如字段和方法成员的事实

如图4.4所示,MemberInfo类型相当于大多数特定反射类型的通用基类型

MemberInfo类型有四个重要属性:Name属性把该成员名称作为一个字符串返回:MemberType属性返回一个System.Reflection.MemberTypes值,表明该成员是否是一个字段,方法或者另外一种成员;ReflectedType属性返回MemberInfo对象从属的System.Type对象;对于继承的情形,ReflectedType属性返回的类型可能是(也可能不是)与实际声明该成员的类型相同.要获取声明该成员的类型,必需使用DeclaringType属性

特殊的方法

丰富的元数据主要是为了更精确地保留和传达程序员的意图.例如,程序员经常会针对一个命名的值定义一对方法,典型的情形就是用一个方法get(获取)这个值,另一个方法set(设定)这个值.一个CLR类型可以包含附加的元数据,来表明该类型中哪些方法是以这种方式使用的.这种附加元数据被正式命名为属性(property)

与属性的思路一样,对于用作注册或取消事件处理程序(event handler)的指定方法,CLR提供了显式的支持.CLR事件(CLR event)是类型的命名成员,它引用同一类型中的其他方法.在被引用的这些方法中,有一个是用于注册事件处理程序的.另一个方法则用于取消该注册.一个给定的事件可以有多重事件处理程序,这些处理程序有着截然不同的名字.就像属性一样,事件属于一个类型.事件的类型必需派生于System.Delegate.

类型的事件可以通过System.Type.GetEvents和System.Type.GetEvent方法访问,这两个方法都返回System.Reflection.EventInfo来描述事件.EventInfo对象有一些属性用来表明事件的名称和类型。EventInfo最令人感兴趣的成员是GetAddMethod方法和GetRemovemethod方法.如图4.6所示,每个方法都会返回一个MethodInfo,依次描述事件的注册方法和取消方法.它们能够接收一个布尔值,用于控制是否返回非公有方法

不管是哪一种形式,C#编译器都会发射两个具有如下签名的方法定义:

元数据和可扩展性

你可以相当自由地使用元数据特性,而不必提供正式的定义.CLR元数据中的大多数构件都有一个32位特性(atrribute)字段,用于调整该特性所关联的类型,字段和方法的定义.

到目前为止讨论的数据特性有initonly(字段)(在C#中,通过关键字readonly可以声明一个initonly字段),beforefieldinit(类型)(C#编译器会在所有缺乏显式类型初始化器方法的类型上,设置一个beforefieldinit特性.而带有显式的类型初始化器方法的类型将不会被设置这个元数据特性),hidebysig(方法)(C#定义的类型总是使用按签名隐藏).为了使这些固化的特性可见,你既可以使用反射,也可以采用非托管的元数据接口IMetaDataImport

有时候编程语言的设计者会选择公开一个元数据特性,将其作为修饰符关键字(例如,C#的readonly字段修饰符).然而,为了避免关键字数量的膨胀,往往采用另一种机制,那就是自定义特性(custom atrribute).

自定义特性允许语言设计者支持任意的元数据特性, 而不用引入新的关键字到编程语言中.当自定义特性被用于支持CLI的预定义特性时,它们被称为伪定制特性(pseudo-custom attribute),原因就是当编译器发射CLR元数据时,这个特性将被转换成一个标准的固定特性.例如,CLI在类型的元数据中预定义了一个标志,以标明类型的实例是否支持对象的序列化.尽管这个特性在元数据中只是一个简单的二进制位,但它被System.SerializableAttribute伪定制特性控制.

.NET本质论 用类型编程的更多相关文章

  1. 《类型编程晋级——shapeless类库使用指南》前言及第一章翻译

    从年初开始进行此项工作,我和合作伙伴包亮付出了大量而艰辛的劳动,现基本翻译完毕,有出版意向,如果有意向欢迎联系,不甚感激!也欢迎各位博友对此翻译提出意见建议以及指导如何出版,在此谢过! 前言 时间回到 ...

  2. 如何理解 TS 类型编程中的 extends 和 infer

    extends extends 在TS类型编程中用法(T extends U),表示 T 中的某些在 U 里面,比较难描述,用法如下: T extends U ? X : Y 分为两种情况理解更直观一 ...

  3. WCF分布式开发步步为赢(9):WCF服务实例激活类型编程与开发

    .Net Remoting的激活方式也有三种:SingleTon模式.SingleCall模式.客户端激活方式,WCF服务实例激活类型包括三种方式:单调服务(Call Service),会话服务(Se ...

  4. WWDC-UIKit 中协议与值类型编程实战

    本文为 WWDC 2016 Session 419 的部分内容笔记.强烈推荐观看. 设计师来需求了 在我们的 App 中,通常需要自定义一些视图.例如下图: 我们可能会在很多地方用到右边为内容,左边有 ...

  5. mypy 支持静态类型编程的python变种

    每种编程语言都有一群固定的用户,对于那些习惯将不同编程语言用成同样的感觉的人来说,最是难受.因为每种语言都有它独特的设计『哲学』和擅长的应用领域. PHP给大家的一贯的印象都是动态弱类型语言,Pyth ...

  6. 成为编程大牛很简单,把这些书看个八成就OK

    原文链接:http://lucida.me/blog/developer-reading-list/ 本文把程序员所需掌握的关键知识总结为三大类19个关键概念,然后给出了掌握每个关键概念所需的入门书籍 ...

  7. Scalaz(27)- Inference & Unapply :类型的推导和匹配

    经过一段时间的摸索,用scala进行函数式编程的过程对我来说就好像是想着法儿如何将函数的款式对齐以及如何正确地匹配类型,真正是一种全新的体验,但好像有点太偏重学术型了. 本来不想花什么功夫在scala ...

  8. 对“针对接口编程,而不是针对实现编程”的理解

    今天在阅读<Head First设计模式>的时候,看到了这句话:"针对接口编程,而不是针对实现编程",第一次见到的时候,不太清楚作者想表达的意思,想着到后来看看实例就懂 ...

  9. Java编程思想 学习笔记11

    十一.持有对象  通常,程序总是根据运行时才知道的某些条件去创建新对象.在此之前,不会知道所需对象的数量,甚至不知道确切的类型. Java实用库还提供了一套相当完整的容器类来解决这个问题,其中基本的类 ...

随机推荐

  1. VCL界面控件DevExpress VCL发布v18.2.2|附下载

    DevExpress VCL Controls是 Devexpress公司旗下最老牌的用户界面套包.所包含的控件有:数据录入,图表,数据分析,导航,布局,网格,日程管理,样式,打印和工作流等,让您快速 ...

  2. 单字段去重 distinct 返回其他多个字段

    select a.*, group_concat(distinct b.attribute_name) from sign_contract_info a left join sign_temp_at ...

  3. usort 函数

    function getNameFromNumber($num){ // Used to figure out what the Excel column name would be for a gi ...

  4. orm的理解

    orm:是对象->关系->映射,的简称. mvc或者mvc框架中包括一个重要的部分,就是orm,它实现了数据模型于数据库的解耦,即数据模型的设计不需要依赖于特定的数据库,通过简单的配置就可 ...

  5. golang flag简单用法

    package main import ( "flag" "strings" "os" "fmt" ) var ARGS ...

  6. 反转链表(python3)

    问题描述: 反转一个单链表. 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL解法1: ...

  7. Spring NoSQL

    把数据收集到一个非规范化的结构中,按照这种方式优化处理文档的数据库称之为文档数据库.文档数据库不适用于数据具有明显关联关系,因为文档数据库并没有针对存储这样的数据进行优化. Spring Data M ...

  8. deconvolution layer parameter setting

    reference: 1. Paper describes initializing the deconv layer with bilinear filter coefficients and tr ...

  9. LBS推荐系统的设计方法

    https://www.csdn.net/article/2015-12-24/2826554 http://www.datayuan.cn/article/14797.htm https://my. ...

  10. directive例子2

    (function() { angular.module('app.widgets') .directive('bsModalPlus', function($window, $sce, $modal ...