Previous Up Next

第 3 章  OCaml 中的对象

(作者:Jérôme Vouillon、Didier Rémy、Jacques Garrigue)



本章概述了 OCaml 的面向对象特性。

请注意,OCaml 中的对象、类和类型之间的关系与主流面向对象语言(如 Java 和 C++)中的关系不同,因此您不应该假设类似的关键字具有相同的含义。与那些语言相比,在 OCaml 中使用面向对象特性的频率要低得多。使用 OCaml 时通常有更合适的选择,比如模块和函数。事实上,很多 OCaml 程序根本不使用对象。

3.1 类和对象
3.2 直接对象
3.3 引用 self
3.4 初始化器
3.5 虚方法
3.6 私有方法
3.7 类接口
3.8 继承
3.9 多继承
3.10 参数化类
3.11 多态方法
3.12 使用强制转换
3.13 函数对象
3.14 克隆对象
3.15 递归类
3.16 二元方法
3.17 友元

3.1  类与对象

下面的 point 类定义了一个实例变量 x 与两个方法 get_x 和 move。其中实例变量的初始值是 0。变量 x 被声明为可变的,因此 move 方法可以更改它的值。

class point = object val mutable x = 0 method get_x = x method move d = x <- x + d end;;
class point : object val mutable x : int method get_x : int method move : int -> unit end

现在我们创建一个 point 类的实例,点 p

let p = new point;;
val p : point = <obj>

需要注意,p 的类型是 point。这其实是由上面的类定义所自动定义的缩写。它代表对象类型 <get_x : int; move : int -> unit>,该类型给出了 point 包含的方法及它们的类型。

我们现在可以调用 p 的方法:

p#get_x;;
- : int = 0
p#move 3;;
- : unit = ()
p#get_x;;
- : int = 3

类中变量的求值只在对象被创建时进行。因此,在下面的示例中,实例变量 x 被初始化为两个不同对象的不同值。

let x0 = ref 0;;
val x0 : int ref = {contents = 0}
class point = object val mutable x = incr x0; !x0 method get_x = x method move d = x <- x + d end;;
class point : object val mutable x : int method get_x : int method move : int -> unit end
new point#get_x;;
- : int = 1
new point#get_x;;
- : int = 2

point 类还可以被抽象为 x 坐标的初值。

class point = fun x_init -> object val mutable x = x_init method get_x = x method move d = x <- x + d end;;
class point : int -> object val mutable x : int method get_x : int method move : int -> unit end

与函数定义一样,上面的定义可以缩写为:

class point x_init = object val mutable x = x_init method get_x = x method move d = x <- x + d end;;
class point : int -> object val mutable x : int method get_x : int method move : int -> unit end

point 类的一个实例现在是一个函数,该函数需要一个初始参数来创建一个点对象:

new point;;
- : int -> point = <fun>
let p = new point 7;;
val p : point = <obj>

当然,参数 x_init 在定义的整个主体中都是可见的,包括方法。例如,下面类中的 get_offset 方法可以返回对象与其初始位置的相对位置。

class point x_init = object val mutable x = x_init method get_x = x method get_offset = x - x_init method move d = x <- x + d end;;
class point : int -> object val mutable x : int method get_offset : int method get_x : int method move : int -> unit end

在定义类的对象主体之前,可以对表达式进行求值和绑定。这对于增强不变式是有用的。例如,点可以被自动调整到坐标网格中最近的点,如下所示:

class adjusted_point x_init = let origin = (x_init / 10) * 10 in object val mutable x = origin method get_x = x method get_offset = x - origin method move d = x <- x + d end;;
class adjusted_point : int -> object val mutable x : int method get_offset : int method get_x : int method move : int -> unit end

(当 x_init 坐标不在坐标网格上时,即坐标并非整数时,可能会引发异常) 事实上,这里也可以通过使用 origin 的值调用 point 类的定义来获得相同的效果。

class adjusted_point x_init = point ((x_init / 10) * 10);;
class adjusted_point : int -> point

另一种解决办法是在一个特殊的分配函数中进行调整:

let new_adjusted_point x_init = new point ((x_init / 10) * 10);;
val new_adjusted_point : int -> point = <fun>

但是,前一种模式通常更合适,因为用于调整的代码是类定义的一部分,可以被继承。

这种机制提供了其他语言中的类构造函数。可以用这种方式定义几个构造函数来构建具有相同类中具备不同初始化模式的对象;另一种方法是使用初始化器,如下面的第 3.4 节所述。

3.2  直接对象

还有另一种更直接的方法来创建对象:不通过类来创建它。

这种方法的语法与类表达式完全相同,但是结果是一个对象而不是一个类。本节其余部分中描述的所有结构也适用于直接对象。

let p = object val mutable x = 0 method get_x = x method move d = x <- x + d end;;
val p : < get_x : int; move : int -> unit > = <obj>
p#get_x;;
- : int = 0
p#move 3;;
- : unit = ()
p#get_x;;
- : int = 3

与类不能在表达式中定义这一点不同,直接对象可以出现在任何地方,并使用它们所在环境中的变量。

let minmax x y = if x < y then object method min = x method max = y end else object method min = y method max = x end;;
val minmax : 'a -> 'a -> < max : 'a; min : 'a > = <fun>

