物件
物件與類別
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>
這個堆疊現在是 float stack
,而且只能將浮點數推入和彈出這個堆疊。讓我們示範我們新的 float stack
的類型安全性
# 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
。
我們可以透過縮小 s
引數的類型,強制 OCaml 更具體,並且只允許在 'a stack
上呼叫 drain_stack
,如下所示
# let drain_stack (s : 'a stack) =
while s#size > 0 do
ignore (s#pop)
done;;
val drain_stack : 'a stack -> unit = <fun>
繼承、虛擬類別、初始化器
我注意到 Java 的程式設計師傾向於過度使用繼承,可能是因為那是該語言中擴展程式碼的唯一合理方式。擴展程式碼更好、更通用的方式通常是使用 Hook(參見 Apache 的模組 API)。儘管如此,在某些狹窄的領域中,繼承還是有用的,其中最重要的是編寫 GUI 小工具函式庫。
讓我們考慮一個虛構的 OCaml 小工具函式庫,類似於 Java 的 Swing。我們將使用以下類別階層定義按鈕和標籤
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;;
這會透過將 x
前置到小工具列表來修改我的 container
類別的私有內部表示嗎?不會的。如果您執行上述程式碼,您會看到它會擲回錯誤
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 程式設計的有用函式。
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>]