封装机制确保开发者将数据和操作数据的行为组织到一起,将行为细节对用户隐藏。在先前的章节中,我们将面向对象作为“module-as-type“机制的扩展。也可以将面向对象放在“module-as_manager”框架中。下面的第一小节,我们考虑在非面向对象语言中的数据隐藏机制 module。第二小节中我们考虑当引入继承到 module 之后新的数据隐藏问题。在第三小节,我们简要回顾 module-as-manager 方法,展示几种语言包括 Ada 95 和 Fortran 2003 如何增加继承,从而允许 (static) module 允许继续提供数据隐藏。

10.2.1 Modules

数据隐藏的 Scope 规则是 Clu, Modula, Euclid 和其他基于模块的 1970 年代的语言的主要创新。在 Clu 和 Euclid 中,module 的声明和定义总是在一起出现。在 Modula-2 中,开发者可以选择性的将声明和定义在不同文件。不幸的是,没有办法将声明分为 public / private 部分,所有的东西都是 public。唯一的数据隐藏是允许指针类型声明在 header 里,然后不透露指向的对象结构。编译器可以生成代码,因为指针类型大小都是一样的。

Ada 通过将 header 分为 public 还是 private 提升了复杂性。导出的类型细节可以放在 private 部分,然后在 public 只有名字。

分配堆和栈的解释。

‘this’ parameter

module 内部的 subroutine 出现了问题。它们如何知道哪个被使用?当然可以给每个实例复制一份代码,就像复制数据一样。但是复制很浪费,仅仅在地址计算上有所不同。更好的方法是创建一个 subroutine 单例,然后在运行时,然后将实例地址传递给单例,这个地址就是隐藏的每个 subroutine 的第一个参数。

my_stack.push(x) -> push(my_stack, x)

Making do without Moudle Headers

C++ 的 namespace 可以跨编译单元,一个文件也可以包含多个 namespace。与此同时,很多现代语言,比如 Java 和 C#,不需要显式声明 header 和 body。开发者只需要定义接口,如果需要 header,有工具可以帮助开发者提取,而不需要手动编写。

10.2.2 Classes

随着继承的引入,面向对象语言必须解决基于 module 的 scope 规则无法覆盖的问题。比如,如何控制基类成员在派生类中的访问规则?基类的 private 应该被派生类访问吗?基类的 public 总是应该被派生类访问吗?

C++ 中基类的 private 在派生类中还是 private,public 也是,但是 C++ 允许派生类显式删除基类的 public 成员:

class queue: public list {
  ...
  void append(list_node *new_node) = delete;
}

类似的机制还可以在 Eiffel, Python, Ruby 中发现。

除了 public 和 private 标签,C++ 还有 protected 标签。protected 成员只在基类和派生类中可见。当然,protected 也可以用在描述基类的位置:

class derived: protected base {...}

这样基类中的 public 成员在派生类中就是 protected 成员。C++ 规则可以总结如下:

  • 类可以限制成员的可见级别。public 可以在 class 声明的 scope 可见。private 只能被类成员可见。protected 只在基类和派生类成员可见。(例外情况是,可以声明 friend 类和 subroutine,访问自己的 private 成员)
  • 派生类可以限制基类成员的访问级别,但是不能扩大。通过声明基类 protected, private
  • 派生类可以限制基类成员的访问级别,但是派生类中,可以通过 using 声明恢复基类的访问级别(注意是恢复,还是不能扩大)
  • 派生类可以通过 delete 使得方法不可被访问。

其他面向对象语言有不同的方法控制级别。比如 Eiffel 不仅可以限制,而且可以在派生类中提升基类成员的可见级别。

Java 和 C# 继承了 C++ 的标签,但是不提供 protected 和 private 的关于基类的继承描述,派生类不能限制也不能扩大基类的访问级别。