与类相比,直接对象有两个缺点:它们的类型没有缩写,并且不能被继承。但是这两个缺点在某些情况下可能是优点,我们将在第 3.3 和 3.10 节中进行阐释。

3.3  引用 self

方法或初始化器可以调用 self(即当前对象)上的方法。为此,必须显式地将 self 绑定到变量 s (事实上 s 可以是任何标识符,尽管我们通常会选择 self 这个名称)。

class printable_point x_init = object (s) val mutable x = x_init method get_x = x method move d = x <- x + d method print = print_int s#get_x end;;
class printable_point : int -> object val mutable x : int method get_x : int method move : int -> unit method print : unit end
let p = new printable_point 7;;
val p : printable_point = <obj>
p#print;;
7- : unit = ()

变量 s 会在方法调用时被动态地绑定。特别是,当 printable_point 类被继承时,变量 s 将被正确地绑定到子类的对象上。

self 的一个常见问题是,由于它的类型可能在子类中被扩展,所以您不能预先固定它的类型。这里有一个简单的例子。

let ints = ref [];;
val ints : '_weak1 list ref = {contents = []}
class my_int = object (self) method n = 1 method register = ints := self :: !ints end ;;
Error: This expression has type < n : int; register : 'a; .. > but an expression was expected of type 'weak1 Self type cannot escape its class

您可以忽略报错消息的前两行。最后一行才是最重要的:把 self 放在外部引用中将使我们无法通过继承来拓展它。我们将在第 3.12 节中看到这个问题的解决方案。但是请注意,由于直接对象是不可扩展的,所以使用它们时不会遭遇这一问题。

let my_int = object (self) method n = 1 method register = ints := self :: !ints end;;
val my_int : < n : int; register : unit > = <obj>

3.4  初始化器

类定义中的 let 绑定在对象被构建前被执行。我们也可以在构建对象之后立即执行其他表达式。这样的代码可以被编写为一个隐藏的匿名方法,我们称之为初始化器(initializer)。因此,初始化器可以访问 self 和实例变量。

class printable_point x_init = let origin = (x_init / 10) * 10 in object (self) val mutable x = origin method get_x = x method move d = x <- x + d method print = print_int self#get_x initializer print_string "new point at "; self#print; print_newline () end;;
class printable_point : int -> object val mutable x : int method get_x : int method move : int -> unit method print : unit end
let p = new printable_point 17;;
new point at 10 val p : printable_point = <obj>

初始化器是无法被重载的。相反,所有初始化器都是按顺序被执行的。初始化器对于保持不变式特别实用。另一个例子可以在第 6.1 节中看到。

3.5  虚方法

我们可以使用关键字 virtual 声明一个方法,但不实际定义它。这个方法将在随后的子类中提供。这种方法被称为虚方法。包含虚方法的类必须被包换 virtual 标记,并且不能被实例化(也就是说,不能创建该类的任何对象)。该类的定义仍然会定义类型缩写(将虚方法视为其他方法)。

class virtual abstract_point x_init = object (self) method virtual get_x : int method get_offset = self#get_x - x_init method virtual move : int -> unit end;;
class virtual abstract_point : int -> object method get_offset : int method virtual get_x : int method virtual move : int -> unit end
class point x_init = object inherit abstract_point x_init val mutable x = x_init method get_x = x method move d = x <- x + d end;;
class point : int -> object val mutable x : int method get_offset : int method get_x : int method move : int -> unit end

实例变量也可以被声明为虚变量,其效果与方法相同。

class virtual abstract_point2 = object val mutable virtual x : int method move d = x <- x + d end;;
class virtual abstract_point2 : object val mutable virtual x : int method move : int -> unit end
class point2 x_init = object inherit abstract_point2 val mutable x = x_init method get_offset = x - x_init end;;
class point2 : int -> object val mutable x : int method get_offset : int method move : int -> unit end

3.6  私有方法

私有方法是不出现在对象接口中的方法,它们只能被同一对象的其他方法调用。

class restricted_point x_init = object (self) val mutable x = x_init method get_x = x method private move d = x <- x + d method bump = self#move 1 end;;
class restricted_point : int -> object val mutable x : int method bump : unit method get_x : int method private move : int -> unit end
let p = new restricted_point 0;;
val p : restricted_point = <obj>
p#move 10 ;;
Error: This expression has type restricted_point It has no method move
p#bump;;
- : unit = ()

请注意,这与 Java 或 C++ 中的私有方法和受保护方法不同,后者可以从同一类的其他对象调用。这是 OCaml 中类型和类之间独立性的直接结果:两个不相关的类可能产生相同类型的对象,这意味着我们无法在类型上确保某一对象来自特定的类。然而,在第 3.17 节中给出了友元的编写方式。

私有方法是会被子类继承的(默认情况下它们在子类中是可见的),除非它们被签名隐藏,如下所示。

同时,私有方法可以在子类中被转化为公有。

class point_again x = object (self) inherit restricted_point x method virtual move : _ end;;
class point_again : int -> object val mutable x : int method bump : unit method get_x : int method move : int -> unit end

这里的 virtual 标记只用于声明一个方法,但不提供其定义。由于我们没有添加私有标记,这使得该方法成为公有的,并保留了原始定义。

另一种定义方式是

class point_again x = object (self : < move : _; ..> ) inherit restricted_point x end;;
class point_again : int -> object val mutable x : int method bump : unit method get_x : int method move : int -> unit end

