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

返回目录

  

类的特性

在前面的章节(“类的元数据”)列出了包括在一个类型定义中的各种信息。在最简单的情况中,当只涉及到TypeDef元数据表的时候,对于一个类型定义的ILAsm语法如下所示:

.class <flags> <dotted_name> extends <class_ref> {

...

}

       .class指令中指定的<dotted_name>值定义了TypeDefNamespaceName项,在.extends子句中指定的<class_ref>定义了Extends项,而<flags>定义了Flags项。

 

标记

大量的TypeDef标记可以被分成很多组,描述如下。

可见性标记(二进制标记0x00000007):

l         private0x00000000)。这个类型在程序集外部是不可见的。这是默认值。

l         public0x00000001)。这个类型在程序集外部是可见的。

l         nested public0x00000002)。这个内嵌类型的可见性是public的。

l         nested private0x00000003)。这个内嵌类型的可见性是public的;它在外包类的外部是不可见的。

l         nested family0x00000004)。这个内嵌类型的可见性是family的——就是说,只对外包类的子类是可见的。

l         nested assembly0x00000005)。这个内嵌类型只在程序集内部是可见的。

l         nested famandassem0x00000006)。这个内嵌类型只对位于同一个程序集的外包类的子类是可见的。

l         nested famorassem0x00000007)。这个内嵌类型对于位于程序集内部或外部的外包类的子类,以及程序集中的每个类型(而不考虑“世系”关系),都是可见的

布局标记(二进制标记0x00000018):

l         auto0x00000000)。这个类型字段是由加载器自动排列的。这是默认值。

l         sequential0x00000008)。加载器将会保持这些实例字段的顺序。

l         explicit0x00000010)。显示指定类型的布局,加载器将遵循这个布局。(参见第9章获取更多关于字段声明的信息。)

       类型语义标记(二进制标记0x000005A0):

interface0x00000020)。这个类型是一个接口。如果这个类型没有被指定,就会被认为是一个类或值类型;如果这个标记被指定,其默认的父类就被设置为nil。(一个类被认为是父类,如果没有指定extends子句,通常是[mscorlib]System.Object

abstract0x00000080)。这个类型是一个抽象类——例如,它有抽象的成员方法。同样的,这个类不能被实例化并且只能用作另一个类型或很多类型的父类。这个标记对于值类型是无效的。

sealed0x00000100)。没有任何类型可以从这个类型中派生。所有的值类型和枚举必须携带这个标记。

specialname0x00000400)。这个类型有一个特殊的名称。究竟有多特殊则取决于名称本身。这个标记指示元数据API和加载器这个名称是它们可能会感兴趣的——例如,_Deleted*

类型实现标记(二进制标记0x00103000):

import0x00001000)。这个类型由一个COM类库导入。

serialzable0x00002000)。这个类型可以通过.NET Framework类库被序列化为一系列的数据。

beforefieldinit0x00100000)。这个类型可以在第一次访问静态字段之前任何时候被初始化(它的.cctor运行)。如果没有设置这个标记,这个类型就会在第一次访问它的静态字段或方法之前或在第一次实例化这个类型之前被初始化。我将在第10章更详细地讨论这个标记以及它对类型初始化上的影响。

字符串格式标记(二进制标记0x00030000):

ansi0x00000000)。当对本地方法进行互操作时,托管的字符串默认以ANSI字符串进行来回之间的封送。封送的字符串是定义在.NET Framework类库中的System.String类的实例。封送(Marshaling)是一种用于在托管和非托管代码的边界上数据转化的基本形式。(参见第18章获取更多细节信息。)字符串信息标记只指出了默认的封送,并且在封送被显示指定时这些标记是不相关的。这个标记,ansi,是类的默认标记,从而表示了一个“默认的默认”字符串封送。

unicode0x00010000)。默认地,托管的字符串以UnicodeUTF-16)进行来回之间的封送。

autochar0x00200000)。默认的字符串封送由底层平台所定义。

保留标记(二进制标记0x00004080):

rtspecialname0x00000800)。这个名称是由CLR保留的,并且有特殊的含义。这个标记只在pecialname的联合中是合法的。关键字rtspecialnameILAsm中没有任何作用,并且被提供只是出于信息化的意图。IL反编译器使用这个关键字来显示这个保留标记的存在。保留字段不能被随意设置——这个标记,例如,是由元数据发布API自动设置的,当它发布一个带有pecialname标记的项并且这个名称被认为是特定于CLR的时候,例如,.ctor.cctor

<没有关键字>0x00040000)。这个类型具有与之关联的声明性安全元数据。当相应的声明性安全元数据发布时,这个标记由元数据发布API设置。

伪语法标记(没有二进制标记)。这些并不是真正的定义了TypeDef记录的Flags项二进制标记,而是伪词法的标记,用来修改类的默认父类。

       value。这个类型是一个值类型。默认的父类是System.ValueType

       enum。这个类型是一个枚举。默认的父类是System.Enum

 

类的可见性和友元程序集

