第 3 章 OCaml 中的对象



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

请注意,OCaml 中的对象、类和类型的关系与 Java 和 C++ 等主流面向对象语言不同,因此不应假设类似的关键字具有相同的含义。OCaml 中的面向对象特性使用频率远低于这些语言。OCaml 有其他通常更合适的替代方案,例如模块和函子。实际上,许多 OCaml 程序根本不使用对象。

1 类和对象

下面的 point 类定义了一个实例变量 x 和两个方法 get_xmove。实例变量的初始值为 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

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

# 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

也可以对 x 坐标的初始值抽象出 point 类。

# 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 节所述。

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 自我引用

方法或初始化器可以调用自身上的方法(即当前对象)。为此,`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: 此表达式类型为 < n : int; register : 'a; .. >,但预期表达式类型为 'weak1 自身类型无法逃逸其类

您可以忽略错误消息的前两行。重要的是最后一行:将 `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>

4 初始化器

类定义中的 `let` 绑定在对象构建之前进行求值。也可以在对象构建后立即求值表达式。此类代码被写成一个名为初始化器的匿名隐藏方法。因此,它可以访问 `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>

初始化器不可被覆盖。相反,所有初始化器都按顺序进行求值。初始化器对于强制执行不变量特别有用。在第 ‍8.1 节中可以看到另一个示例。

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

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: 此表达式类型为 restricted_point,它没有 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` 仅用于提及方法而不提供其定义。由于我们没有添加 `private` 注释,这使得该方法变为公共方法,保留了原始定义。

另一种定义是

# 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`。

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

8 继承

我们通过定义一个继承自点类的彩色点类来说明继承。这个类具有点类所有实例变量和所有方法,以及一个新的实例变量 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")

点和彩色点类型不兼容,因为点没有方法 color。但是,下面的函数 get_x 是一个泛型函数,它将方法 get_x 应用于任何具有此方法的对象 p(以及可能的其他一些方法,在类型中用省略号表示)。因此,它适用于点和彩色点。

# 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>

9 多重继承

允许多重继承。仅保留最后一个方法定义:子类中对父类可见方法的重新定义会覆盖父类中的定义。可以通过绑定相关的祖先来重用先前的方法定义。下面,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_pointprint 方法,编译器会抛出错误

# object method! m = () end;;
Error: 方法 m 没有先前的定义

此显式覆盖注释也适用于 valinherit

# 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

10 参数化类

引用单元格可以实现为对象。朴素的定义无法通过类型检查

# class oref x_init = object val mutable x = x_init method get = x method set y = x <- y end;;
Error: 此类型中存在一些未绑定的类型变量:class oref : 'a -> object val mutable x : 'a method get : 'a method set : 'a -> unit end 方法 get 的类型为 'a,其中 'a 未绑定

原因是至少一个方法具有多态类型(此处,引用单元格中存储的值的类型),因此类应该参数化,或者方法类型应该被限制为单态类型。可以定义类的单态实例,方法如下

# 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>

另一方面,用于多态引用的类必须在其声明中显式列出类型参数。类类型参数列在 [] 之间。类型参数还必须在类体中的某处通过类型约束进行绑定。

# 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

以下展示了 circle 的另一种定义,它在类定义中使用了 constraint 子句。下面在 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 的一个专门版本,它要求中心的类型与 #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

11 多态方法

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

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

# List.fold_left;;
- : ('acc -> 'a -> 'acc) -> 'acc -> 'a list -> 'acc = <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 ^ Int.to_string x ^ " ") "" ;;
Error: 此表达式类型为 int,但期望表达式类型为 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 ^ Int.to_string x ^ " ") "";;
- : string = "1 2 3 "

正如你在编译器显示的类类型中看到的,虽然多态方法类型必须在类定义中完全明确(出现在方法名称之后),但在类描述中可以省略量化的类型变量。为什么要要求类型明确?问题是 (int -> int -> int) -> int -> int 也会是 fold 的一个有效类型,并且它恰好与我们给出的多态类型不兼容(自动实例化仅适用于顶层类型变量,不适用于内部量词,在那里它变成了一个不可判定的问题)。因此,编译器无法在这两种类型之间进行选择,需要帮助。

