第 14 章:类(Classes)
原文:Anil Madhavapeddy and Yaron Minsky, Real World OCaml: Functional Programming for the Masses, Second Edition, Chapter 14。维护者已确认本书为开源书籍,可翻译并发布用于学习研究。
本章由 Leo White 和 Jason Hickey 撰写。
直接使用对象很适合做封装,但面向对象编程的主要目标之一,是通过继承实现代码复用。为了使用继承,就需要引入类(classes)。在面向对象编程中,类本质上是创建对象的配方。这个配方可以通过添加新的方法和字段来改变,也可以通过修改已有方法来改变。
14.1 OCaml 类(OCaml Classes)
在 OCaml 中,类定义必须作为模块中的顶层语句出现。类定义使用关键字 class:
# open Base;;
# class istack = object
val mutable v = [0; 2]
method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None
method push hd =
v <- hd :: v
end;;
class istack :
object
val mutable v : int list
method pop : int option
method push : int -> unit
end
结果中的 class istack : object ... end 表明我们创建了一个名为 istack 的类,它的类类型(class type)是 object ... end。和模块类型一样,类类型与普通 OCaml 类型(例如 int、string 和 list)完全分离,尤其不要把它和对象类型(例如 < get : int; .. >)混淆。类类型描述的是类本身,而不是类所创建的对象。这里的类类型说明,istack 类定义了一个可变字段 v、一个返回 int option 的 pop 方法,以及一个类型为 int -> unit 的 push 方法。
要产生对象,可以用关键字 new 实例化类:
# let s = new istack;;
val s : istack = <obj>
# s#pop;;
- : int option = Some 0
# s#push 5;;
- : unit = ()
# s#pop;;
- : int option = Some 5
你可能注意到,对象 s 的类型被写成了 istack。但前面强调过,类不是类型,这是怎么回事?事实上,类和类名确实不是类型。不过为了方便,定义类 istack 时,OCaml 也会定义一个同名对象类型 istack,它拥有和该类相同的方法。这个类型定义等价于:
# type istack = < pop: int option; push: int -> unit >;;
type istack = < pop : int option; push : int -> unit >
注意,这个类型代表任何拥有这些方法的对象。用 istack 类创建出来的对象会拥有这个类型,但拥有这个类型的对象不一定是由 istack 类创建的。
14.2 类参数与多态(Class Parameters and Polymorphism)
类定义也充当类的构造器。一般来说,类定义可以带参数;用 new 创建对象时必须提供这些参数。
下面实现 istack 的一个变体,它能保存任意值,而不只是整数。定义类时,类型参数放在类名前的方括号中。我们还添加一个参数 init,表示栈的初始内容:
# class ['a] stack init = object
val mutable v : 'a list = init
method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None
method push hd =
v <- hd :: v
end;;
class ['a] stack :
'a list ->
object
val mutable v : 'a list
method pop : 'a option
method push : 'a -> unit
end
注意,定义中的类型参数 ['a] 使用方括号,但在类型的其他使用处会省略方括号。如果有多个类型参数,则会用圆括号。
字段 v 声明上的类型标注用于约束类型推断。如果省略这个标注,类的推断类型会“过于多态”:init 可能具有某个 'b list 类型。
# class ['a] stack init = object
val mutable v = init
method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None
method push hd =
v <- hd :: v
end;;
Lines 1-13, characters 1-6:
Error: Some type variables are unbound in this type:
class ['a] stack :
'b list ->
object
val mutable v : 'b list
method pop : 'b option
method push : 'b -> unit
end
The method pop has type 'a option where 'a is unbound
一般来说,需要提供足够约束,让编译器推断出正确类型。可以给参数、字段和方法加类型约束。加多少是风格问题:三处都可以加,但额外文本不一定更清晰。一个方便的折中是给字段和/或类参数加标注,只有必要时再给方法加约束。
14.3 对象类型作为接口(Object Types as Interfaces)
有时我们希望遍历栈中的元素。在面向对象语言中,一种常见风格是定义一个 iterator 对象类。迭代器提供一种通用机制,用来检查和遍历集合元素。
有两种常见方式定义这类抽象接口。在 Java 中,迭代器通常会用 interface 指定,它说明一组方法类型:
// Java-style iterator, specified as an interface.
interface <T> iterator {
T Get();
boolean HasValue();
void Next();
};
在没有 interface 的语言中,例如 C++,通常用抽象类指定方法而不实现它们。C++ 中的 = 0 定义表示“未实现”。
// Abstract class definition in C++.
template<typename T>
class Iterator {
public:
virtual ~Iterator() {}
virtual T get() const = 0;
virtual bool has_value() const = 0;
virtual void next() = 0;
};
OCaml 两种风格都支持。事实上,OCaml 比这些方式更灵活,因为任何拥有合适方法的对象都可以实现某个对象类型;并不需要预先由对象所属的类指定。抽象类稍后再讲,这里先用对象类型展示这个技巧。
首先定义一个对象类型 iterator,指定迭代器中的方法:
# type 'a iterator = < get : 'a; has_value : bool; next : unit >;;
type 'a iterator = < get : 'a; has_value : bool; next : unit >
接着,为列表定义一个真正的迭代器。可以用它遍历栈中的内容:
# class ['a] list_iterator init = object
val mutable current : 'a list = init
method has_value = not (List.is_empty current)
method get =
match current with
| hd :: tl -> hd
| [] -> raise (Invalid_argument "no value")
method next =
match current with
| hd :: tl -> current <- tl
| [] -> raise (Invalid_argument "no value")
end;;
class ['a] list_iterator :
'a list ->
object
val mutable current : 'a list
method get : 'a
method has_value : bool
method next : unit
end
最后,给 stack 类添加一个 iterator 方法来产生迭代器。为此,构造一个引用当前栈内容的 list_iterator:
# class ['a] stack init = object
val mutable v : 'a list = init
method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None
method push hd =
v <- hd :: v
method iterator : 'a iterator =
new list_iterator v
end;;
class ['a] stack :
'a list ->
object
val mutable v : 'a list
method iterator : 'a iterator
method pop : 'a option
method push : 'a -> unit
end
现在可以构建一个新栈,压入一些值,然后迭代它们:
# let s = new stack [];;
val s : '_weak1 stack = <obj>
# s#push 5;;
- : unit = ()
# s#push 4;;
- : unit = ()
# let it = s#iterator;;
val it : int iterator = <obj>
# it#get;;
- : int = 4
# it#next;;
- : unit = ()
# it#get;;
- : int = 5
# it#next;;
- : unit = ()
# it#has_value;;
- : bool = false
14.3.1 函数式迭代器(Functional Iterators)
实践中,大多数 OCaml 程序员会避免使用迭代器对象,而更偏向函数式风格技术。例如,下面这个替代版本的 stack 类接收一个函数 f,并把它应用到栈中的每个元素:
# class ['a] stack init = object
val mutable v : 'a list = init
method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None
method push hd =
v <- hd :: v
method iter f =
List.iter ~f v
end;;
class ['a] stack :
'a list ->
object
val mutable v : 'a list
method iter : ('a -> unit) -> unit
method pop : 'a option
method push : 'a -> unit
end
那 map 和 fold 这类函数式操作怎么办?一般来说,这些方法接收一个函数,而这个函数会产生一个和集合元素类型不同的值。
例如,对 ['a] stack 类来说,fold 方法应该有类型 ('b -> 'a -> 'b) -> 'b -> 'b,其中 'b 是多态的。为了表达这种多态方法类型,必须使用类型量词,如下所示:
# class ['a] stack init = object
val mutable v : 'a list = init
method pop =
match v with
| hd :: tl ->
v <- tl;
Some hd
| [] -> None
method push hd =
v <- hd :: v
method fold : 'b. ('b -> 'a -> 'b) -> 'b -> 'b =
(fun f init -> List.fold ~f ~init v)
end;;
class ['a] stack :
'a list ->
object
val mutable v : 'a list
method fold : ('b -> 'a -> 'b) -> 'b -> 'b
method pop : 'a option
method push : 'a -> unit
end
类型量词 'b. 可以读作“对所有 'b”。类型量词只能直接放在方法名之后,这意味着方法参数必须用 fun 或 function 表达式来表示。
14.4 继承(Inheritance)
继承使用已有类来定义新类。例如,下面的类定义继承字符串栈版本的 stack 类,并添加一个新方法 print,用于打印栈中的所有字符串:
# class sstack init = object
inherit [string] stack init
method print =
List.iter ~f:Stdio.print_endline v
end;;
class sstack :
string list ->
object
val mutable v : string list
method pop : string option
method print : unit
method push : string -> unit
end
类可以覆盖从父类继承来的方法。例如,下面这个类创建整数栈,并在整数入栈前把它们加倍:
# class double_stack init = object
inherit [int] stack init as super
method push hd =
super#push (hd * 2)
end;;
class double_stack :
int list ->
object
val mutable v : int list
method pop : int option
method push : int -> unit
end
上面的 as super 语句创建了一个特殊对象 super,可用于调用父类方法。注意,super 不是真正的对象,只能用于调用方法。
14.5 类类型(Class Types)
如果希望另一个文件或模块中的代码继承某个类,就必须暴露这个类,并为它给出类类型。那么类类型是什么?
举例来说,把 stack 类包装进显式模块中。这里为了说明使用显式模块;定义 .mli 文件时流程类似。按照模块的一般风格,定义一个类型 'a t 表示栈的类型:
module Stack = struct
class ['a] stack init = object
...
end
type 'a t = 'a stack
let make init = new stack init
end
定义模块类型时有多个选择,取决于希望暴露多少实现细节。一个极端是最大程度抽象的签名,它会完全隐藏类定义:
module AbstractStack : sig
type 'a t = < pop: 'a option; push: 'a -> unit >
val make : 'a list -> 'a t
end = Stack
这个抽象签名很简单,因为它忽略了类。但如果想把类也包含在签名中,让其他模块可以继承这些类定义,该怎么办?这时需要为类指定类型,也就是类类型。
类类型并不出现在主流面向对象编程语言中,所以你可能不熟悉它,但概念很简单。类类型指定类中每个可见部分的类型,包括字段和方法。和模块类型一样,不必给所有内容都写类型;省略的内容会被隐藏:
module VisibleStack : sig
type 'a t = < pop: 'a option; push: 'a -> unit >
class ['a] stack : object
val mutable v : 'a list
method pop : 'a option
method push : 'a -> unit
end
val make : 'a list -> 'a t
end = Stack
在这个签名中,我们选择让所有内容可见。stack 的类类型指定了字段 v 的类型,以及每个方法的类型。
14.6 开放递归(Open Recursion)
开放递归允许对象的方法调用同一对象上的其他方法。这些调用会动态查找,因此如果两个类被同一个对象继承,一个类中的方法可以调用另一个类中的方法。这允许对象中相互递归的部分分开定义。
这种从独立组件定义相互递归方法的能力,是类的关键特性。用数据类型或模块实现类似功能会笨拙得多,也冗长得多。
例如,考虑为一种简单文档格式编写递归函数。这个格式表示为一棵树,其中有三类节点:
type doc =
| Heading of string
| Paragraph of text_item list
| Definition of string list_item list
and text_item =
| Raw of string
| Bold of text_item list
| Enumerate of int list_item list
| Quote of doc
and 'a list_item =
{ tag: 'a;
text: text_item list }
写一个通过递归遍历这些数据来工作的函数很容易。但如果需要写许多类似递归函数,该如何抽出公共部分,避免重复样板代码?
最简单的方式是使用类和开放递归。例如,下面这个类定义了对文档数据进行折叠的对象:
class ['a] folder = object(self)
method doc acc = function
| Heading _ -> acc
| Paragraph text -> List.fold ~f:self#text_item ~init:acc text
| Definition list -> List.fold ~f:self#list_item ~init:acc list
method list_item: 'b. 'a -> 'b list_item -> 'a =
fun acc {tag; text} ->
List.fold ~f:self#text_item ~init:acc text
method text_item acc = function
| Raw _ -> acc
| Bold text -> List.fold ~f:self#text_item ~init:acc text
| Enumerate list -> List.fold ~f:self#list_item ~init:acc list
| Quote doc -> self#doc acc doc
end
object (self) 语法把 self 绑定到当前对象,使 doc、list_item 和 text_item 方法可以相互调用。
通过继承这个类,可以创建折叠文档数据的函数。例如,count_doc 函数统计文档中不在列表内部的粗体标签数量:
class counter = object
inherit [int] folder as super
method list_item acc li = acc
method text_item acc ti =
let acc = super#text_item acc ti in
match ti with
| Bold _ -> acc + 1
| _ -> acc
end
let count_doc = (new counter)#doc
注意,text_item 中使用特殊对象 super 调用 [int] folder 类的 text_item 方法,从而折叠 text_item 节点的子节点。
14.7 私有方法(Private Methods)
方法可以声明为私有(private),这意味着子类可以调用它们,但除此之外不可见,类似 C++ 中的 protected 方法。
例如,我们可能想在 folder 类中加入处理 doc 和 text_item 中不同情况的方法。但也许不想强迫 folder 的子类暴露这些方法,因为它们很可能不应被直接调用:
class ['a] folder2 = object(self)
method doc acc = function
| Heading str -> self#heading acc str
| Paragraph text -> self#paragraph acc text
| Definition list -> self#definition acc list
method list_item: 'b. 'a -> 'b list_item -> 'a =
fun acc {tag; text} ->
List.fold ~f:self#text_item ~init:acc text
method text_item acc = function
| Raw str -> self#raw acc str
| Bold text -> self#bold acc text
| Enumerate list -> self#enumerate acc list
| Quote doc -> self#quote acc doc
method private heading acc str = acc
method private paragraph acc text =
List.fold ~f:self#text_item ~init:acc text
method private definition acc list =
List.fold ~f:self#list_item ~init:acc list
method private raw acc str = acc
method private bold acc text =
List.fold ~f:self#text_item ~init:acc text
method private enumerate acc list =
List.fold ~f:self#list_item ~init:acc list
method private quote acc doc = self#doc acc doc
end
let f :
< doc : int -> doc -> int;
list_item : 'a . int -> 'a list_item -> int;
text_item : int -> text_item -> int > = new folder2
最后构造值 f 的语句展示了:实例化 folder2 对象得到的对象类型会隐藏私有方法。
准确地说,私有方法是类类型的一部分,但不是对象类型的一部分。例如,对象 f 没有 bold 方法。不过,私有方法对子类可用,因此可以用它们简化 counter 类:
class counter_with_private_method = object
inherit [int] folder2 as super
method list_item acc li = acc
method private bold acc txt =
let acc = super#bold acc txt in
acc + 1
end
私有方法的关键性质是:它们对子类可见,但其他地方不可见。如果想要更强保证,即某个方法真的私有,连子类也不能访问,可以使用显式类类型并从中省略该方法。下面代码中,私有方法被明确从 counter_with_sig 的类类型中省略,因此 counter_with_sig 的子类也不能调用它们:
class counter_with_sig : object
method doc : int -> doc -> int
method list_item : int -> 'b list_item -> int
method text_item : int -> text_item -> int
end = object
inherit [int] folder2 as super
method list_item acc li = acc
method private bold acc txt =
let acc = super#bold acc txt in
acc + 1
end
14.8 二元方法(Binary Methods)
二元方法(binary method)是接收 self 类型对象作为参数的方法。一个常见例子是定义相等性方法:
# class square w = object(self : 'self)
method width = w
method area = Float.of_int (self#width * self#width)
method equals (other : 'self) = other#width = self#width
end;;
class square :
int ->
object ('a)
method area : float
method equals : 'a -> bool
method width : int
end
# class circle r = object(self : 'self)
method radius = r
method area = 3.14 *. (Float.of_int self#radius) **. 2.0
method equals (other : 'self) = other#radius = self#radius
end;;
class circle :
int ->
object ('a)
method area : float
method equals : 'a -> bool
method radius : int
end
注意,可以用类型标注 (self: 'self) 获得当前对象的类型。
现在可以用 equals 这个二元方法测试不同对象实例是否相等:
# (new square 5)#equals (new square 5);;
- : bool = true
# (new circle 10)#equals (new circle 7);;
- : bool = false
这能工作,但里面潜伏着一个问题。equals 方法接收的是精确类型 square 或 circle 的对象。因此,我们不能定义一个也包含相等性方法的通用基类 shape:
# type shape = < equals : shape -> bool; area : float >;;
type shape = < area : float; equals : shape -> bool >
# (new square 5 :> shape);;
Line 1, characters 1-24:
Error: Type square = < area : float; equals : square -> bool; width : int >
is not a subtype of shape = < area : float; equals : shape -> bool >
Type shape = < area : float; equals : shape -> bool >
is not a subtype of
square = < area : float; equals : square -> bool; width : int >
The first object type has no method width
问题在于,square 期望和另一个 square 比较,而不是任意形状;circle 也一样。这是一个根本问题。许多语言会用收窄(动态类型检查)或方法重载解决它。但 OCaml 两者都没有,那么该怎么办?
由于有问题的方法是相等性,一种提议是干脆把它从基础类型 shape 中去掉,改用多态相等。但内置多态相等应用到对象上时行为很差:
# Poly.(=)
(object method area = 5 end)
(object method area = 5 end);;
- : bool = false
这里的问题是,只有当两个对象物理相同时,内置多态相等才会认为它们相等。使用内置多态相等还有其他理由需要谨慎,而这些假阴性已经足以让它不能接受。
如果想为一般形状定义相等性,剩下的解决方案就是使用前面讨论收窄时的做法:引入一个用变体实现的表示类型(representation type),并基于这个表示类型实现比较。
# type shape_repr =
| Square of int
| Circle of int;;
type shape_repr = Square of int | Circle of int
# type shape =
< repr : shape_repr; equals : shape -> bool; area : float >;;
type shape = < area : float; equals : shape -> bool; repr : shape_repr >
# class square w = object(self)
method width = w
method area = Float.of_int (self#width * self#width)
method repr = Square self#width
method equals (other : shape) =
match (self#repr, other#repr) with
| Square x, Square x' -> Int.(=) x x'
| _ -> false
end;;
class square :
int ->
object
method area : float
method equals : shape -> bool
method repr : shape_repr
method width : int
end
二元方法 equals 现在基于具体类型 shape_repr 实现。使用这种模式时,无法隐藏 repr 方法,但可以用模块系统隐藏类型定义:
module Shapes : sig
type shape_repr
type shape =
< repr : shape_repr; equals : shape -> bool; area: float >
class square : int ->
object
method width : int
method area : float
method repr : shape_repr
method equals : shape -> bool
end
end = struct
type shape_repr =
| Square of int
| Circle of int
...
end
注意,这种方案会阻止我们在不向 shape_repr 类型添加新构造器的情况下添加新的形状种类,这相当受限。不过,可以用 OCaml 少用但仍然有用的可扩展变体(extensible variants)来修复这一点。
可扩展变体允许把变体类型定义和它的构造器定义分离。得到的类型从定义上就是开放的,也就是说,总是可以添加新变体。因此,编译器无法检查对这种变体的模式匹配是否穷尽。幸运的是,这里并不需要穷尽性。
下面是用可扩展变体重写前面例子的方式。
# type shape_repr = ..;;
type shape_repr = ..
# type shape =
< repr : shape_repr; equals : shape -> bool; area : float >;;
type shape = < area : float; equals : shape -> bool; repr : shape_repr >
# type shape_repr += Square of int;;
type shape_repr += Square of int
# class square w = object(self)
method width = w
method area = Float.of_int (self#width * self#width)
method repr = Square self#width
method equals (other : shape) =
match (self#repr, other#repr) with
| Square x, Square x' -> Int.(=) x x'
| _ -> false
end;;
class square :
int ->
object
method area : float
method equals : shape -> bool
method repr : shape_repr
method width : int
end
表示类型方法有个奇怪之处:这些类创建的对象和表示类型成员之间是一一对应的,这会让对象显得有些多余。
但相等性是二元方法的极端例子:它需要访问另一个对象的全部信息。许多其他二元方法只需要对象的部分信息。例如,可以考虑一个按大小比较形状的方法:
# class square w = object(self)
method width = w
method area = Float.of_int (self#width * self#width)
method larger (other : shape) = Float.(self#area > other#area)
end;;
class square :
int ->
object
method area : float
method larger : shape -> bool
method width : int
end
larger 方法可以用于 square,也可以应用到任意 shape 类型对象上。
14.9 虚类与虚方法(Virtual Classes and Methods)
虚(virtual)类是某些方法或字段只被声明但没有实现的类。这不应和 C++ 中的 virtual 混淆。C++ 中的 virtual 方法使用动态分派,而普通非 virtual 方法是静态分派。在 OCaml 中,所有方法都使用动态分派;关键字 virtual 表示方法或字段没有实现。包含虚方法的类本身也必须标记为 virtual,并且不能直接实例化,也就是不能创建这个类的对象。
为了探索这一点,把形状例子扩展成简单的交互式图形。这里会使用 Async 并发库,以及 Async_graphics 库,后者为 OCaml 内置 Graphics 库提供异步接口。第 17 章会再讨论 Async;现在可以忽略细节。只需要运行 opam install async_graphics 安装这个库。
给每个形状添加一个 draw 方法,描述如何在 Async_graphics 显示上绘制该形状:
open Core
open Async
open Async_graphics
type drawable = < draw: unit >
14.9.1 创建一些简单形状(Create Some Simple Shapes)
现在添加创建正方形和圆形的类。我们会包含一个 on_click 方法,为形状添加事件处理器。
class square w x y = object(self)
val mutable x: int = x
method x = x
val mutable y: int = y
method y = y
val mutable width = w
method width = width
method draw = fill_rect x y width width
method private contains x' y' =
x <= x' && x' <= x + width &&
y <= y' && y' <= y + width
method on_click ?start ?stop f =
on_click ?start ?stop
(fun ev ->
if self#contains ev.mouse_x ev.mouse_y then
f ev.mouse_x ev.mouse_y)
end
square 类相当直接,下面的 circle 类也很相似:
class circle r x y = object(self)
val mutable x: int = x
method x = x
val mutable y: int = y
method y = y
val mutable radius = r
method radius = radius
method draw = fill_circle x y radius
method private contains x' y' =
let dx = x' - x in
let dy = y' - y in
dx * dx + dy * dy <= radius * radius
method on_click ?start ?stop f =
on_click ?start ?stop
(fun ev ->
if self#contains ev.mouse_x ev.mouse_y then
f ev.mouse_x ev.mouse_y)
end
这些类有很多共同点,把共同功能提取到父类中会很有用。可以轻松把 x 和 y 的定义移到父类中,但 on_click 怎么办?它的定义依赖 contains,而每个类中的 contains 定义都不同。解决方案是创建虚类:这个类声明一个 contains 方法,但把它的定义留给子类。
下面是更简洁的定义,先从一个虚 shape 类开始,它实现 on_click 和 on_mousedown:
class virtual shape x y = object(self)
method virtual private contains: int -> int -> bool
val mutable x: int = x
method x = x
val mutable y: int = y
method y = y
method on_click ?start ?stop f =
on_click ?start ?stop
(fun ev ->
if self#contains ev.mouse_x ev.mouse_y then
f ev.mouse_x ev.mouse_y)
method on_mousedown ?start ?stop f =
on_mousedown ?start ?stop
(fun ev ->
if self#contains ev.mouse_x ev.mouse_y then
f ev.mouse_x ev.mouse_y)
end
现在可以通过继承 shape 来定义 square 和 circle:
class square w x y = object
inherit shape x y
val mutable width = w
method width = width
method draw = fill_rect x y width width
method private contains x' y' =
x <= x' && x' <= x + width &&
y <= y' && y' <= y + width
end
class circle r x y = object
inherit shape x y
val mutable radius = r
method radius = radius
method draw = fill_circle x y radius
method private contains x' y' =
let dx = x' - x in
let dy = y' - y in
dx * dx + dy * dy <= radius * radius
end
看待 virtual 类的一种方式是把它想成函子,其中“输入”是已声明但未定义的虚方法和字段。函子应用则通过继承实现,也就是在继承时给虚方法提供具体实现。
14.10 初始化器(Initializers)
可以在实例化类时执行表达式,做法是把表达式放在对象表达式之前,或放在字段的初始值中:
# class obj x =
let () = Stdio.printf "Creating obj %d\n" x in
object
val field = Stdio.printf "Initializing field\n"; x
end;;
class obj : int -> object val field : int end
# let o = new obj 3;;
Creating obj 3
Initializing field
val o : obj = <obj>
不过,这些表达式会在对象创建前执行,不能引用对象方法。如果实例化期间需要使用对象方法,可以使用初始化器(initializer)。初始化器是在实例化期间执行的表达式,但它会在对象创建之后运行。
例如,假设要用 growing_circle 类扩展前面的形状模块,让圆在被点击时扩张。可以继承 circle,并使用继承来的 on_click 为点击事件添加处理器:
class growing_circle r x y = object(self)
inherit circle r x y
initializer
self#on_click (fun _x _y -> radius <- radius * 2)
end
14.11 多重继承(Multiple Inheritance)
当一个类从多个父类继承时,就是在使用多重继承(multiple inheritance)。多重继承扩展了组合类的方式,尤其在配合虚类时可能很有用。不过,它也可能很棘手,特别是当继承层次是图而不是树时,因此应谨慎使用。
14.11.1 名称如何解析(How Names Are Resolved)
多重继承的主要棘手之处来自命名:当某个方法或字段名在多个类中都有定义时,会发生什么?
如果只记住 OCaml 继承的一件事,那就是:继承像文本包含。如果一个名字有多个定义,最后一个定义获胜。
例如,考虑下面这个类,它继承 square,并定义一个新的 draw 方法,用 draw_rect 而不是 fill_rect 来绘制正方形:
class square_outline w x y = object
inherit square w x y
method draw = draw_rect x y width width
end
由于 inherit 声明出现在方法定义之前,新的 draw 方法会覆盖旧方法,于是正方形会用 draw_rect 绘制。但如果把 square_outline 定义成下面这样呢?
class square_outline w x y = object
method draw = draw_rect x y w w
inherit square w x y
end
这里 inherit 声明在方法定义之后,所以来自 square 的 draw 方法会覆盖另一个定义,正方形会用 fill_rect 绘制。
再强调一次:要理解继承的含义,可以把每个 inherit 指令替换成它的定义,然后取每个方法或字段的最后一个定义。注意,继承添加的方法和字段,是其类类型中列出的那些;被类型隐藏的私有方法不会被包含进来。
14.11.2 Mixin
什么时候该使用多重继承?问不同的人,很可能会得到多个甚至激烈的答案。有些人会说多重继承过于复杂;另一些人会说继承本身就有问题,应该改用对象组合。但无论问谁,你都很少会听到“多重继承很棒,应该广泛使用”。
无论如何,如果你在用对象编程,有一种多重继承的一般模式既有用又相对简单:mixin 模式。一般来说,mixin 只是一个虚类,它基于另一个特性实现某个特性。如果你有一个类实现方法 A,又有一个 mixin M 能基于 A 提供方法 B,那么就可以继承 M,也就是把它“混入”进来,从而得到特性 B。
这太抽象了,所以用交互式形状举例。也许我们希望允许用户用鼠标拖动某个形状。可以为任何拥有可变 x 和 y 字段,并且拥有用于添加事件处理器的 on_mousedown 方法的对象定义这个功能:
class virtual draggable = object(self)
method virtual on_mousedown:
?start:unit Deferred.t ->
?stop:unit Deferred.t ->
(int -> int -> unit) -> unit
val virtual mutable x: int
val virtual mutable y: int
val mutable dragging = false
method dragging = dragging
initializer
self#on_mousedown
(fun mouse_x mouse_y ->
let offset_x = x - mouse_x in
let offset_y = y - mouse_y in
let mouse_up = Ivar.create () in
let stop = Ivar.read mouse_up in
dragging <- true;
on_mouseup ~stop
(fun _ ->
Ivar.fill mouse_up ();
dragging <- false);
on_mousemove ~stop
(fun ev ->
x <- ev.mouse_x + offset_x;
y <- ev.mouse_y + offset_y))
end
这样就可以用多重继承创建可拖动形状:
class small_square = object
inherit square 20 40 40
inherit draggable
end
也可以用 mixin 创建动画形状。每个动画形状都有一组更新函数,会在动画期间被调用。创建一个 animated mixin,用来提供这个更新列表,并确保形状动画运行时定期调用其中函数:
class virtual animated span = object(self)
method virtual on_click:
?start:unit Deferred.t ->
?stop:unit Deferred.t ->
(int -> int -> unit) -> unit
val mutable updates: (int -> unit) list = []
val mutable step = 0
val mutable running = false
method running = running
method animate =
step <- 0;
running <- true;
let stop =
Clock.after span
>>| fun () -> running <- false
in
Clock.every ~stop (Time.Span.of_sec (1.0 /. 24.0))
(fun () ->
step <- step + 1;
List.iter ~f:(fun f -> f step) updates
)
initializer
self#on_click (fun _x _y -> if not self#running then self#animate)
end
我们用初始化器向这个更新列表添加函数。例如,下面这个类会产生被点击时向右移动一秒的圆:
class my_circle = object
inherit circle 20 50 50
inherit animated Time.Span.second
initializer updates <- [fun _ -> x <- x + 5]
end
这些初始化器也可以通过 mixin 添加:
class virtual linear x' y' = object
val virtual mutable updates: (int -> unit) list
val virtual mutable x: int
val virtual mutable y: int
initializer
let update _ =
x <- x + x';
y <- y + y'
in
updates <- update :: updates
end
let pi = (Float.atan 1.0) *. 4.0
class virtual harmonic offset x' y' = object
val virtual mutable updates: (int -> unit) list
val virtual mutable x: int
val virtual mutable y: int
initializer
let update step =
let m = Float.sin (offset +. ((Float.of_int step) *. (pi /. 64.))) in
let x' = Float.to_int (m *. Float.of_int x') in
let y' = Float.to_int (m *. Float.of_int y') in
x <- x + x';
y <- y + y'
in
updates <- update :: updates
end
由于 linear 和 harmonic mixin 只用于副作用,它们可以在同一对象中被多次继承,用来产生多种不同动画:
class my_square x y = object
inherit square 40 x y
inherit draggable
inherit animated (Time.Span.of_int_sec 5)
inherit linear 5 0
inherit harmonic 0.0 7 ~-10
end
let my_circle = object
inherit circle 30 250 250
inherit animated (Time.Span.minute)
inherit harmonic 0.0 10 0
inherit harmonic (pi /. 2.0) 0 10
end
14.11.3 显示动画形状(Displaying the Animated Shapes)
最后,通过创建一个 main 函数在图形显示上绘制一些形状,并用 Async 调度器运行这个函数,完成形状模块:
let main () =
let shapes = [
(my_circle :> drawable);
(new my_square 50 350 :> drawable);
(new my_square 50 200 :> drawable);
(new growing_circle 20 70 70 :> drawable);
] in
let repaint () =
clear_graph ();
List.iter ~f:(fun s -> s#draw) shapes;
synchronize ()
in
open_graph "";
auto_synchronize false;
Clock.every (Time.Span.of_sec (1.0 /. 24.0)) repaint
let () = never_returns (Scheduler.go_main ~main ())
main 函数创建一个要显示的形状列表,并定义真正把它们画到显示上的 repaint 函数。然后打开图形显示,并要求 Async 定期运行 repaint。
最后,通过链接 async_graphics 包来构建二进制,它会拉入所有其他依赖:
(executable
(name shapes)
(modules shapes)
(libraries async_graphics))
$ dune build shapes.exe
运行这个二进制后,应当出现一个新的图形窗口。在 macOS 上,需要先安装 X11 包,系统会提示你。试着点击各种组件,看看随之展开的动画效果。
这里描述的图形库是 OCaml 内置的图形库,与其说它适合实际生产,不如说更适合学习。还有几个第三方库为不同图形子系统提供了更复杂的绑定:
Lablgtk : GTK widget 库的强类型接口。
LablGL : OCaml 与 OpenGL 之间的接口。OpenGL 是受到广泛支持的 3D 渲染标准。
js_of_ocaml : 把 OCaml 代码编译成 JavaScript,并提供 WebGL 绑定。这正在成为浏览器中 3D 渲染的新兴标准。