620 likes | 791 Views
兼容性. 兼容性. 本讲介绍 源码级和二进制级的兼容性 它可以被认为一个基本编程规则的集合 ... 以确保代码能在未来可用 以及作为一个很好的兼容性例子. 兼容性. 基本的 任何接口就是必须与使用者维护的合同 合同被破坏了,兼容性也就被破坏了 一个好的 Symbian 开发者应该理解 为了扩展组件,接口中什么能以及什么不能修改 而不需要破坏源码或者二进制的兼容性. 兼容性的等级. 理解源码的、二进制的、库的、语义的,以及前向和后向兼容. 前向和后向兼容. 兼容性存在于两个方向 前向和后向 当一个组件被更新时
E N D
兼容性 • 本讲介绍 • 源码级和二进制级的兼容性 • 它可以被认为一个基本编程规则的集合... • 以确保代码能在未来可用 • 以及作为一个很好的兼容性例子
兼容性 • 基本的 • 任何接口就是必须与使用者维护的合同 • 合同被破坏了,兼容性也就被破坏了 • 一个好的Symbian 开发者应该理解 • 为了扩展组件,接口中什么能以及什么不能修改 • 而不需要破坏源码或者二进制的兼容性
兼容性的等级 • 理解源码的、二进制的、库的、语义的,以及前向和后向兼容
前向和后向兼容 • 兼容性存在于两个方向 • 前向和后向 • 当一个组件被更新时 • 是以这样的方法,即其他使用组件原来版本的代码 • 还能继续与更新的版本一起工作 • 这是后向可兼容变化 • 当软件 • 能与组件更新后版本一起工作 • 也能跟以前的版本一起工作 • 这种改变叫做前向可兼容
库 version 2.0 应用程序 A2 后向可兼容 应用程序A1 与库v2一起工作 更新及重新发布 库 version 1.0 应用程序 A1 前向兼容性 应用程序A2与库v1.0一起工作 前向和后向兼容
新代码与旧代码一起工作 • 例如 • 应用程序使用现有的库 • 该库被更新到一个新的版本 • 如果应用程序与新库一起工作 • 能够保持跟旧库工作一样的行为 • 那么新的库就保持了后向可兼容的关系 • 即——新代码可以和就代码一起工作
旧代码与新代码一起工作 • 一个前向可兼容关系 • 需要一些巧妙的方法还能够获得 • 如果库的早期版本 • 替换了现有的库 • 而应用程序能够继续以相同的方式工作 • 则前期版本的库 • 被称为有前向可兼容关系 • 即——旧代码跟新代码一起工作
前向和后向兼容 • 后向兼容性 • 是在进行组件的增量发布时的主要目标 • 而前向兼容性则是额外的需要 • 一些修改是不能前向可兼容的 • 例如修改bug • 它从本质上是在修改之前的版中不能正确工作的
源代码兼容 • 如果对一个组件进行了修改 • 而依赖它的组件可以重新编译 • 而不需做任何修改 • 这可以说是源代码可兼容的修改 • 源代码可兼容修改的一个例子 • 是对一个导出函数进行内部bug修正 • 它不要求修改函数的声明 • 即组件的接口
源代码兼容 • 典型的源代码不兼容的修改 • 会修改成员函数的内部 • 使得它可能发生Leave,而以前的版本不会这样 • 为了严格遵守命名规范 • 函数的名称可以必须增加后缀L
源代码兼容 • 源代码可兼容修改 • 并不意味着依赖的组件不需要重新编译 • 而是它们不需要修改就能够重新编译成功.
二进制兼容 • 二进制兼容即一个组件依赖于另一个组件 • 在它所依赖的组件被修改以后 • 还能够继续运行 • 而不需要重新编译或重新链接 • 这种兼容性是跨越编译和链接边界的
源代码和二进制兼容 • 二进制可兼容修改的一个例子 • 是增加类的公开非虚函数 • 它是按照序号从库中导出的 • 它位于以前导出的函数之后 • 客户端组件 • 它依赖于原来版本的库 • 也不会受到导出列表末尾新添加函数的影响 • 因此,这种修改是二进制可兼容的,也是后向兼容的
源代码和二进制兼容 • 如果添加新的函 • 到类中,会引起导出函数序号的重新排序 • 这种修改就不是二进制可兼容的 • 虽然它仍然是源代码可兼容的,即重新编译不需要修改代码 • 依赖的代码必须重新编译 • 否则它使用新的非法序号 • 来识别导出的函数
类级和库库的兼容 • 在类级维护兼容性意味着: • 确保函数继续保持与最迟文档记录相同的语义 • 没有公开看访问的数据被移除 • 或者减低可访问性 • 类对象的大小也没有改变 • 维护库级兼容性意味着确保: • DLL导出的API函数保持相同的序号 • 并且参数和返回值也是兼容饿.
防止兼容性中断什么是不能改变的? • 知道类的哪些属性在大小上的改变会中断兼容性 • 理解类级修改会中断源代码兼容性 • 理解哪些类级修改会中断二进制兼容性 • 理解哪些二进制级修改会中断二进制兼容性 • 理解哪些函数级修改会中断二进制和源代码兼容性 • 就哪些修改会导致二进制兼容性中断角度,区分可派生和不可派生类
类对象的大小必须不改变 • 改变一个类对象的大小 • 例如通过增加或移除数据 • 将引起二进制兼容性的中断 • 除非它能保证: • 类不是外部可派生的 • 即构造函数没有从定义它的DLL中导出 • 分配对象的唯一代码 • 位于被改变的组件/DLL中 • 或者它有非公开的构造函数,该函数组织它从栈上派生 • 类具有虚析构函数
类对象的大小必须不改变 • 对象需要分配栈上的内存大小 • 是确定的 • 对每个组件,在构建时 • 改变对象的大小 • 将影响以前编译的客户端代码 • 除非客户端保证实例化该对象 • 只在堆中 • 例如,使用NewL()工厂函数
类对象的大小必须不改变 • 另外 • 访问对象中的数据成员 • 是通过指针偏移量实现的 • 如果类的大小改变了 • 例如,增加了一个数据成员 • 派生类的数据成员的偏移量 • 就变成非法了
类对象的大小必须不改变 • 为确保一个类的对象 • 不能被派生或实例化 • 除了由类的成员或友元 • 它应该具有私有的、非内联、非导出的构造函数 • 简单的的不去声明任何构造函数 • 是不够的 • 因为编译器会生成隐式的公共缺省构造函数
类对象的大小必须不改变 • 如果一个类需要缺省构造函数 • 它应当被定义为私有的 • 然后在源文件中实现 • 或者在可以公开访问的地方,它不是内联函数 • 所有的Symbian OS C类都是从CBase派生的 • CBase定义了受保护的缺省构造函数 • 防止编译器创建一个隐含的版本
可访问的,必不能删除 • 如果某些东西被从一个API中移除 • 该API被一个外部的组件使用 • 该组件的代码不能再与API进行编译 • 即源代码兼容性被破坏了 • 该组件也不会在它的实现上运行 • 即 二进制兼容性被破坏了
如果一些东西是可访问的 ... • 在API层面上,不要删除任何: • 外部可见的类 • 函数 • 枚举 • 枚举中的值 • 像文字字符或常量的全局数据 • 在类一级,不要删除任何: • 方法 • 成员数据 • 私有和保护的成员数据 • 不应被删除 • 因为这会改变结果对象的大小
可访问的成员数据必须不被重新组织 • 重新安排成员数据的顺序 • 会引起直接访问数据的客户端代码的问题 • 因为成员数据从对象的this指针的偏移量 • 会被改变 • 不要改变成员变量的位置,如果该数据是: • 公开的 –或者保护的(如果客户端代码从该类派生的) • 通过公共或者保护的内联方法被暴露 –内联的方法将会被编译到客户端代码中
可访问的成员数据必须不被重新组织 • 这条规则也意味着 • 一个类多重继承基类的顺序 • 不能改变了而又不中断兼容性 • 因为这个顺序影响派生对象的整体数据布局
导出的函数必须不给重新排序 • 每个导出API函数 • 与一个序号关联 • 被连接器(linker)用来标识函数 • 该函数序号被保存在模块定义文件(.def) 中 • 如果.def文件列表被重新排序 • 例如,加列表中加入了新的导出函数 • 序号数值就会改变 • 这样,以前编译的代码 • 就不能正确定位函数了
导出的函数必须不给重新排序 • 例如 • 在列表开始处添加一个新文件 • 将是所有的序号增加1 • 这样,现在使用序号4的组件 • 就是原来序号3的组件 • 这种改变中断了二进制兼容性 • 为了避免它 • 新的导入应当总是被添加到.def文件的末尾 • 这将分配了新函数一个以前没有使用的序号值
虚函数 • 外部可派生的类的虚函数必须不被 • 增加 • 删除 • 修改 • 因为这将破坏二进制兼容性
虚函数 • 如果一个派生类 • 定义了它自己的虚函数 • 该函数将至于其虚函数表中 • 直接的位于基类定义的虚函数后面 • 如果一个虚函数 • 在基类中被增加或者删除 • 那么虚函数表vtable中 • 派生类所定义的序号的位置将发生改变 • 这样,任何代码 • 它是基于派生类的原始版本进行编译的 • 将使用一个错误的vtable布局 • 这就破坏了二进制兼容性
虚函数 • 虚函数的下列改变也将破坏兼容性: • 改变参数 • 修改返回值 • 改变const 的使用 • 但是 • 函数内部操作的修改 • 例如修正bug • 不会影响后向兼容性
虚函数必须不被重排序 • 虽然没有在C++标准中声明 • 虚成员函数的顺序 • 在类定义中指定的 • 可以认为是唯一因素… • …会影响它们在虚函数表中的顺序 • 该顺序不应被改变 • 因为客户端编译是根据 • 早期版本的虚函数表 • (概不虚函数顺序)这样会使得它调用完全不同的虚函数
虚函数 • 以前继承过的虚函数不应当被覆写 • 覆写一个以前继承过的虚函数 • 会改变基类的虚函数表 • 因为现有客户端代码是基于原来vtable编译的 • 它将继续访问继承过来的基类函数 • 而不是新派生的函数 • 这导致了不一致性 • 在根据原始版本库编译的调用者 • 和根据新版本编译的调用者之间 • 虽然这不会确切的产生不兼容 • 但是最好还是避免
虚函数 • 例如 • 版本1.0 的CSiamese类的客户端 • 调用SleepL(),这会调用CCat::SleepL() • 而版本2.0的客户端 • 是调用CSiamese::SleepL()... • class CCat : public CBase // 抽象基类 • { • public: • IMPORT_C virtual ~CCat() = 0; • public: • IMPORT_C virtual void PlayL(); // 缺省实现 • IMPORT_C virtual void SleepL(); // 缺省实现 • protected: • CCat(); • };
虚函数 • class CSiamese : public CCat // Version 1.0 • { • public: • IMPORT_C virtual ~CSiamese(); • public: • // 覆写 PlayL() 但是使用缺省的 CCat::SleepL() • IMPORT_C virtual void PlayL(); • // ... • }; • class CSiamese : public CCat // Version 2.0 • { • public: • IMPORT_C virtual ~CSiamese(); • public: • // 覆写 PlayL() 和 SleepL() • IMPORT_C virtual void PlayL(); • IMPORT_C virtual void SleepL(); • // ... • };
API的语义不应被修改 • 修改一个类或者全局方法或者一个常量意义的文档记录的行为 • 可能破坏兼容性 • 无论源代码和二进制兼容性是否被维持
API的语义不应被修改 • 作为一个很简单的例子 • 考虑一个类,它在输入一个数据集时 • 返回数据的平均值 • 如果Average()函数的第一个发行版本 • 返回的是算术平均值 • 而第二次发布的Average() • 也应当这么做 • 而不是返回 • 一个中间值或者其他解释的平均值
API的语义不应被修改 • 在头文件中指定的缺省参数 • 被编译到客户端代码中 • 虽然对缺省参数所做的改变 • 不会破坏二进制或者源代码兼容性 • 但是客户但必须被重新编译以获知该变化 • 使用就的缺省参数的客户端 • 会得到一个意外的返回值 • 这会是个问题,因为函数的行为 • 也是组成它接口的一部分
Const 的使用不应被删除 • Const的语义不应被移除 • 因为这将是一个源代码不兼容的修改 • 这意味着下列要素的“不变性” • 参数 • 返回值 • 或者方法 • 不应被删除
通过值传递参数 • 通过值传递参数 • 绝不能改为通过引用传递 • 或者相反 • 因为这回破坏二进制兼容性 • 当一个参数用值进行传递时 • 编译器会产生一个栈副本,并将其传递给函数 • 但是,如果函数签名改变了 • 以接收通过引用传递的参数 • 指向原始对象的一个字大小的引用 • 会被传递给函数
通过值传递参数 • 栈结构的使用 • 对于引用传递函数调用,与 • 值传递函数调用是显著不同 • 这导致了二进制不兼容
通过值传递参数 • class TColor • { • ... • private: • TInt iRed; • TInt iGreen; • TInt iBlue; • }; • // version 1.0 • // Pass in TColor by value (12 bytes) • IMPORT_C void Fill(TColor aBackground); • // version 2.0 – binary compatibility is broken • // Pass in TColor by reference (4 bytes) • IMPORT_C void Fill(TColor& aBackground);
什么可以改变而不会破坏兼容性? • 理解什么类级的修改不会破坏源代码兼容性 • 理解什么类级的修改不会破坏二进制兼容性 • 理解什么库级的修改不会破坏源代码兼容性 • 理解什么函数级的修改不会破坏二进制和源代码兼容性 • 区分可派生和不可派生类在那些可以改变而不破坏二进制兼容性方面的不同
一个API可能被扩展 • 类,常数,全局数据或函数 • 可以被添加而不破坏兼容性 • 一个类可以被扩展通过添加 • 静态成员函数 • 或者非虚成员函数 • 而不是虚成员函数 • 导出函数的序号 • 必须被添加在模块定义文件(.def)导出列表的底部 • 以避免重新牌讯现有函数
类的私有构件实现可以修改 • 对私有或保护方法进行修改 • 它及不导出也不是虚的,是不会破坏客户端兼容性的 • 但是这些函数绝不能被调用 • 通过外部可访问的内联函数 • 因为内涵函数中的调用 • 会被编译成外部调用代码 • 从而会因为类内部的不兼容修改而被破坏
类的私有构件可以修改 • 修改私有成员数据 • 也是允许的 • 除非 • 它们会导致对象大小的改变 • 或者移动对象中公共或受保护数据的位置 • 它是通过继承直接暴露出去的 • 或者通过公共的内联访问函数暴露
访问规格可以放松 • C++ 的访问规格符 • public, protected, private • 不影响类的布局 • 可以放松,而不会影响对象的数据顺序 • 一个对象中成员数据的位置 • 仅仅是有指定的位置所决定的 • 在类定义时
访问规格可以放松 • 改变访问规则 • 到一个更严格的形式 • 例如 –从 public到 private • 意味着成员数据 • 对于外部客户端变得不可见的,而它从前是可见的 • 这会破坏源代码兼容性 • 但是不破坏二进制兼容性
指针可以用应用替代,反之亦然 • 从指针变为引用参数或返回类型 • 或者相反 • 不会破坏二进制兼容性 • 但是破坏源代码兼容性 • 这是因为引用和指针 • 可以被C++编译器以相同的方式表示 • 都是一个机器字
导出的非虚函数的名称可以改变 • Symbian OS 只是通过序号进行链接 • 而不是通过名字或者签名 • 这意味着 • 改变导出函数的名字是可能的 • 仍然保持二进制兼容性 • 但不是源代码兼容性