但是,如果类型已经通过继承或对自身的类型约束而已知,则可以在类定义中完全省略该类型。以下是一个方法重写示例。

# 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: 此表达式类型为 intlist,但期望表达式类型为 < fold : (int -> int -> int) -> int -> 'b; .. > 方法 fold 的类型为 'a. ('a -> int -> 'a) -> 'a -> 'a,但期望的方法类型为 (int -> int -> int) -> int -> 'b

解决方法很简单:您应该对参数添加一个类型约束。

# 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 中,n2x 的参数必须具有相同的类型,该类型与 'a 在同一级别量化。

12 使用强制转换

子类型化从不隐式进行。然而,有两种方法可以执行子类型化。最通用的构造是完全显式的:必须给出类型强制转换的域和值域。

我们已经看到,点和彩色点具有不兼容的类型。例如,它们不能混合在同一个列表中。但是,彩色点可以强制转换为点,隐藏其 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>]

仅当 tt' 的子类型时,类型 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

实际上,在没有运行时检查的情况下缩小强制转换是不安全的。运行时类型检查可能会引发异常,并且需要在运行时存在类型信息,而 OCaml 系统中没有这种情况。由于这些原因,语言中没有这样的操作可用。

请注意,子类型化和继承并不相关。继承是类之间的语法关系,而子类型化是类型之间的语义关系。例如,彩色点的类可以直接定义,而不必继承自点的类;彩色点的类型将保持不变,因此仍然是点的子类型。

强制转换的域通常可以省略。例如,可以定义

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

在这种情况下,函数 colored_point_to_point 是函数 to_point 的实例。但是,这并不总是正确的。完全显式的强制转换更精确,有时是不可避免的。例如,考虑以下类

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

对象类型 c0<m : 'a; n : int> as 'a 的缩写。现在考虑类型声明

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

对象类型 c1 是类型 <m : 'a> as 'a 的缩写。从类型 c0 的对象到类型 c1 的对象的强制转换是正确的

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

但是,强制转换的域并不总是可以省略。在这种情况下,解决方案是使用显式形式。有时,类类型定义的更改也可以解决问题

# 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>

虽然类类型 c1c2 不同,但对象类型 c1c2 都扩展到相同的对象类型(相同的方法名和类型)。然而,当强制转换的域被隐式地省略,而其值域是已知类类型的缩写时,则类类型而不是对象类型用于推导出强制转换函数。这允许在大多数情况下从子类强制转换为其超类时省略域。强制转换的类型总是可以看作如下

# 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>

注意这两个强制转换之间的区别:在 to_c2 的情况下,类型 #c2 = < m : 'a; .. > as 'a 是多态递归的(根据 c2 类类型中的显式递归);因此,将此强制转换应用于类 c0 的对象是成功的。另一方面,在第一种情况下,c1 仅被扩展和展开两次以获得 < m : < m : c1; .. >; .. >(记住 #c1 = < m : c1; .. >),没有引入递归。你可能还注意到 to_c2 的类型是 #c2 -> c2,而 to_c1 的类型比 #c1 -> c1 更通用。这并不总是正确的,因为存在一些类类型,其 #c 的某些实例不是 c 的子类型,如第 ‍3.16 节所述。然而,对于无参数类,强制转换 (_ :> c) 总是比 (_ : #c :> c) 更通用。

