(翻译)《Expert .NET 2.0 IL Assembler》 第七章 命名空间和类(一)

返回目录

  

     正如在前面章节所讨论的,CLR计算模型本质上是面向对象的。类的概念——或者,为了使用更加精确的运行时术语,类型的概念——围绕着整个被组织起来的计算模型的核心原则。一个项的类型——变量、常量、参数等等——定义了数据表示和这个项的行为特性。因此,一个类型可以被另一个代替只有当这两方面和这两种类型是相当的——例如,一个派生类可以被解释为它的父类的类型。
     CLI的ECMA国际/ISO标准规范将类型划分为值类型和引用类型,依赖于一个项的类型表示一个数据项本身还是一个对数据项的引用(一个地址或一个位置指示器)。
     引用类型包括对象类型、接口类型和指针类型。对象类型——类——是自描述类型的值,完全的或部分的。部分自描述类型的值被称为抽象类。接口类型总是部分自描述类型的值。接口总是表示由类暴露的行为特性的子集;类被认为是实现了相应的接口。指针类型是对项的简单的引用,指出了项的位置。
     这就是ECMA国际/ISO标准所说的,而我不打算讨论这些尖端的理论,如类和接口为什么是自描述的而值类型不是,或者为什么在功能单元之间传递项的方式——通过值或者引用——突然变成了这些项本身的天生特性。
     CLR对象模型支持单一类型的继承,而多继承是通过一个或更多的接口的实现来模拟的。结果,CLR对象模型完全是层次化的,System.Object类位于这棵树的根部(参见图7-1)。然而,接口类型并不是类型层次化的一部分,因为它们在层次上是不完整的而且其自身没有任何实现。

 

图7-1 CLR类型层次

 

接口扮演一个有趣的角色:它们被用作一个类的本票(promissory note)。当类X派生于类YX继承了Y的所有成员,因此继承性直接影响了派生类的结构。但是当你说类X实现了接口IY,你就允诺了类X将要暴露在IY中描述的所有方法,这被视为强加在类X上的一个约定。类X没有从它实现的IY接口中继承什么,除了IY的方法实现上的“欠债”外。

包包译注:本票(Promssory Note)是一项书面的无条件的支付承诺,由一个人作成,并交给另一人,经制票人签名承诺,即期或定期或在可以确定的将来时间,支付一定数目的金钱给一个特定的人或其指定人或来人。

所有类型(接口除外)最终派生于System.Object。本章检查了类型和它们的声明,将类型划分为5个类别:类、接口、值类型、枚举和委托。这些类别并不是互不相容的——例如,委托就是一个类,而枚举则是值类型——但是每个分类的这些类型具有不同的特性。

 

类的元数据

出于结构化的观点,所有这5个类别都具有唯一的元数据表示。从而,我们可以笼统地讨论类的元数据,或类型的元数据。

       类的元数据围绕着两个截然不同的概念来进行分组:类型定义(TypeDef)和类型引用(TypeRef)。TypeDef和相关的元数据描述了声明在当前模块的类型,反之TypeRef描述了指向声明在其它地方的类型的引用。充分定义一个类型而不是引用一个已经定义的类型显然需要获取更多的信息。TypeDef和相关的元数据要远比TypeRef复杂。

       当定义一个类型的时候,你应该提供以下的信息:

l         已定义的类型的完整名称

l         指出了这个类型应该拥有的特性的标记

l         这个类型所派生的类型

l         这个类型所实现的接口

l         加载器应该如何在内存中对这个类型进行布局

l         这个类型是否内嵌在另一个类型中——如果是,内嵌在哪一个类型中

l         这个类型的字段和方法(如果存在)在哪里可以被找到

 

当引用一个类型的时候,只有它的名称和作用域需要被指定。作用域指出了引用类型的定义在哪里可以被找到:在这个模块中,在这个程序集的另一个模块中,或者在另一个程序集中。在引用内嵌类型的情形中,作用域是另一个TypeRef

       7-2显示了用于类型定义和引用的元数据表,但是没有涉及类型成员定义的表——例如,字段和方法以及它们的特性。箭头通过元数据符号来表示交叉表的引用。在下面的章节,你将看到所有涉及到的元数据表。

 

7-2 用于类型定义和引用的元数据表

我必须指出在图7-2比较靠下部分的三个表(TypeSpecGenericParamGenericParamContraint)以及它们的关联链路只在2.0版本中进入到这个图中(这话可没有弦外之音啊)。它们涉及到泛型并将在第11章讨论。

 