public标记意味着这些类是可见的并可以被外部的程序集引用——在它被声明的地方。private标记意思正好相反,因此,对于这个标记一个更合适的名称可能是assembly。在CLR2.0版本中,是可以通过使用自定义特性System.Runtime.CompilerServices.InternalsVisibleToAttribute来声明某些程序集为当前程序集的“友元”。如果程序集A声明了程序集B作为它的“友元”,那么A中所有具有程序集范围内的可见性和可访问性的类和成员对于程序集B就都变成了可见的和可访问的。同时,这些类和成员对于其他程序集仍保持为不可见和不可访问的。

这是托管世界中的“友元”程序集和非托管C++的友元类和友元函数之间的重大不同。首先,在托管世界中,友元关系的粒度并不在程序集级别之下;然而在非托管C++中,友元关系定义在类或函数的级别。其次,在非托管C++中一个友元类或友元函数对这个类具有完全的访问权,包括私有的程序;然而在托管世界中,一个友元程序集只能访问内部的(程序集范围内的)类或成员,而不是那些私有的或保护的类或成员。

 

类的引用

extends子句中的非终止符<class_ref>表示指向一个类型的引用并转换成TypeDefTypeRefTypeSpec(如果父级是基本类型的一个实例)。类引用的基本语法如下:

<class_ref> ::= [<resolution_scope>]<full_type_name>
where 
<resolution_scope> ::= [<assembly_ref_alias>]
| [
.module  <module_ref_name>]

注意到在< resolution_scope >的定义中的方括号是一些语法元素;它们并没有指出定义的任何部分是可选的。

      前面的语法并没有描述泛型类型的实例,我们将会在第11章对其进行介绍。

      这里有一些类的引用的例子:

[mscorlib]System.ValueType  //  Type is defined in another assembly
[ .module  Second.dll]Foo.Bar  //  Type is defined in another module
Foo.Baz   //  Type is defined in this module

   如果类的引用的解析范围指向一个外部的程序集获模块,类的引用就会被转换为一个TypeRef元数据符号,带有由完整类型名称为NameNamespace项提供的值,以及由解析范围为ResolutionScope项提供的AssemblyRefModuleRef符号。

   如果没有定义解析范围——就是说,如果被引用的类型定义在当前模块的某处——类的引用就会被转换为相应的TypeDef元数据符号。

 

父类型

在构建的过程中,将类的引用解析为一个TypeRefTypeDef符号,我从而为TypeDef记录的Extends项提供了值。这个符号引用了类型的父类型——就是说,当前类型是从哪个类型派生出来的。

       extends子句中引用的类型必须是密闭的而且不可以是一个接口;否则,加载器加载这个类型的时候就会失败。当一个类型是密闭的时候,没有类型可以从中派生。

       如果extends子句被省略,ILAsm编译器就会根据指定的标记为这个类型分配一个默认的父类:

l         interface。没有父类。这类接口没有派生于其它类型。

l         value。父类是[mscorlib]System.ValueType

l         enum。父类是[mscorlib]System.Enum

l         以上都不是。父类是[mscorlib]System.Object

 

如果extends子句是存在的,valueenum标记就会被省略,而interface标记则会导致一个编译期错误。ILAsm中对这种错误标记的不同反应可以容易地解释为:valueenum是伪标记,就像是对IL编译器的暗示,而interface标记是一个真正的元数据标记,并且它与extends子句结合就表示无效的元数据。

       如果类型布局被指定为sequentialexplicit,这个类型的父类就也必须具有相应的布局,除非父类是[mscorlib]System.Object[mscorlib]System.ValueType,或[mscorlib]System.Enum。基本原理是这样的,类型可能从它的父类继承字段,而且这个类型并没有混合的布局——就是说,它不可能有自动排列的字段,以及排列方式为显示的或保持序列的字段。然而,一个自动布局的类型可以派生与一个有任意布局的类型;在这种情形中,关于父类字段布局的信息在排列派生类型的实例字段中不起任何作用。

 

接口的实现

如果被定义的类型实现了一个或更多接口,这个类型声明就有一个额外的子句,implements子句,如下所示:

.class  <flags> <dotted_name>
extends  <class_ref>
implements <class_refs> {

}

   非休止符<class_refs>简单表示了一个由逗号分割的类的引用清单:

.class   public  MyNamespace.MyClass
extends  MyNamespace.MyClassBase
implements MyNamespace.IOne,
MyNamespace.ITwo,
MyNamespace.IThree {

}

   在implements子句中引用的类型必须是接口。一个实现了接口的类型必须为这个接口的所有实例方法提供实现。对这个规则,唯一的例外是抽象类。

   类型声明的implements子句在InterfaceImpl元数据表中创建了和在这个子句中列出的类的引用同样多的记录。在前面的例子中,三个InterfaceImpl记录将会被创建。

   而且,尽管一个接口不能扩展任何类型,包括另一个接口,但它的确可以实现一个或多个接口。我在本章的前面部分讨论了一个类型扩展(继承)另一个类型和一个类型实现一个接口的不同。

 