当尝试定义一个强制转换为类 c 的强制转换时,可能会出现一个常见的问题,同时定义了类 c。问题在于类型缩写尚未完全定义,因此其子类型尚不清楚。然后,强制转换 (_ :> c)(_ : #c :> c) 被视为恒等函数,如

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

因此,如果强制转换应用于 self,如以下示例所示,self 的类型将与封闭类型 c 统一(封闭对象类型是没有省略号的对象类型)。这将限制 self 的类型为封闭类型,因此被拒绝。实际上,self 的类型不能是封闭的:这将阻止类的任何进一步扩展。因此,当此类型与另一个类型的统一会导致封闭对象类型时,就会生成类型错误。

# 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

但是,此问题最常见的实例,将 self 强制转换为其当前类,由类型检查器检测为特例,并被正确类型化。

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

这允许使用以下习惯用法,保留属于类或其子类的所有对象的列表

# 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

这种习惯用法可以用来检索类型已被弱化的对象

# 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>

我们在这里看到的 < m : int > 类型只是 c 的展开,这是由于使用了引用;我们已经成功地获取了 c 类型的对象。


先定义缩写,使用类类型,通常可以避免之前的强制类型转换问题。

# 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

也可以使用虚拟类。从这个类继承会同时强制 c 的所有方法与 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

人们可能会想到直接定义类型缩写。

# type c' = <m : int>;

然而,缩写 #c' 不能以类似的方式直接定义。它只能通过类或类类型定义来定义。这是因为 # 缩写隐含了一个匿名变量 ..,它不能被显式命名。你所能接近的就是

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

其中有一个额外的类型变量捕获了开放的对象类型。

13 函数式对象

可以编写一个不包含实例变量赋值的 point 类版本。覆盖结构 {< ... >} 返回一个“self”(即当前对象)的副本,可能还会更改一些实例变量的值。

# class functional_point y = object val x = y method get_x = x method move d = {< x = x + d >} method move_to x = {< x >} end;;
class functional_point : int -> object ('a) val x : int method get_x : int method move : int -> 'a method move_to : 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#move_to 15)#get_x;;
- : int = 15
# p#get_x;;
- : int = 7

与记录类似,形式 {< x >}{< x = x >} 的省略形式,避免了重复实例变量名称。注意,类型缩写 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) method move_to x = new bad_functional_point x end;;
class bad_functional_point : int -> object val x : int method get_x : int method move : int -> bad_functional_point method move_to : int -> bad_functional_point end

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

函数式更新通常与二元方法一起使用,如第 ‍8.2.1 节中所述。

14 克隆对象

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

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]

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

虽然它们的类型是相互递归的,但类 widgetwindow 本身是独立的。

16 二元方法

二元方法是接受与 self 类型相同的参数的方法。下面 comparable 类是具有类型为 'a -> bool 的二元方法 leq 的类的模板,其中类型变量 '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 的子类 money。类 money 只是将浮点数作为可比较对象包装。1 我们将在下面使用更多操作扩展 money。由于原始的 <= 是 OCaml 中的多态函数,因此我们必须对类参数 x 使用类型约束。 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 上调用方法 leq,其参数没有方法 value,这将是一个错误。

同样,下面类型 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

然而,可以定义操作类型为 moneymoney2 的对象的函数:函数 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>

此函数可以应用于类型为 moneymoney2 的对象。

# (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

可以在部分 ‍8.2.1 和 ‍8.2.3 中找到更多二元方法的示例。

请注意对方法 times 使用 override。编写 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

17 朋友

上述类 money 揭示了二元方法中经常出现的一个问题。为了与同一类的其他对象交互,必须通过 value 等方法公开 money 对象的表示。如果我们移除所有二元方法(这里指的是 plusleq),通过移除 value 方法,就可以轻松地将表示隐藏在对象内部。但是,只要某个二元方法需要访问同一类对象(除自身外)的表示,就无法实现这一点。

# 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;;

在第 ‍8.2.3 节中可以找到另一个朋友函数的示例。这些示例发生在当一组对象(这里指的是同一类的对象)和函数应该能够看到彼此的内部表示,而它们的表示应该对外部隐藏时。解决方法始终是在同一个模块中定义所有朋友,授予访问表示的权限,并使用签名约束使表示在模块外部抽象。


1
浮点数是对十进制数的近似值,它们不适合用于大多数货币计算,因为它们可能会引入错误。

(本章由 Jérôme Vouillon、Didier Rémy 和 Jacques Garrigue 撰写)