TypeDef元数据表

TypeDef表包括类型定义信息的主表。这个表中的每笔记录有6项:

l         Flags4字节无符号整数)。二进制标记,指出了类型的特性。TypeDef标记是众多的而且重要的,因此本章单独讨论它们;参见“Class特性”。

l         Name#String流中的偏移量)。类型的名称。这个项不能是空的。还记得第一章的Odd.or.Even类么?Odd.or.Even是它的全名。这个类的NameEven——全名的一部分,在最右边句点的右边。

l         Namespace#String流中的偏移量)。类型的命名空间,全名的一部分,在最右边句点的左边。第一章的Odd.or.Even类具有Odd.or的命名空间。如果这个类的全名不包括句点,Namespace项就可以是空的。命名空间和名称组成了类型的全名。

l         ExtendsTypeDefOrRef类型的编码符号)。这个类型的父级的一个符号——就是说,是这个类型所派生自的那个类型。对于所有接口以及一个类——类型层次基类System.Object,这个项必须被设置为0。对于所有其它的类型,这个项必须携带一个指向TypeDefTypeRefTypeSpec表的有效的引用。只有当父一级的类型是一个泛型的实例时,TypeSpec表才可以被引用。

l         FieldList(在Field表中的记录索引[RID])。Field表的一个索引,标记了属于这个类型的字段记录的开始位置。

l         MethodList(在Method表中的RID)。Method表的一个索引,标记了属于这个类型的方法记录的开始位置。

 

TypeRef元数据表

TypeRef元数据表具有比TypeDef表更简单的结构,因为它只需携带必要的数据来识别任意被引用的类型,因此CLR加载器能够在运行时解决这个引用。这个表中的每笔记录有3个项:

l         ResolutionScopeTypeDefOrRef类型的编码符号)。这个类型定义位置的指示器。如果被引用的类型定义在当前程序集的什么位置,这个项就必须被设置为0,或者,如果被引用的类型定义在同样的模块中,就设置为4(压缩符号1——模块符号)。除了这两种相当特殊的情形,一般而言,ResolutionScope可以是一个引用了ModuleRef表的符号——如果这个类型定义在同一个程序集中的另一个模块中;可以是一个引用了AssemblyRef表的符号——如果这个类型定义在另一个程序集中;可以是一个引用了TypeRef表的符号——如果这个类型内嵌在另一个类型中。让类型的TypeRef定义在同一个模块中,并没有构成一个元数据错误,但这样做是多余的而且如果可能的话是应该避免的。

l         Name#String流中的偏移量)。被引用的类型的名称。这个项不能是空的。

l         Namespace#String流中的偏移量)。被引用的类型的命名空间。这个项可以是空的。命名空间和名称组成了类型的全名。

 

InterfaceImpl元数据表

       如果已定义的类型实现了一个或多个接口,相应的TypeDef记录就被一个或一些InterfaceImpl元数据表的记录所引用。这个表被认为是一个查找表(不是描述了一些元数据项,而是在其它表中描述的项之间的关系),提供了关于“什么实现了什么”的信息,而且它是按实现类型排序的。InterfaceImpl表在每笔记录中只有两个项:

l         ClassTypeDef表中的RID)。TypeDef表的索引,包括了已实现的类型。

l         InterfaceTypeDefOrRef类型的编码符号)。一个已实现的类型的符号,它是可以位于TypeDefTypeRefTypeSpec表中的。TypeSpec表只有在已实现的接口是泛型接口的一个实例的时候,才可以被引用(参见第11章)。这个已实现的类型必须被标记为一个接口。

 

NestedClass元数据表

       如果已定义的类型内嵌在另一个类型中,它的TypeDef记录就被另一个查找表所引用:NestedClass元数据表。(更多关于内嵌的信息,参见本章后面的“内嵌类型”。)和InterfaceImpl表一样,NestedClass表是一个查找表,而其中的记录描述了一些其它表之间的“连接”。作为一个查找表,NestedClass表的每条记录只有两个项:

NestedClassTypeDef表中的RID)。内嵌类型的索引(嵌套类)。

EnclosingClassTypeDef表中的RID)。当前类型所内嵌的类型的索引(包装类,或者嵌套类)。

由于这两项的类型都是在TypeDef表中的RID,内嵌类型以及他的包装类型部可以定义在不同模块或程序集中。

 

ClassLayout元数据表

