二进制兼容性
Binary Compatibility
正如第1章所详述的,面向组件编程的一个核心原则是客户端和服务器之间的二进制兼容性。二进制兼容性使二进制组件成为可能,因为它强制双方都遵循一个二进制契约(通常是一个接口)。只要服务器更新的版本遵守客户端和服务器的原始契约,则客户端不会因服务器的改变而受到影响。COM是第一个真正通过提供二进制兼容性免除“DLL地狱”(前一章已经描述过)的组件技术,许多开发人员已经把COM对“二进制兼容性”的实现(和由此带来的局限性,如不可变的COM接口)与“二进制兼容性”的原则相等同。.NET处理二进制兼容性的方法不同于COM,因而编程模式也不同。为了搞清这些含义,了解它们为什么与COM不同,本节首先简要地描述COM二进制兼容性的实现,然后论述.NET支持二进制兼容性的方式。
COM二进制兼容性
COM Binary Compatibility
COM通过使用接口指针和虚拟表来提供二进制兼容性。在COM中,客户端直接通过一个接口指针来和对象交互。接口指针实际上指向另一个被称为“虚拟表指针”的指针。虚拟表指针指向“函数指针”的一个表。表中的每个槽指向的位置,是对应的接口方法代码所驻留的位置(见图2-9)。
当客户端使用接口指针来调用一个方法(譬如接口的第二个方法)时,编译器嵌入到客户端的代码是一个跳转指令,它跳越到虚拟表的第二个入口指向的地址。事实上,这个地址是从表开始点计算的偏移值。在运行时,加载器修补跳转命令到实际的地址,因为它已经

图2-9:COM二进制兼容性
知道表的内存位置。COM客户端在其代码中记录的东西,实际上是从虚拟表开始位置的偏移值。在运行时,所有提供相同内存表布局的服务器(表中的项指向签名完全相同的方法)被认为是二进制兼容的。顺便要说的是,这就是实现一个COM接口的确切定义。这个模式产生出了著名的COM 第一法则:“永远不要改变已发布的接口。”对虚拟表布局的任何改变,都会破坏现有的客户端代码。虚拟表布局与接口定义是同质的——例如,接口的第二个方法是虚拟表的第二个入口。接口有多少个函数,虚拟表就要有多少个项。对接口定义的任何改变,也必然引起对所有客户端重新编译和重新部署;否则,客户端不再与服务器二进制兼容。
.NET二进制兼容性
.NET Binary Compatibility
.NET使用元数据来提供二进制兼容性。高级别的客户端代码(譬如C#或者Visual Basic 2005)被编译到IL中。客户端代码不能包含内存地址的任何偏移,因为这些偏移取决于JIT编译器如何生成本地代码。在运行时,JIT编译器编译和链接IL到本地机器代码。原始IL仅仅包含一些基于该类型元数据的、对调用某个方法或访问某个对象的字段的请求——这就好像IL仅包含一个标记来识别要调用的方法,而不是本地代码中的一个传统的方法调用。在元数据中提供这些方法或者字段的所有类型,都是二进制兼容的,因为该类型在内存中实际上的二进制布局,是由JIT编译器在运行时决定的,并且在客户端的IL中没有与这个布局有关的内容。
.NET基于元数据的二进制兼容性的最主要优势是,每个类都是一个二进制组件。这样与COM相比就大大地简化了组件开发。诸如ATL这样的复杂框架不再是必需的,因为.NET本身就支持组件。.NET架设了一座跨越技术鸿沟的桥梁,它跨越了大部分开发人员能做的与传统的组件技术所要求的之间的鸿沟(这在第1章里提到过)。如果你只理解面向对象编程,你可以面向对象做开发,但仍然获得面向组件编程的一些好处,即靠.NET来管理二进制兼容性和版本化。如果你理解了面向组件编程的核心问题,那你获得了面向组件
应用程序的全部优势的同时,可以最大化你的生产效率和潜能。与COM不同,.NET二进制兼容性不局限于基于接口编程。任何.NET的类型,不论是一个类还是一个结构,都与它的客户端兼容。任何入口点,不论是一个实例方法、对象字段、静态方法,还是静态字段,都是与二进制兼容的。基于元数据二进制兼容性的其他主要优势是,它给你提供了晚绑定的灵活性,而同时保留早绑定的安全性,代码从不跳转到错误的地址,不需要将实际的入口点地址或者偏移值写到客户端的代码中。例如,考虑如下的.NET接口:
public interface IMyInterface
{
void Method1();
void Method2();
}
接口本身定义和实现在一个服务器程序集中,但它的客户端是在另一个服务器程序集中。客户端按接口定义来编译,编译器通过强制正确的参数类型和返回值来提供类型安全。然而,如果你改变接口中方法的顺序,同样的客户端也会运行得很好(不用重新编译):
public interface IMyInterface
{
void Method2();
void Method1();
}
或者如果你增加一个新的方法:
public interface IMyInterface
{
void Method3();
void Method1();
void Method2();
}
如果客户端不使用某个方法,则你可以移除该方法,客户端将不受影响。以前,只有晚绑定的脚本语言才有这种级别的灵活性,因为晚绑定的脚本语言是解释代码,而不是编译代码。COM接口禁止所有这些改变,因为它们与第一法则(永远不要改变已发布的接口)相抵触。添加新方法是在COM中定义新接口的主要原因(这使COM编程模式复杂化了)。然而,.NET允许你移除不使用的方法、添加新方法(或者字段)和改变方法的顺序(尽管你不能改变方法参数,或者移除客户端想使用的方法)。
二进制继承
Binary Inheritance
基于元数据二进制兼容性的一个有趣的副作用是,它允许实现二进制继承。在传统的面向对象的编程中,一个子类的开发人员必须有描述基类的源文件才能从基类派生子类。在.NET中,类型是用元数据来描述的,因此子类开发人员仅须访问基类的元数据。编译器从二进制文件中读取元数据,就可了解在基类中哪个方法和字段是可用的。事实上,即使基类和子类都在相同的项目中被定义,编译器仍然会使用项目中的元数据来编译子类。这就是为什么在.NET中的类如何定义序位是没有关系的(不同于在C++中,它经常要求有序的类型声明,或一个特定顺序的头文件列表)。你可以使用二进制继承来扩展.NET框架所有未密封的基类。但是注意,继承是一把双刃剑。继承是一种白盒重用的形式:它造成子类与基类的耦合,而且要求对基类功能的深入了解。COM不允许对实现的二进制继承(仅允许对接口的二进制继承)的部分原因是因为COM的设计师知道这些副作用。.NET的设计师希望支持继承来弥补上面提到的技术鸿沟。然而,我强烈反对滥用继承。应该尽可能地使你的类层次结构简单明了,尽可能多地使用基于接口的编程。