protected 在 Java 中还有比 C++ 中不同的含义: protected 成员不仅在派生类中可见,而且在 package 中可见。C# 的 protected 就像 C++ 中一样,不过还提供了 internal 关键字使得成员在类出现的汇编组件中可见(汇编组件是一系列链接在一起的编译单元,类似 java 中的 .jar 包),C# 中成员默认是私有的。

在 SmallTalk 和 Object-C 中,没有成员可见级别的问题:都是 public 的,Python 也是。Ruby 都是 private。

Static Fields and Methods

除了 public, private, protected 的可见级别标签,大多数面向对象语言还允许字段或者方法使用 static 描述。static 类成员被认为属于整个类,而不是单个对象。static 方法没有 this 参数,不能访问非 static 成员或者字段。

10.2.3 Nesting(Inner Classes)

很多语言允许类嵌套声明。那么问题是:如果 Inner 是 Outer 的成员,Inner 的方法可以访问 Outer 的成员吗,如果可以,如何实现?C++ 和 C# 中采用的最简单实现,只允许访问 Outer 的静态成员。实际上,嵌套仅作为信息隐藏的手段。Java 采用了更复杂的方案,允许 Java 中的嵌套类访问周围类的任何成员。因此每个内部类的实例必须属于外部类的某个实例。

class Outer {
  int n;
  class Inner {
    public void bar() {n = 1;}
  }
  Inner i;
  Outer() {i = new Inner();} // constructor
  public void foo() {
    n = 0;
    System.out.println(n); // prints 0
    i.bar();
    System.out.println(n); // prints 1
  }
}

如果有多个 Outer 实例,每个实例有不同的 n, Inner.bar 的调用必须访问正确的 n。所以,每个 Inner 实例要有 Outer 实例的指针。如果 Java 的嵌套类被声明为 static,就像 C++ 和 C# 一样,只能访问周围类的静态成员。

Java 类也可以嵌套在方法内。这种 local 类不仅能访问周围类的成员,方法的参数和变量也可以。【译者注:更详细规则感兴趣请参考原文】

Inner 和 local 类在 Java 中被用于创建对象闭包。在 9.6.2 中我们用来处理事件。并且,local 类可以是匿名的:可以内联出现在 new 方法内。

10.2.4 Type Extensions

Smalltalk,Objective-C, Eiffel, C++, Java, C# 都是一开始就设计为面向对象的语言,要么就是从一门没有强力封装机制的语言演化而来。都支持 module-as-type 的抽象,其中类机制提供了封装和继承。几种其他类型的语言,包括 Modula-3, Oberon, CLOS, Ada 95/2005, Fortran 2003 可以被描述为在 module 已经支持了封装之后的面向对象扩展的语言。这些语言没有改变现有的 module 机制,而是通过扩展 record 的方式提供了动态方法绑定和继承。

【译者注:关于更多细节请参考原书本节,译者不了解 Ada 语言,以免翻译出错】

10.2.5 Extending without Inheritance

扩展现有抽象功能是面向对象编程的主要动机之一。继承可能是扩展的标准机制。但是有时候,不能使用继承的时候,比如处理已经存在的代码,类也想要扩展:比如 Java 中 final 标签的;C# 中使用了 sealed。即使原则上继承可用,但是比如有大量已存在代码使用了原来的类名,返回更改所有变量的声明为派生类不是很好的方式。

对于这些场景,C# 提供了 extension 方法,可以扩展已有的类:

static class AddToString {
  public static int toInt(this string s) {
    return int.Parse(s);
  }
}

int n = myString.toInt();

extension 方法必须是 static,也必须声明为 static 类。第一个参数也必须 this 前缀。这个方法就可以作为该类的方法被调用。

实际上,这个方法声明就是语法糖:

static class AddToString {
  public static int toInt(string s) {
    return int.Parse(s);
  }
}
...
int n = AddToString.toInt(myString);

没有什么特别的。需要注意的是,extension 方法它们不能访问类的 private 成员和动态绑定的方法。相反,几种脚本语言,包括 JavaScript 和 Ruby,允许开发者向已经存在的类中添加方法,我们在 14.4.4 中讨论这些。