通常,加载器对于如何安排已加载的类型有其自己的想法:它可能在类的字段之间添加过滤器用于对齐,或者甚至是搅乱这些字段。然而,某些类型必须以一种特殊的方式来安排(例如,设想你引进一个用来描述COFF头的值类型,它具有一个非常有限的结构和布局,或者你想创建一个简单的东西作为一个联合union),并且它们携带了充实这些细节的元数据信息。

ClassLayout元数据表提供了额外的关于打包顺序和类型大小的的信息。在第1章,例如,当我声明一个不具备任何内部结构的“placeholder”类型时,我使用这样的额外信息——这个类型的全部大小。

       ClassLayout元数据表中的记录有三个项:

PackingSize2字节无符号整数)。字节的对齐因子。这个项必须被设置为0或者2的乘方,从1128。如果这个项不是0,那么它的值将被用作字段的对齐因子,用以取代这个字段类型的“中性”对齐特征(“中性”对齐通常与类型的大小或2的最接近乘方相符)。例如,如果PackingSize被设置为2,并且你有两个字段——一个字节和一个指针——那么你的布局中将包括一个字节(第一个字段),另一个字节(过滤器),和一个指针;这种情形中的指针将是按2字节对齐的,在几乎所有的处理器架构中,这是一件不好的事情。可是,如果PackingSize的值大于一个字段的“中性”对齐,就会使用这个“中性”对齐;如果,例如,PackingSize设置为2,并且你有两个1字节的字段,那么你的布局中就恰好包括2个字节(第一个字段,第二个字段)而在它们中间没有任何过滤器。

ClassSize4字节无符号整数)。类型所需布局的全部大小。如果这个类型有实例字段和这些字段的概要大小,通过PackingSize对齐,是不同于ClassSize的,加载器为这个类型分配这两个型号中比较大的那个。

       ParentTypeDef表中的RID)。这个类型所属的类型定义记录的索引。ClassLayout表不应该包括任何具有相同Parent项的值的重复数据。

 

命名空间和类的全名

是时候要认真讨论CLRILAsm中的名称了。到目前为止,在第6章,你所遇到的名称实际上只是文件名称,因此必须遵守众所周知的文件命名约定。从现在开始,你将需要处理一般意义上的名称,因此了解规则是非常重要的。

 

ILAsm命名约定

ILAsm中的名称要么是简单的,要么是复合的。复合名称由简单名称和特殊的连接符号(如句点)组成。例如,SystemObject是简单名称,而System.Object是复合名称。根据ILAsm语法,每种名称的长度都是不受限制的,但是元数据规则在名称长度上强加了一些限制。

       简单名称的最简单形式是一个标志符,在ILAsm中必须必须以一个文字符号或下列符号之一作为开始:

       #$@_

并以文字符号或下列符号之一作为继续:

?$@_`

(最后一个符号并不是一个省略符号,而是一个backtick

这里是一些有效的ILAsm标志符的例子:

l         Object

l         _Never_Say_Never_Again

l         men@work

l         GType`1

 

警告:ILAsm标志符中有一个明显的限制,ILAsm标志符不能匹配任何ILAsm关键字(数量相当的多)。

 

CLR接受广泛的多样性的名称而具有非常少的局限性。某些名称——例如,.ctor(实例构造函数),.cctor(类的构造函数,又叫做类型初始化)以及_Deleted*(一个元数据项,用来标记在编辑并继续期间的删除操作),都是为CLR内部使用而保留的。然而,一般来说,CLR是不受名称的拘泥的。只要一个名称能服务于它的意图——清楚地表示一个元数据项——并且不会被误解,这就是非常好的了。这种自由,当然,包括了以错误符号开始的名称以及由错误符号组成的名称,并没有提及恰好匹配ILAsm关键字的名称。

为了覆盖这种多样性,ILAsm提供了一种可选的方法来表示一个简单名称:单引号。例如,这里是一些有效的ILAsm简单名称的例子:

l         '123'

l         'Space Between'

l         '&%!'

 

一种最频繁遇到的复合名称是带句点的名称,一个由简单名称组成并通过句点分割的名称:

<dotted_name> ::= <simple_name>[.<simple_name>*]

带句点的名称的例子如下:

l         System.Object

l         '123'.'456'.'789'

l         Foo.Bar.'&%!'

 

命名空间

