对象
对象和类
OCaml 是一种面向对象、命令式、函数式编程语言。它融合了所有这些范式,并允许您根据手头的任务使用最合适(或最熟悉的)编程范式。在本章中,我们将探讨 OCaml 中的面向对象编程,但我们也会讨论为什么您可能想要或可能不想编写面向对象的程序。
教科书中用于演示面向对象编程的经典入门示例是栈类。从很多方面来说,这是一个非常糟糕的例子,但我们在这里用它来展示面向对象 OCaml 的基本知识。
以下是一些提供整数栈的基本代码。该类使用链接列表实现。
# class stack_of_ints =
object (self)
val mutable the_list = ([] : int list) (* instance variable *)
method push x = (* push method *)
the_list <- x :: the_list
method pop = (* pop method *)
let result = List.hd the_list in
the_list <- List.tl the_list;
result
method peek = (* peek method *)
List.hd the_list
method size = (* size method *)
List.length the_list
end;;
class stack_of_ints :
object
val mutable the_list : int list
method peek : int
method pop : int
method push : int -> unit
method size : int
end
基本模式 class name = object (self) ... end
定义了一个名为 name
的类。
该类有一个可变(非常量)的实例变量,名为 the_list
。这是底层的链接列表。我们使用您可能不熟悉的某些代码来初始化它(每次创建 stack_of_ints
对象时)。表达式 ( [] : int list )
表示“一个空列表,类型为 int list
”。回想一下,简单的空列表 []
类型为 'a list
,表示任何类型的列表。但是我们想要一个 int
的栈,而不是其他任何东西,所以在这种情况下,我们想要告诉类型推断引擎这个列表不是通用的“任何东西的列表”,而实际上是更窄的“int
的列表”。语法 ( expression : type )
表示 expression
,其类型为 type
。这**不是**一个通用的类型转换,因为您不能用它来覆盖类型推断引擎,而只能将通用类型缩小以使其更具体。因此,例如,您不能编写 ( 1 : float )
# (1 : float);;
Line 1, characters 2-3:
Error: This expression has type int but an expression was expected of type
float
Hint: Did you mean `1.'?
类型安全性得以保留。回到示例……
此类有四个简单的方法
push
将一个整数压入栈。pop
将栈顶整数弹出并返回它。请注意用于更新我们的可变实例变量的<-
赋值运算符。它与用于更新记录中可变字段的<-
赋值运算符相同。peek
返回栈顶(即列表头部),而不影响栈size
返回栈中元素的数量(即列表的长度)。
让我们编写一些代码来测试 int
的栈。首先,让我们创建一个新的对象。我们使用熟悉的 new
运算符
# let s = new stack_of_ints;;
val s : stack_of_ints = <obj>
现在我们将一些元素压入栈并弹出。
# for i = 1 to 10 do
s#push i
done;;
- : unit = ()
# while s#size > 0 do
Printf.printf "Popped %d off the stack.\n" s#pop
done;;
Popped 10 off the stack.
Popped 9 off the stack.
Popped 8 off the stack.
Popped 7 off the stack.
Popped 6 off the stack.
Popped 5 off the stack.
Popped 4 off the stack.
Popped 3 off the stack.
Popped 2 off the stack.
Popped 1 off the stack.
- : unit = ()
注意语法。object#method
表示在 object
上调用 method
。这与您在命令式语言中熟悉的 object.method
或 object->method
相同。
在 OCaml toplevel 中,我们可以更详细地检查对象和方法的类型
# let s = new stack_of_ints;;
val s : stack_of_ints = <obj>
# s#push;;
- : int -> unit = <fun>
s
是一个不透明对象。实现(即列表)对调用者隐藏。
多态类
整数栈很好,但如果一个栈可以存储任何类型呢?(不是一个可以存储混合类型的栈,而是多个栈,每个栈都存储任何单一类型的对象)。与 'a list
一样,我们可以定义 'a stack
# class ['a] stack =
object (self)
val mutable list = ([] : 'a list) (* instance variable *)
method push x = (* push method *)
list <- x :: list
method pop = (* pop method *)
let result = List.hd list in
list <- List.tl list;
result
method peek = (* peek method *)
List.hd list
method size = (* size method *)
List.length list
end;;
class ['a] stack :
object
val mutable list : 'a list
method peek : 'a
method pop : 'a
method push : 'a -> unit
method size : int
end
class ['a] stack
实际上并没有定义一个类。它定义了一个完整的“类类”,每个可能的类型对应一个(即无限多个类!)。让我们尝试使用我们的 'a stack
类。在这个例子中,我们创建一个栈并将一个浮点数压入栈。注意栈的类型
# let s = new stack;;
val s : '_weak1 stack = <obj>
# s#push 1.0;;
- : unit = ()
# s;;
- : float stack = <obj>
现在这个栈变成了一个浮点数栈
,并且只能向这个栈中压入和弹出浮点数。让我们演示一下新的浮点数栈
的类型安全。
# s#push 3.0;;
- : unit = ()
# s#pop;;
- : float = 3.
# s#pop;;
- : float = 1.
# s#push "a string";;
Line 1, characters 8-18:
Error: This expression has type string but an expression was expected of type
float
我们可以定义多态函数,这些函数可以操作任何类型的栈。我们的第一次尝试是这样的
# let drain_stack s =
while s#size > 0 do
ignore (s#pop)
done;;
val drain_stack : < pop : 'a; size : int; .. > -> unit = <fun>
注意drain_stack
的类型。巧妙地(也许太巧妙了),OCaml 的类型推断引擎已经推断出drain_stack
可以作用于任何具有pop
和size
方法的对象!因此,如果我们定义另一个完全独立的类,它碰巧包含具有适当类型签名的pop
和size
方法,那么我们可能会意外地对该其他类型的对象调用drain_stack
。
我们可以强制 OCaml 更具体,并且只允许在'a stack
上调用drain_stack
,方法是缩小s
参数的类型,如下所示
# let drain_stack (s : 'a stack) =
while s#size > 0 do
ignore (s#pop)
done;;
val drain_stack : 'a stack -> unit = <fun>
继承、虚类、初始化器
我注意到 Java 中的程序员倾向于过度使用继承,可能是因为这是在该语言中扩展代码的唯一合理方法。一种更好、更通用的扩展代码的方法通常是使用钩子(参见 Apache 的模块 API)。然而,在某些狭窄的领域,继承可能是有用的,其中最重要的是编写 GUI 组件库。
让我们考虑一个类似于 Java 的 Swing 的虚构 OCaml 组件库。我们将使用以下类层次结构定义按钮和标签
widget (superclass for all widgets)
|
+----> container (any widget that can contain other widgets)
| |
| +----> button
|
+-------------> label
(注意,button
是一个container
,因为它可以包含标签或图像,具体取决于按钮上显示的内容)。
widget
是所有组件的虚基类。我希望每个组件都有一个名称(只是一个字符串),该名称在组件的生命周期内保持不变。这是我的第一次尝试
# class virtual widget name =
object (self)
method get_name =
name
method virtual repaint : unit
end;;
Error: Some type variables are unbound in this type:
class virtual widget :
'a ->
object method get_name : 'a method virtual repaint : unit end
The method get_name has type 'a where 'a is unbound
糟糕!我忘记了 OCaml 无法推断name
的类型,因此将假设它是'a
。但这定义了一个多态类,而我没有将类声明为多态的(class ['a] widget
)。我需要像这样缩小name
的类型
# class virtual widget (name : string) =
object (self)
method get_name =
name
method virtual repaint : unit
end;;
class virtual widget :
string -> object method get_name : string method virtual repaint : unit end
现在代码中出现了一些新内容。首先,类包含一个**初始化器**。您可以将其视为传递给类的参数(name
),这与例如 Java 中构造函数的参数完全等效。
public class Widget
{
public Widget (String name)
{
...
}
}
在 OCaml 中,构造函数构建整个类;它不仅仅是一个特殊命名的函数,因此我们编写参数的方式就像它们是类的参数一样。
class foo arg1 arg2 ... =
其次,类包含一个虚方法,因此整个类被标记为虚类。虚方法是我们的repaint
方法。我们需要告诉 OCaml 它是一个虚方法(method virtual
),并且我们需要告诉 OCaml 方法的类型。因为该方法不包含任何代码,所以 OCaml 无法使用类型推断自动为您推断出类型,因此您需要告诉它类型。在这种情况下,该方法只返回unit
。如果您的类包含任何虚方法(即使只是继承的虚方法),您也需要使用class virtual ...
将整个类指定为虚类。
与 C++ 和 Java 一样,虚类不能使用new
直接实例化。
# let w = new widget "my widget";;
Error: Cannot instantiate the virtual class widget
现在我的container
类更有意思了。它必须继承自widget
并具有存储包含组件列表的机制。这是我为container
提供的简单实现
# class virtual container name =
object (self)
inherit widget name
val mutable widgets = ([] : widget list)
method add w =
widgets <- w :: widgets
method get_widgets =
widgets
method repaint =
List.iter (fun w -> w#repaint) widgets
end;;
class virtual container :
string ->
object
val mutable widgets : widget list
method add : widget -> unit
method get_name : string
method get_widgets : widget list
method repaint : unit
end
说明
container
类被标记为虚类。它不包含任何虚方法,但在这种情况下,它用于防止人们直接创建容器。container
类有一个name
参数,在构建widget
时直接传递给它。inherit widget name
表示container
继承自widget
,并且它将name
参数传递给widget
的构造函数。- 此
container
包含一个组件的可变列表以及将组件add
到此列表和get_widgets
(返回组件列表)的方法。 get_widgets
返回的组件列表不能被类外部的代码修改。这样做的原因有些微妙,但基本上归结为 OCaml 的链接列表是不可变的。让我们假设有人编写了以下代码
# let list = container#get_widgets in
x :: list;;
这会修改我的container
类的私有内部表示形式吗,方法是在组件列表的开头添加x
?不会。如果运行上述代码,您会看到它会抛出错误。
Error: Unbound value container
然而,私有变量widgets
不会受到此或任何其他外部代码尝试更改的影响。这意味着,例如,您可以在以后的某个日期将内部表示形式更改为使用数组,并且类外部的任何代码都不需要更改。
最后但并非最不重要的一点是,我们实现了之前声明为虚函数的repaint
函数,以便container#repaint
将重新绘制所有包含的组件。请注意,使用List.iter
迭代列表,我还使用了一个您可能不熟悉的匿名函数表达式
# (fun w -> w#repaint);;
- : < repaint : 'a; .. > -> 'a = <fun>
这定义了一个具有一个参数w
的匿名函数,该函数只调用w#repaint
(组件w
上的repaint
方法)。
在本例中,我们的button
类很简单(实际上有点不切实际,但没关系)
# type button_state = Released | Pressed;;
type button_state = Released | Pressed
# class button ?callback name =
object (self)
inherit container name as super
val mutable state = Released
method press =
state <- Pressed;
match callback with
| None -> ()
| Some f -> f ()
method release =
state <- Released
method repaint =
super#repaint;
print_endline ("Button being repainted, state is " ^
(match state with
| Pressed -> "Pressed"
| Released -> "Released"))
end;;
class button :
?callback:(unit -> unit) ->
string ->
object
val mutable state : button_state
val mutable widgets : widget list
method add : widget -> unit
method get_name : string
method get_widgets : widget list
method press : unit
method release : unit
method repaint : unit
end
说明
- 此函数有一个可选参数(参见上一章),用于传递可选的回调函数。当按下按钮时,将调用回调。
- 表达式
inherit container name as super
将超类命名为super
。我在repaint
方法中使用了它:super#repaint
。这明确地调用了超类方法。 - 按下按钮(在此相当简单的代码中调用
button#press
)将按钮设置为Pressed
并调用回调函数(如果已定义)。请注意,callback
变量要么是None
,要么是Some f
,这意味着它的类型为(unit -> unit) option
。如果您不确定,请重新阅读上一章。 - 注意
callback
变量的一个奇怪之处。它被定义为类的参数,但任何方法都可以看到和使用它。换句话说,变量在对象构造时提供,并且在对象的生命周期内也保持存在。 repaint
方法已实现。它调用超类(重新绘制容器),然后重新绘制按钮,显示按钮的当前状态。
在定义我们的label
类之前,让我们在 OCaml toplevel 中使用button
类。
# let b = new button ~callback:(fun () -> print_endline "Ouch!") "button";;
val b : button = <obj>
# b#repaint;;
Button being repainted, state is Released
- : unit = ()
# b#press;;
Ouch!
- : unit = ()
# b#repaint;;
Button being repainted, state is Pressed
- : unit = ()
# b#release;;
- : unit = ()
这是我们相对简单的label
类。
# class label name text =
object (self)
inherit widget name
method repaint =
print_endline ("Label: " ^ text)
end;;
class label :
string ->
string -> object method get_name : string method repaint : unit end
让我们创建一个显示“Press me!”的标签并将其添加到按钮中。
# let l = new label "label" "Press me!";;
val l : label = <obj>
# b#add l;;
- : unit = ()
# b#repaint;;
Label: Press me!
Button being repainted, state is Released
- : unit = ()
self
的说明
关于在以上所有示例中,我们都使用通用模式定义了类
class name =
object (self)
(* ... *)
end
对self
的引用命名了对象,允许您在同一类中调用方法或将对象传递给类外部的函数。换句话说,它与 C++/Java 中的this
完全相同。如果您不需要引用自身,则可以完全省略(self)
部分。实际上,在以上所有示例中,我们都可以这样做。但是,我们建议您保留它,因为您永远不知道何时可能会修改类并需要对self
的引用。这样做没有任何损失。
继承和强制转换
# let b = new button "button";;
val b : button = <obj>
# let l = new label "label" "Press me!";;
val l : label = <obj>
# [b; l];;
Error: This expression has type label but an expression was expected of type
button
The first object type has no method add
我们创建了一个按钮b
和一个标签l
,然后尝试创建一个包含两者的列表,但我们遇到了错误。然而,b
和l
都是widget
,所以也许我们不能将它们放入同一个列表中,因为 OCaml 无法猜测我们想要一个widget list
。让我们尝试告诉它
# let wl = ([] : widget list);;
val wl : widget list = []
# let wl = b :: wl;;
Error: This expression has type widget list
but an expression was expected of type button list
Type widget = < get_name : string; repaint : unit >
is not compatible with type
button =
< add : widget -> unit; get_name : string;
get_widgets : widget list; press : unit; release : unit;
repaint : unit >
The first object type has no method add
事实证明,OCaml 默认情况下不会将子类强制转换为超类类型,但您可以使用:>
(强制转换)运算符告诉它执行此操作。
# let wl = (b :> widget) :: wl;;
val wl : widget list = [<obj>]
# let wl = (l :> widget) :: wl;;
val wl : widget list = [<obj>; <obj>]
表达式(b :> widget)
表示“将按钮b
强制转换为类型widget
”。类型安全得到保留,因为可以在编译时完全确定强制转换是否会成功。
实际上,强制转换比上面描述的要微妙一些,因此请阅读手册以了解完整细节。
上面定义的container#add
方法实际上是不正确的;如果尝试将不同类型的组件添加到container
中,它将失败。强制转换可以解决此问题。
是否可以从超类(例如widget
)强制转换为子类(例如button
)?答案,也许令人惊讶,是不可以!向这个方向强制转换是不安全的。您可能会尝试强制转换一个实际上是label
而不是button
的widget
。
Oo
模块和比较对象
Oo
模块包含一些用于面向对象编程的有用函数。
Oo.copy
创建对象的浅拷贝。Oo.id object
为每个对象返回一个唯一的标识号(跨所有类的唯一编号)。
=
和<>
可用于比较对象的物理相等性(对象及其副本在物理上并不相同)。您还可以使用<
等,它根据对象的 ID 提供对象的排序。
无类对象
在这里,我们研究如何使用对象,几乎就像记录一样,而不必使用类。
直接对象和对象类型
对象可以代替记录使用。此外,它们还具有一些不错的属性,在某些情况下可以使它们优于记录。我们看到,创建对象的规范方法是首先定义一个类,然后使用此类创建单个对象。在某些情况下,这可能很麻烦,因为类定义不仅仅是类型定义,并且不能使用类型递归定义。但是,对象具有与记录类型非常类似的类型,并且可以在类型定义中使用。此外,对象可以在没有类的条件下创建。它们被称为直接对象。以下是直接对象的定义
# let o =
object
val mutable n = 0
method incr = n <- n + 1
method get = n
end;;
val o : < get : int; incr : unit > = <obj>
此对象具有一个类型,该类型仅由其公共方法定义。值不可见,私有方法(未显示)也不可见。与记录不同,此类类型不需要显式预定义,但这样做可以使事情更清晰。我们可以这样做
# type counter = <get : int; incr : unit>;;
type counter = < get : int; incr : unit >
与等效的记录类型定义进行比较
# type counter_r =
{get : unit -> int;
incr : unit -> unit};;
type counter_r = { get : unit -> int; incr : unit -> unit; }
像我们的对象一样工作的记录的实现将是
# let r =
let n = ref 0 in
{get = (fun () -> !n);
incr = (fun () -> incr n)};;
val r : counter_r = {get = <fun>; incr = <fun>}
在功能方面,对象和记录都是相似的,但每种解决方案都有其自身的优势
- 速度:记录中的字段访问速度稍快。
- 字段名称:当某些字段名称相同但对象没有问题时,操作不同类型的记录会很不方便。
- 子类型化:不可能将记录类型强制转换为具有较少字段的类型。但是,对象可以做到这一点,因此在数据结构中可以混合不同类型的对象,在这些数据结构中,只有它们的公共方法可见(参见下一节)。
- 类型定义:无需预先定义对象类型,因此减轻了模块之间的依赖关系约束。
类类型与普通类型
注意区分类类型和对象类型。类类型不是数据类型,在OCaml术语中通常称为类型。对象类型是一种数据类型,就像记录类型或元组一样。
当定义一个类时,会同时定义一个同名的类类型和一个对象类型。
# class t =
object
val x = 0
method get = x
end;;
class t : object val x : int method get : int end
object val x : int method get : int end
是一个类类型。
在这个例子中,t
也是这个类创建的对象的类型。只要对象具有相同的类型,就可以将派生自不同类或根本不派生自任何类(直接对象)的对象混合在一起。
# let x = object method get = 123 end;;
val x : < get : int > = <obj>
# let l = [new t; x];;
val l : t list = [<obj>; <obj>]
可以混合共享公共子类型的对象,但这需要使用:>
运算符进行显式类型强制转换。
# let x = object method get = 123 end;;
val x : < get : int > = <obj>
# let y = object method get = 80 method special = "hello" end;;
val y : < get : int; special : string > = <obj>
# let l = [x; y];;
Error: This expression has type < get : int; special : string >
but an expression was expected of type < get : int >
The second object type has no method special
# let l = [x; (y :> t)];;
val l : t list = [<obj>; <obj>]