类的布局信息

为了提供额外的关于类型布局的信息(字段对齐,全部类型的大小,或两者兼有),你需要使用.pack.size指令,如下面的例子所示:

.class   public  value explicit MyNamespace.MyStruct {
.pack 
4
.size 
1024

}

   这些指令,显然足够了,相应地设置联合了给定类的ClassLayout记录的PackingSizeClassSize项。

       .pack.size指令,以任意顺序出现在类型声明的范围中。如果没有指定.pack,字段对齐就默认为1。如果指定了.pack.size,就会为这个TypeDef创建一笔ClassLayout记录。

       .pack指令中指定的整数值必须为0或者2的乘方,在1120的范围内。破坏这个规则会导致一个编译错误。当这个值被设置为0,这个字段对齐就默认为由这个字段类型定义的“natural”值——类型的大小或2的乘方的最接近值。

       类的布局信息不应用于指定自动布局类型。形式上,为一个自动布局类型定义类的布局信息表示了无效的元数据。可是事实上,这完全是对元数据空间的浪费;当加载器遇到一个自动布局类型,它不会检查这个类型是否有一个相应的ClassLayout记录。

 

接口

接口是一种特殊的类型,在国际化ECMA/ISO标准的第一部分中定义为“一组已命名的方法、地址和其它约定,将会由支持相同名称的接口约定的任意对象类型实现”。换句话说,接口不是一个“真正的”类型,而只是一个已命名的暴露于其它类型的方法和属性的描述符——一个类型的凭证。从概念上,CLR中的接口类似于一个COM接口——或者说至少大意相同。

接口不是一个真实的类型,不派生于任何其它类型,而且其它类型也不能从接口中派生。但是一个接口可以“实现”其它的接口。当然,这不是真实的实现。当我提到“接口IA实现了接口IBIC”,我只是说由IBIC定义的约定是由IA定义的约定的子约定。

    作为暴露于其它类型的项(方法、属性、事件)的描述符,接口自身不能提供这些项的实现,并因此从定义上是一个抽象类型。当你在ILAsm中定义一个接口时,你可以省略关键字abstract,因为编译器遇到interface时会自动添加这些标记。

出于同样的原因,接口不能有实例字段,因为字段的声明就是字段的实现。然而,接口必须提供它的静态成员的实现——这些项由这个类型的所有实例所共享——如果确实有的话。当然,要记住静态的定义“由所有实例所共享”是通用于所有类型的,而且并不意味着借口可以被实例化。他们是不可以这么做的。接口在层次上是抽象的并且甚至不能有实例构造器。

    接口的静态成员(字段、方法)并不是定义于接口中的约定的一部分,不会生成在实现了接口的类型中。一个实现了接口的类型必须实现这个接口的所有实例成员,但它与这个接口的静态成员无关。一个实例的静态成员可以被直接访问,就像任何类型的静态成员一样,而你为此并不需要接口的一个“实例”(意味着实现了这个接口的一个类的实例)。

接口的天性作为暴露于其它类型的项的描述符要求接口本身和它的所有实例成员必须是公有的——这是有意义的——毕竟我正在讨论exposed项。

接口有很多限制。有一点是显然的:由于接口不是一个真实的类型,所以它没有布局。讨论一个约定描述符的打包大小和全部大小是完全没有意义的。

    另一个限制来源于由接口声明的实例方法必须是虚的,因为这些方法是在别处实现的,也就是,通过类来实现接口。第10章详细讨论了虚方法和它们的实现。

    还有另一个不是很明显的限制:接口不可以是密闭的。这听起来可能是自相矛盾的,因为,正如刚刚提到的,没有类型可以从接口派生——这是sealed的精确定义。这种限制的背后逻辑如下:由于一个密闭类型不可以扩展任何其它类型,它的虚方法就不能被复写而变成简单实例方法;而且,正如你可能回想到的,接口只可能为它的静态方法提供实现,因此这些实例(之前称为虚)方法就被保留为未实现的。

这个逻辑来源于一个更一般的规律,适用于所有的类型,指出一个抽象类不应该是密闭的,除非它只有静态成员。至少这是国际化ECMA/ISO标准所说的。我个人认为一个一般规律的正确表达应该是抽象类型不可以是密闭的,除非它没有抽象的(未实现的)虚方法。而且一个类型可能被声明为抽象的,即使它不包括抽象方法。你可能并不想这个特殊的类型在任何时候被实例化。这是“没有实例方法”和“没有抽象虚方法”之间显著的不同,你认为呢?

另一方面,如果你不能实例化一个类型(它是抽象的)或从中派生一些可实例化的东西(它是密闭的),那么这个类型的实例成员还有什么用途呢?因此国际化ECMA/ISO标准可能是正确的——只带有非抽象实例成员的抽象类型可以被声明为密闭的,但是它们不应该被声明为密闭的。

    然而,一个接口的实例方法,在定义上全都是抽象的、虚的。因此,不存在“应该”还是“可以”的困难选择。

你可能感兴趣的:(.net)