简单说一句,命名空间是类的全名的公共的前缀。类的全名是一个带句点的名称,它包括的最后一个简单名称是类的名称,其余部分是类的命名空间。

       可能需要很长时间来解释命名空间不是什么。命名空间不是元数据项——它们没有关联的元数据表,并且它们不能被符号所引用。命名空间也不是直接生成在程序集上的。一个程序集的名称可能或不可能完全或部分地匹配在程序集中使用的命名空间。一个程序集可能使用多个命名空间,而同样的命名空间可以使用在不同的程序集中(一个程序集使用一个命名空间意味着一个程序集定义了类而这个类的名称属于这个命名空间)。

       那么为什么元数据模型即使烦恼于命名空间和类的名称,也不使用简单的使用类的完整名称呢?答案很简单:节省空间。让我们设想一下,定义来那两个带有全名的类Foo.BarFoo.Baz。由于名称是不同的,所以在完整名称类型中,你不得不在字符串堆中存储两个完整名称:Foo.Bar"0Foo.Baz"0。但是如果你将完整名称拆分成命名空间和名称,你只需要存储Foo"0Bar"0Baz"0。当你考虑可能存在的类的数量时,这是完全不同的。

ILAsm中的命名空间声明下面的方式:

.namespace MyNamespace
{

// Classes declared here
//
 Have full name "MyNamespace.<simple_name>"
}

命名空间可是是内嵌的,如下所示:

.namespace MyNamespace
{

// Classes declared here
//
 Have full name "MyNamespace.<simple_name>"
.namespace X
{

// Classes declared here
//
 Have full name "MyNamespace.X.<simple_name>"
}
}

或者他们也可以不是内嵌的。这是IL反编译器的1.01.1版本用以表示反编译程序集文本中的命名空间的方式

.namespace MyNamespace
{

// Classes declared here
//
 Have full name "MyNamespace.<simple_name>"
}
.namespace MyNamespace.X
{

// Classes declared here
//
 Have full name "MyNamespace.X.<simple_name>"
}

2.0版本中,推荐使用类的全名来代替命名空间的规范,而IL反编译器2.0版本遵循这种样式。出于向后兼容的原因,.namespace指令仍然可以被IL编译器识别。

 

类的全名

正如前面的章节所说明的,类的全名在一般情况下是一个带句点的名称,由类的命名空间和类的名称组成。加载器通过它们的全名称和解析范围来解析类的引用,因此通常的规则是,在同一个模块中不可以定义具有同样的全名称的类。对于多模块程序集而言,一个额外的(不是很严格)规则是禁止定义公有类——类在程序集的外部是可见的——在同一个程序集中带有同样的全名称。

ILAsm中,一个类总是由它的全名称来引用,即使是在同一个命名空间中被引用。这使得类的上下文引用是独立的。

       ILAsm1.01.1版本允许带句点的名称作为类的名称,但是你可以通过为这个带句点的名称加上引号来绕过这个限制,从而将其转为一个简单名称并避免了一个语法错误:

.namespace X
{
.class public 'Y.Z'
{

}
}

而一个类总是被它的全名称所引用,因此一个带句点名称的类不会引起任何名称解析的问题(它仍然被引用为X.Y.Z),并且这个模块将会编译并运行。但是如果你反编译这个模块,你会发现带句点名称的类的左边部分被移动到命名空间中,作为元数据发布的API的结果。

.namespace X.Y
{
.class public Z
{

}
}

     虽然这并不是你想要的,但也没有什么可怕的结果——只不过是一种天生的混淆而已。如果你了解并希望这种效果而且轻易不会产生混淆,你甚至可以完全放弃命名空间的声明,而通过它们的全名来声明类,从而匹配它们被引用的方式:

.class public 'X.Y.Z'{

}

     这就是如何在ILAsm2.0中正确的实现,只是在类的完整名称周围不带单引号,因为ILAsm2.0版本允许带引号的名称作为类或者方法的名称。

从类的声明的命名空间/名称模型切换到ILAsm2.0版本的完整名称模型的原因有两部分。首先,这种方式,类通过他们的完整名称来统一地声明和引用。其次,这样解决了内嵌类的命名问题:如果命名空间A包括了对类B的声明,B包括了内嵌类C的声明,那么这个内嵌类的全名是什么呢?A.CA.B.C?(实际上,是C,因为包装类的命名空间和内嵌类的命名空间无关。)

CLR对类的完全名称强加了限制,详细指定在UTF-8编码中不应该超过1023字节。然而,ILAsm编译器,并不支持这种限制。单引号,在ILAsm中应该用于简单名称,是一个纯的词法上的工具,而并不生成到元数据中;因此,他们不会对类的完全名称的总长度产生影响。

posted @ 2008-09-08 13:57  包建强  Views(989)  Comments(0Edit  收藏  举报