self 的类型约束要求使用公有的 move 方法,这足以重载 private 标记。

可以认为私有方法应该在子类中保持私有。然而,由于该方法在子类中是可见的,所以总是可以选取它的代码并定义运行该代码的同名方法,因此另一个(较重的)解决方案是:

class point_again x = object inherit restricted_point x as super method move = super#move end;;
class point_again : int -> object val mutable x : int method bump : unit method get_x : int method move : int -> unit end

当然,私有方法也可以是虚拟的。其关键字必须按此顺序出现 method private virtual

3.7  类接口

类接口是从类定义中推导出来的。它们也可以直接被定义,并用于限制类的类型。像类声明一样,它们还定义了一个新的类型缩写。

class type restricted_point_type = object method get_x : int method bump : unit end;;
class type restricted_point_type = object method bump : unit method get_x : int end
fun (x : restricted_point_type) -> x;;
- : restricted_point_type -> restricted_point_type = <fun>

除了用于程序文档,类接口还可以被用来约束类的类型。具体的实例变量和私有方法都可以通过类的类型约束来隐藏。但是,公有方法和虚拟成员则无法被隐藏。

class restricted_point' x = (restricted_point x : restricted_point_type);;
class restricted_point' : int -> restricted_point_type

或者,相当于:

class restricted_point' = (restricted_point : int -> restricted_point_type);;
class restricted_point' : int -> restricted_point_type

类的接口也可以在模块签名中指定,并用于限制模块的推导签名。

module type POINT = sig class restricted_point' : int -> object method get_x : int method bump : unit end end;;
module type POINT = sig class restricted_point' : int -> object method bump : unit method get_x : int end end
module Point : POINT = struct class restricted_point' = restricted_point end;;
module Point : POINT

3.8  继承

我们通过定义从 point 类继承的 colored_point 类来说明继承。这个类具备 point 类中的所有的实例变量和方法,与一个新的实例变量 c 和一个新的 color 方法。

class colored_point x (c : string) = object inherit point x val c = c method color = c end;;
class colored_point : int -> string -> object val c : string val mutable x : int method color : string method get_offset : int method get_x : int method move : int -> unit end
let p' = new colored_point 5 "red";;
val p' : colored_point = <obj>
p'#get_x, p'#color;;
- : int * string = (5, "red")

point 和 colored_point 具有不兼容的类型,因为 point 没有 color 方法。但是,下面的 get_x 函数是一个通用函数,它可以将 get_x 方法应用于任何具有此方法的对象 p(可能还有其他的对象,它们由类型中的省略号表示)。因此,它同时适用于 point 和 colored_point

let get_succ_x p = p#get_x + 1;;
val get_succ_x : < get_x : int; .. > -> int = <fun>
get_succ_x p + get_succ_x p';;
- : int = 8

在定义函数前不需要提前声明所使用的方法,如示例所示:

let set_x p = p#set_x;;
val set_x : < set_x : 'a; .. > -> 'a = <fun>
let incr p = set_x p (get_succ_x p);;
val incr : < get_x : int; set_x : int -> 'a; .. > -> 'a = <fun>

3.9  多继承

OCaml 允许多继承。其中只保留方法的最后一个定义:在子类中重新定义父类中可见的方法会覆盖父类中的定义。可以通过绑定相关的祖先来重用被覆盖方法的定义。下面,super 被绑定到祖先 printable_point 上。super 是一个伪值标识符,只能用于调用超类方法,如 super#print

class printable_colored_point y c = object (self) val c = c method color = c inherit printable_point y as super method! print = print_string "("; super#print; print_string ", "; print_string (self#color); print_string ")" end;;
class printable_colored_point : int -> string -> object val c : string val mutable x : int method color : string method get_x : int method move : int -> unit method print : unit end
let p' = new printable_colored_point 17 "red";;
new point at (10, red) val p' : printable_colored_point = <obj>
p'#print;;
(10, red)- : unit = ()

隐藏在父类中的私有方法是不可见的,因此不会被覆盖。由于初始化器被视为私有方法,所以沿着类层次结构的所有初始化器都将按照引入它们的顺序被执行。

注意,为了清晰起见,方法·print 被通过 method 关键字与感叹号 ! 显式地标记为覆盖另一个定义。如果 print 方法没有覆盖 printable_point 的 print 方法,编译器会抛出一个错误:

object method! m = () end;;
Error: The method `m' has no previous definition

这个显式覆盖标记对 val 和 inherit 也同样适用:

class another_printable_colored_point y c c' = object (self) inherit printable_point y inherit! printable_colored_point y c val! c = c' end;;
class another_printable_colored_point : int -> string -> string -> object val c : string val mutable x : int method color : string method get_x : int method move : int -> unit method print : unit end

3.10  参数化类

可以将 ref 类型作为一个对象来实现。不过以下的定义无法通过类型检查:

class oref x_init = object val mutable x = x_init method get = x method set y = x <- y end;;
Error: Some type variables are unbound in this type: class oref : 'a -> object val mutable x : 'a method get : 'a method set : 'a -> unit end The method get has type 'a where 'a is unbound

报错的原因是至少有一个方法具有多态类型(这里是存储在 ref 单元中值的类型),因此这个类应该是参数化的,或者方法的类型应该被限制为单态类型。类的单态实例可以定义为:

class oref (x_init:int) = object val mutable x = x_init method get = x method set y = x <- y end;;
class oref : int -> object val mutable x : int method get : int method set : int -> unit end

注意,由于直接对象不定义类类型,因此它们没有这样的限制。

let new_oref x_init = object val mutable x = x_init method get = x method set y = x <- y end;;
val new_oref : 'a -> < get : 'a; set : 'a -> unit > = <fun>

另一方面,用于多态 ref 的类必须在其声明中显式列出类型参数。类的类型参数被列在 [ 和 ] 之间。类型参数还必须通过类型约束绑定到类主体中的某个位置。

class ['a] oref x_init = object val mutable x = (x_init : 'a) method get = x method set y = x <- y end;;
class ['a] oref : 'a -> object val mutable x : 'a method get : 'a method set : 'a -> unit end
let r = new oref 1 in r#set 2; (r#get);;
- : int = 2

声明中的类型参数实际上可能在类定义的主体中受到约束。在类类型中,类型参数的实际值在 constraint 子句中显示。

class ['a] oref_succ (x_init:'a) = object val mutable x = x_init + 1 method get = x method set y = x <- y end;;
class ['a] oref_succ : 'a -> object constraint 'a = int val mutable x : int method get : int method set : int -> unit end

让我们考虑一个更复杂的例子:定义一个圆,其圆心可以是任何类型的点。我们在方法 move 中添加了一个额外的类型约束,因为类的类型参数不能忽略任何自由变量。

class ['a] circle (c : 'a) = object val mutable center = c method center = center method set_center c = center <- c method move = (center#move : int -> unit) end;;
class ['a] circle : 'a -> object constraint 'a = < move : int -> unit; .. > val mutable center : 'a method center : 'a method move : int -> unit method set_center : 'a -> unit end

我们可以使用 constraint 子句实现 circle 另一种定义方式。constraint 子句中使用的 #point 类型是由 point 类的定义生成的缩写。这个缩写能够与属于 point 类子类的任何对象的类型相匹配。它实际上可以被扩展为 < get_x : int; move : int -> unit; .. >。因此我们可以给出 circle 的替代定义如下,该定义对其参数有略微强一些的约束,因为我们现在期望 center 具备 get_x 方法。

class ['a] circle (c : 'a) = object constraint 'a = #point val mutable center = c method center = center method set_center c = center <- c method move = center#move end;;
class ['a] circle : 'a -> object constraint 'a = #point val mutable center : 'a method center : 'a method move : int -> unit method set_center : 'a -> unit end

colored_circle 类是 circle 类的一个特别版本,它要求 center 的类型与 #colored_point 一致,并包括方法 color。注意,在定制参数化类时,类型参数的实例必须始终被显式给出。它同样被写在 [ 和 ] 之间。

class ['a] colored_circle c = object constraint 'a = #colored_point inherit ['a] circle c method color = center#color end;;
class ['a] colored_circle : 'a -> object constraint 'a = #colored_point val mutable center : 'a method center : 'a method color : string method move : int -> unit method set_center : 'a -> unit end

3.11  多态方法

注:需校订,我对于部分概念暂时没有印象 by @lkwq007

虽然参数化类的内容可能是具有多态性的,但它们不足以允许方法使用的多态性。

一个经典的例子是迭代器的定义。

List.fold_left;;
- : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>
class ['a] intlist (l : int list) = object method empty = (l = []) method fold f (accu : 'a) = List.fold_left f accu l end;;
class ['a] intlist : int list -> object method empty : bool method fold : ('a -> int -> 'a) -> 'a -> 'a end

乍看之下,我们似乎得到了一个多态迭代器,但是这在实践中不起作用。

let l = new intlist [1; 2; 3];;
val l : '_weak2 intlist = <obj>
l#fold (fun x y -> x+y) 0;;
- : int = 6
l;;
- : int intlist = <obj>
l#fold (fun s x -> s ^ string_of_int x ^ " ") "" ;;
Error: This expression has type int but an expression was expected of type string

在第一次求和运算中,我们的迭代器可以工作。然而,由于对象本身不是多态的(只有它们的构造函数是多态的),因此·fold 方法的使用规定了这个对象的类型。因此,我们将其用作字符串迭代器第二次尝试就失败了。

这里的问题是量化过程被错误地定位了:我们所需要的不是多态类,而是多态的 fold 方法。这可以通过在方法定义中显式地提供多态类型来实现。

class intlist (l : int list) = object method empty = (l = []) method fold : 'a. ('a -> int -> 'a) -> 'a -> 'a = fun f accu -> List.fold_left f accu l end;;
class intlist : int list -> object method empty : bool method fold : ('a -> int -> 'a) -> 'a -> 'a end
let l = new intlist [1; 2; 3];;
val l : intlist = <obj>
l#fold (fun x y -> x+y) 0;;
- : int = 6
l#fold (fun s x -> s ^ string_of_int x ^ " ") "";;
- : string = "1 2 3 "

正如您在编译器显示的类类型中所看到的,虽然多态方法类型在类定义中必须是显示给出的(紧跟在方法名称之后),但是限定的类型变量可以隐式地留在类描述中。为什么要求类型是显式的呢?这里的问题是 (int -> int -> int) -> int -> int 对于 fold 同样也是一个有效的类型,而它恰好与我们所给出的多态类型不兼容(自动实例化仅适用于顶层的类型变量,对于内层的限定符并不适用,因此内层类型的选取变成了一个不可判定问题)。因此编译器不能在这两种类型之间进行选择,必须获得额外的帮助。

但是,如果类型是已知的,则可以通过继承或 self 上的类型约束在类定义中完全省略该类型。下面是一个方法重载的例子。

class intlist_rev l = object inherit intlist l method! fold f accu = List.fold_left f accu (List.rev l) end;;

下面的习惯用法将描述和定义分开。

class type ['a] iterator = object method fold : ('b -> 'a -> 'b) -> 'b -> 'b end;;
class intlist' l = object (self : int #iterator) method empty = (l = []) method fold f accu = List.fold_left f accu l end;;

注意这里 (self : int #iterator) 的用法,它可以确保这个对象实现 iterator 接口。

多态方法的调用方式与普通方法完全相同,但是您应该了解类型推导的一些限制。也就是说,一个多态方法只能在其类型已知时被调用。否则,该方法将被假定为单态的,并且被给定一个不兼容的类型。

let sum lst = lst#fold (fun x y -> x+y) 0;;
val sum : < fold : (int -> int -> int) -> int -> 'a; .. > -> 'a = <fun>
sum l ;;
Error: This expression has type intlist but an expression was expected of type < fold : (int -> int -> int) -> int -> 'a; .. > Types for method fold are incompatible

解决方法很简单:在参数上给出类型约束。

let sum (lst : _ #iterator) = lst#fold (fun x y -> x+y) 0;;
val sum : int #iterator -> int = <fun>

当然,约束也可以是显式方法类型,只需要出现限定的变量。

let sum lst = (lst : < fold : 'a. ('a -> _ -> 'a) -> 'a -> 'a; .. >)#fold (+) 0;;
val sum : < fold : 'a. ('a -> int -> 'a) -> 'a -> 'a; .. > -> int = <fun>

多态方法的另一个用途是允许用户在方法参数中使用某种形式的隐式子类型。我们已经在第 3.8 节中看到一些函数的如何在它们的参数类中是多态的。这在方法中同样适用。

class type point0 = object method get_x : int end;;
class type point0 = object method get_x : int end
class distance_point x = object inherit point x method distance : 'a. (#point0 as 'a) -> int = fun other -> abs (other#get_x - x) end;;
class distance_point : int -> object val mutable x : int method distance : #point0 -> int method get_offset : int method get_x : int method move : int -> unit end
let p = new distance_point 3 in (p#distance (new point 8), p#distance (new colored_point 1 "blue"));;
- : int * int = (5, 2)

注意这里的特殊语法  (#point0 as 'a),我们必须使用它来限定 #point0 的扩展部分。而可变绑定器是可以在类定义中被省略的。如果您想在对象的字段中实现多态性,则必须独立地对其加以限定。

class multi_poly = object method m1 : 'a. (< n1 : 'b. 'b -> 'b; .. > as 'a) -> _ = fun o -> o#n1 true, o#n1 "hello" method m2 : 'a 'b. (< n2 : 'b -> bool; .. > as 'a) -> 'b -> _ = fun o x -> o#n2 x end;;
class multi_poly : object method m1 : < n1 : 'b. 'b -> 'b; .. > -> bool * string method m2 : < n2 : 'b -> bool; .. > -> 'b -> bool end

在方法 m1 中,o 必须是具有方法 n1 的对象,其本身具有多态性。在方法 m2 中,n2 的参数和 x 必须具有相同的类型,其量化级别与 'a 相同。

3.12  使用强制转换

注:需校订,我对于部分概念暂时没有印象 by @lkwq007

子类型在 OCaml 中并不是隐式的。但是其中有两种使用子类型的方法。最一般的构造方法是完全显式的:类型强制的域与上域都必须被给出。

我们已经看到,点(point 类型)和有色点(colored_point 类型)具有不兼容的类型。例如,它们不能被混合放在同一个列表中。然而,我可以通过隐藏彩色点的 color 方法的方式将它强制转换为一个点:

let colored_point_to_point cp = (cp : colored_point :> point);;
val colored_point_to_point : colored_point -> point = <fun>
let p = new point 3 and q = new colored_point 4 "blue";;
val p : point = <obj> val q : colored_point = <obj>
let l = [p; (colored_point_to_point q)];;
val l : point list = [<obj>; <obj>]

只有当 t 是 t' 的子类型时,一个类型为 t 的对象才能被视为类型为 t' 的对象。例如,一个点不能被看作是一个有颜色的点。

(p : point :> colored_point);;
Error: Type point = < get_offset : int; get_x : int; move : int -> unit > is not a subtype of colored_point = < color : string; get_offset : int; get_x : int; move : int -> unit > The first object type has no method color

Indeed, narrowing coercions without runtime checks would be unsafe. Runtime type checks might raise exceptions, and they would require the presence of type information at runtime, which is not the case in the OCaml system. For these reasons, there is no such operation available in the language.

Be aware that subtyping and inheritance are not related. Inheritance is a syntactic relation between classes while subtyping is a semantic relation between types. For instance, the class of colored points could have been defined directly, without inheriting from the class of points; the type of colored points would remain unchanged and thus still be a subtype of points.

The domain of a coercion can often be omitted. For instance, one can define:

let to_point cp = (cp :> point);;
val to_point : #point -> point = <fun>

In this case, the function colored_point_to_point is an instance of the function to_point. This is not always true, however. The fully explicit coercion is more precise and is sometimes unavoidable. Consider, for example, the following class:

class c0 = object method m = {< >} method n = 0 end;;
class c0 : object ('a) method m : 'a method n : int end

The object type c0 is an abbreviation for <m : 'a; n : int> as 'a. Consider now the type declaration:

class type c1 = object method m : c1 end;;
class type c1 = object method m : c1 end

The object type c1 is an abbreviation for the type <m : 'a> as 'a. The coercion from an object of type c0 to an object of type c1 is correct:

fun (x:c0) -> (x : c0 :> c1);;
- : c0 -> c1 = <fun>

However, the domain of the coercion cannot always be omitted. In that case, the solution is to use the explicit form. Sometimes, a change in the class-type definition can also solve the problem

class type c2 = object ('a) method m : 'a end;;
class type c2 = object ('a) method m : 'a end
fun (x:c0) -> (x :> c2);;
- : c0 -> c2 = <fun>

While class types c1 and c2 are different, both object types c1 and c2 expand to the same object type (same method names and types). Yet, when the domain of a coercion is left implicit and its co-domain is an abbreviation of a known class type, then the class type, rather than the object type, is used to derive the coercion function. This allows leaving the domain implicit in most cases when coercing form a subclass to its superclass. The type of a coercion can always be seen as below:

let to_c1 x = (x :> c1);;
val to_c1 : < m : #c1; .. > -> c1 = <fun>
let to_c2 x = (x :> c2);;
val to_c2 : #c2 -> c2 = <fun>

Note the difference between these two coercions: in the case of to_c2, the type #c2 = < m : 'a; .. > as 'a is polymorphically recursive (according to the explicit recursion in the class type of c2); hence the success of applying this coercion to an object of class c0. On the other hand, in the first case, c1 was only expanded and unrolled twice to obtain < m : < m : c1; .. >; .. > (remember #c1 = < m : c1; .. >), without introducing recursion. You may also note that the type of to_c2 is #c2 -> c2 while the type of to_c1 is more general than #c1 -> c1. This is not always true, since there are class types for which some instances of #c are not subtypes of c, as explained in section 3.16. Yet, for parameterless classes the coercion (_ :> c) is always more general than (_ : #c :> c).

A common problem may occur when one tries to define a coercion to a class c while defining class c. The problem is due to the type abbreviation not being completely defined yet, and so its subtypes are not clearly known. Then, a coercion (_ :> c) or (_ : #c :> c) is taken to be the identity function, as in

function x -> (x :> 'a);;
- : 'a -> 'a = <fun>

As a consequence, if the coercion is applied to self, as in the following example, the type of self is unified with the closed type c (a closed object type is an object type without ellipsis). This would constrain the type of self be closed and is thus rejected. Indeed, the type of self cannot be closed: this would prevent any further extension of the class. Therefore, a type error is generated when the unification of this type with another type would result in a closed object type.

class c = object method m = 1 end and d = object (self) inherit c method n = 2 method as_c = (self :> c) end;;
Error: This expression cannot be coerced to type c = < m : int >; it has type < as_c : c; m : int; n : int; .. > but is here used with type c Self type cannot escape its class

However, the most common instance of this problem, coercing self to its current class, is detected as a special case by the type checker, and properly typed.

class c = object (self) method m = (self :> c) end;;
class c : object method m : c end

This allows the following idiom, keeping a list of all objects belonging to a class or its subclasses:

let all_c = ref [];;
val all_c : '_weak3 list ref = {contents = []}
class c (m : int) = object (self) method m = m initializer all_c := (self :> c) :: !all_c end;;
class c : int -> object method m : int end

This idiom can in turn be used to retrieve an object whose type has been weakened:

let rec lookup_obj obj = function [] -> raise Not_found | obj' :: l -> if (obj :> < >) = (obj' :> < >) then obj' else lookup_obj obj l ;;
val lookup_obj : < .. > -> (< .. > as 'a) list -> 'a = <fun>
let lookup_c obj = lookup_obj obj !all_c;;
val lookup_c : < .. > -> < m : int > = <fun>

The type < m : int > we see here is just the expansion of c, due to the use of a reference; we have succeeded in getting back an object of type c.


The previous coercion problem can often be avoided by first defining the abbreviation, using a class type:

class type c' = object method m : int end;;
class type c' = object method m : int end
class c : c' = object method m = 1 end and d = object (self) inherit c method n = 2 method as_c = (self :> c') end;;
class c : c' and d : object method as_c : c' method m : int method n : int end

It is also possible to use a virtual class. Inheriting from this class simultaneously forces all methods of c to have the same type as the methods of c'.

class virtual c' = object method virtual m : int end;;
class virtual c' : object method virtual m : int end
class c = object (self) inherit c' method m = 1 end;;
class c : object method m : int end

One could think of defining the type abbreviation directly:

type c' = <m : int>;;

However, the abbreviation #c' cannot be defined directly in a similar way. It can only be defined by a class or a class-type definition. This is because a #-abbreviation carries an implicit anonymous variable .. that cannot be explicitly named. The closer you get to it is:

type 'a c'_class = 'a constraint 'a = < m : int; .. >;;

使用一个额外的类型变量捕获打开的对象类型。

3.13  函数式对象

事实上,我们不需要对实例变量赋值,就可以获得 point 类的一个对象。重载结构 {< ... >} 可以返回“self”(即当前对象)的副本,在此过程中可以更改某些实例变量的值。

class functional_point y = object val x = y method get_x = x method move d = {< x = x + d >} end;;
class functional_point : int -> object ('a) val x : int method get_x : int method move : int -> 'a end
let p = new functional_point 7;;
val p : functional_point = <obj>
p#get_x;;
- : int = 7
(p#move 3)#get_x;;
- : int = 10
p#get_x;;
- : int = 7

注意,类型缩写 functional_point 是递归的。我们可以在 functional_point 的类类型中看到它:self 的类型是 'a,而  'a 出现在方法 move 的类型中。

functional_point 的上述定义不同于以下内容:

class bad_functional_point y = object val x = y method get_x = x method move d = new bad_functional_point (x+d) end;;
class bad_functional_point : int -> object val x : int method get_x : int method move : int -> bad_functional_point end

虽然这两个类的对象的行为是相同的,但是它们的子类的对象是不同的。在 bad_functional_point 的子类中,方法 move 将继续返回父类的一个对象。相反,在 functional_point 的子类中,move 方法将会返回子类的一个对象。

如 6.2.1 节所示,函数式更新通常与二元方法一起使用。

3.14  克隆对象

对象也可以被克隆,无论它们是函数式的还是命令式的。库函数 Oo.copy 能够生成对象的浅拷贝。也就是说,它可以返回一个新对象,该对象具有与其参数相同的方法和实例变量。其中实例变量被复制,但它们的内容是被共享的。将新值赋给副本的实例变量(使用方法调用)不会影响原始实例变量,反之亦然。更深层次的赋值(例如,如果实例变量是 ref 单元格)将会自然地同时影响原始和副本。

Oo.copy 的类型如下所示:

Oo.copy;;
- : (< .. > as 'a) -> 'a = <fun>

该类型中的 as 关键字将类型变量 'a 绑定到对象类型 < .. >。因此,Oo.copy 可以接受具有任何方法的对象(由省略号表示),并返回相同类型的对象。Oo.copy 的类型与 < .. > -> < .. > 类型不同,其中每个省略号表示一组不同的方法。在这里省略号实际上表现为一个类型变量。

let p = new point 5;;
val p : point = <obj>
let q = Oo.copy p;;
val q : point = <obj>
q#move 7; (p#get_x, q#get_x);;
- : int * int = (5, 12)

事实上,Oo.copy p 将会表现为 p#copy。这里假设在 p 类的结构体 {< >} 中定义了一个公共方法 copy

对象可以使用泛型比较函数 = 和 <> 进行比较。两个对象相等当且仅当它们在物理上相等。需要特别说明的是,一个对象和它的副本是不相等的。

let q = Oo.copy p;;
val q : point = <obj>
p = q, p = p;;
- : bool * bool = (false, true)

其他的泛型比较,如(<<= 等)也可以用于对象。关系 < 定义了一类未被指定但严格的对象次序。两个对象之间的次序关系在这两个对象被创建之后被是恒定的,其不受字段变化的影响。

克隆和重载有一个非空的交集。当它们在对象中被使用且不覆盖任何字段时,它们是可以互换的:

class copy = object method copy = {< >} end;;
class copy : object ('a) method copy : 'a end
class copy = object (self) method copy = Oo.copy self end;;
class copy : object ('a) method copy : 'a end

只有重载操作可以用于实际重写字段,同时也只有 Oo.copy 原型可以在外部使用。

克隆还可以用于提供保存和恢复对象状态的机制。

class backup = object (self : 'mytype) val mutable copy = None method save = copy <- Some {< copy = None >} method restore = match copy with Some x -> x | None -> self end;;
class backup : object ('a) val mutable copy : 'a option method restore : 'a method save : unit end

上面的定义只会备份一层。我们可以通过多继承将备份机制添加到任何类中。

class ['a] backup_ref x = object inherit ['a] oref x inherit backup end;;
class ['a] backup_ref : 'a -> object ('b) val mutable copy : 'b option val mutable x : 'a method get : 'a method restore : 'b method save : unit method set : 'a -> unit end
let rec get p n = if n = 0 then p # get else get (p # restore) (n-1);;
val get : (< get : 'b; restore : 'a; .. > as 'a) -> int -> 'b = <fun>
let p = new backup_ref 0 in p # save; p # set 1; p # save; p # set 2; [get p 0; get p 1; get p 2; get p 3; get p 4];;
- : int list = [2; 1; 1; 1; 1]

我们可以定义一个保留所有副本的备份的变体。(我们还添加了一个清除所有副本的方法 clear

class backup = object (self : 'mytype) val mutable copy = None method save = copy <- Some {< >} method restore = match copy with Some x -> x | None -> self method clear = copy <- None end;;
class backup : object ('a) val mutable copy : 'a option method clear : unit method restore : 'a method save : unit end
class ['a] backup_ref x = object inherit ['a] oref x inherit backup end;;
class ['a] backup_ref : 'a -> object ('b) val mutable copy : 'b option val mutable x : 'a method clear : unit method get : 'a method restore : 'b method save : unit method set : 'a -> unit end
let p = new backup_ref 0 in p # save; p # set 1; p # save; p # set 2; [get p 0; get p 1; get p 2; get p 3; get p 4];;
- : int list = [2; 1; 0; 0; 0]

3.15  递归类

递归类可被用于定义类型相互递归的对象。

class window = object val mutable top_widget = (None : widget option) method top_widget = top_widget end and widget (w : window) = object val window = w method window = window end;;
class window : object val mutable top_widget : widget option method top_widget : widget option end and widget : window -> object val window : window method window : window end

尽管它们的类型是相互递归的,但是 widget 类和 window 类其本身是独立的。

3.16  二元方法

二元方法是一类参数与 self 具有相同类型的的方法。以下的 comparable 类是一个具有二元方法 leq 的类模板,leq 的类型为 'a -> bool,其中类型变量 'a 被绑定到 self 的类型。因此,#comparable 被拓展为为 < leq : 'a -> bool; .. > as 'a。在这里我们看到绑定器 as 同样允许编写递归类型。

class virtual comparable = object (_ : 'a) method virtual leq : 'a -> bool end;;
class virtual comparable : object ('a) method virtual leq : 'a -> bool end

然后我们定义了 comparable 子类 moneymoney 类仅仅将浮点数封装为可比较的对象,我们将在下面对其进行扩展。在这里,我们必须对类参数 x 使用类型约束,因为原型 <= 是 OCaml 中的一个多态函数。inherit 子句确保该类的对象类型是 #comparable 的实例。

class money (x : float) = object inherit comparable val repr = x method value = repr method leq p = repr <= p#value end;;
class money : float -> object ('a) val repr : float method leq : 'a -> bool method value : float end

注意类型 money 不是类型 comparable 的子类型,因为 self 的类型在方法 leq 的类型中是逆变的。事实上,类 money 的对象 m 有一个 leq 方法,该方法需要 money 类型的参数,因为它访问了参数的 value 方法。考虑到 m 的 comparable 类型会允许在 m 上使用一个没有 value 方法的参数调用 leq 方法,这将会产生一个错误。

同样,下面的 money2 类型不是 money 类型的子类型。

class money2 x = object inherit money x method times k = {< repr = k *. repr >} end;;
class money2 : float -> object ('a) val repr : float method leq : 'a -> bool method times : float -> 'a method value : float end

然而,我们可以定义操作 money 或 money2 类型对象的函数:函数 min 将返回类型与 #comparable 一致的任何两个对象的最小值。min 的类型与 #comparable -> #comparable -> #comparable 不同,因为缩写 #comparable 隐藏了一个类型变量(省略号)。该缩写的每次出现都会产生一个新的变量。

let min (x : #comparable) y = if x#leq y then x else y;;
val min : (#comparable as 'a) -> 'a -> 'a = <fun>

这个函数可以应用于 money 或 money2 类型的对象。

(min (new money 1.3) (new money 3.1))#value;;
- : float = 1.3
(min (new money2 5.0) (new money2 3.14))#value;;
- : float = 3.14

更多二元方法的例子可以在第 6.2.1 节和第 6.2.3 节中找到

对 times 方法使用重载时需要注意。将其写作 new money2 (k *. repr) 而非 {< repr = k *. repr >} 将不能妥当地处理继承:在 money2 的子类 money3 中,times 方法将返回 money2 类的对象,而不像预期的那样返回 money3 类的对象。

money 类可以自然地包含另一种二元方法。这是一种直接的定义:

class money x = object (self : 'a) val repr = x method value = repr method print = print_float repr method times k = {< repr = k *. x >} method leq (p : 'a) = repr <= p#value method plus (p : 'a) = {< repr = x +. p#value >} end;;
class money : float -> object ('a) val repr : float method leq : 'a -> bool method plus : 'a -> 'a method print : unit method times : float -> 'a method value : float end

3.17  友元

上面的 money 类揭示了使用二元方法时常出现的一个问题。为了与同一类的其他对象进行交互,必须使用一个形如 value 的方法使 money 对象的表示暴漏出来。如果我们移除其中全部的二进制方法(在这里为 plus 和 leq),也可以通过删除 value 方法来将该表示隐藏在对象中。然而,当某些二进制方法需要访问相同类(除了 self)的对象的表示时,这是无法实现的。

class safe_money x = object (self : 'a) val repr = x method print = print_float repr method times k = {< repr = k *. x >} end;;
class safe_money : float -> object ('a) val repr : float method print : unit method times : float -> 'a end

在这里,对象的表示只有特定对象知道。为了使其对同一类的其他对象是可用的,我们必须使它对“整个世界”可用。但是,我们可以很容易地使用模块系统来限制表示的可见性。

module type MONEY = sig type t class c : float -> object ('a) val repr : t method value : t method print : unit method times : float -> 'a method leq : 'a -> bool method plus : 'a -> 'a end end;;
module Euro : MONEY = struct type t = float class c x = object (self : 'a) val repr = x method value = repr method print = print_float repr method times k = {< repr = k *. x >} method leq (p : 'a) = repr <= p#value method plus (p : 'a) = {< repr = x +. p#value >} end end;;

友元函数的另一个例子可以在第 6.2.3 节中找到。当一组对象(这里是同一类的对象)和函数需要看到彼此的内部表示,而它们的表示应该对外部隐藏时,这些示例可以用作参考。解决方案始终是在同一个模块中定义所有友元,对表示进行访问,并使用签名约束使表示在模块外部变得抽象。


Previous Up Next