我們先回頭來看昨天的程式:
(define student%
(class object%
(init id name gender)
(define student-id id)
(define student-name name)
(define student-gender gender)
(define courses '())
(super-new)
(define/public get-name
(lambda ()
student-name))
(define/public join-course
(lambda (course)
(set! courses (cons course courses))))
(define/public get-current-courses
(lambda ()
courses))))
(define racket (new student% [id 1]
[name "Racket"]
[gender 'M]))
(send racket get-name)
(send racket join-course 'PLT)
(send racket join-course 'OS)
(send racket join-course 'DataStructures)
(send racket get-current-courses)
記得昨天講了物件導向的概念嗎?我們第一段程式,正是定義類別的方式:(define class-name% (class object%))
,在 Racket 裡,定義類別習慣使用 %
做結尾,當然你若想用 Java/C# CamelCase 的命名法,也不是不行。先宣告了一個 id 名為 student%
之後,再宣告它一個以 object%
為基礎的類別 class
,object%
是 Racket 裡頭的根類別,因此若你的類別沒有繼承別的,預設要以 object%
為根基。
接下來的 init
,你若看到第二段程式的時候,會知道原來這是一個 建構子(Constructor),用來作為初始化這個物件的內容。我們在 init
裡頭定義了三個變數:id
、name
、gender
,這三個變數接下來就用來傳給另外三個正式的物件屬性:student-id
、student-name
、student-gender
。而除了這三個外,student%
另有一個屬性 courses
,內容是一個空 list
。
接下來有一個很奇妙的東西,回想一下我們在 Java/C# 寫物件導向時,預設建構子裡似乎不需要加東西。但若翻 Java 語言規格書 [1],它說到當你的類別的建構子,若是沒有參數、沒有呼叫其他建構子,即使你不寫,它也會隱性地呼叫 super()
,也就是繼承上層的建構子。然而,我想為何要隱性呼叫呢?這應該是一種語法糖。換句話說,Racket 在提供物件導向特性時,其實是把那些語法糖都拿掉,好讓程式更清晰。
接下來便是定義物件的行為(method)了,我們定義了三個,分別是 get-name
、join-course
、get-current-courses
。裡面所使用的機制其實很簡單,相信各位從前文中可以了解它的作用。
最後,定義完了類別,我們便來建立物件。使用 new
,後面接著類別名稱 student%
與它的參數,於是我們可以使用這個物件了!
那好,你想,物件導向的四大特性:繼承、抽象化、封裝、多型,這些 Racket 能不能勝任。Racket 可是 Northeast PLT 的代表作。我們介紹繼承與封裝,多型與抽象化讓你回去研究看看。
再來看個範例程式:
(require racket/date)
(define student%
(class object%
(init id name gender)
(define student-id id)
(define student-name name)
(define student-gender gender)
(define courses '())
(super-new)
(define/public get-name
(lambda ()
student-name))
(define/public join-course
(lambda (course)
(set! courses (cons course courses))))
(define/public get-current-courses
(lambda ()
courses))))
(define alumni%
(class student%
(init id name gender year)
(define graduated-year year)
(super-new [id id] [name name] [gender gender])
(define/public get-graduated-year
(lambda ()
graduated-year))
(define/override get-name
(lambda ()
(string-upcase (super get-name))))
(define/public get-years-after-graduated
(lambda ()
(- (current-year) graduated-year)))
(define/private current-year
(lambda ()
(date-year (current-date))))))
(define scheme (new alumni% [id 2] [name "Scheme"] [gender 'F] [year 1995]))
(send scheme get-graduated-year)
(send scheme get-name)
(send scheme get-years-after-graduated)
首先,我們引入一個好用的日期工具:racket/date
,並且在 student%
下方,定義了新的類別:alumni%
,表示校友。在 class
定義處,不再寫 object%
了,而是寫 student%
表示 alumni%
是從 student%
繼承而來。校友有一個重要的屬性,就是畢業年:graduated-year
。
我們在 init
看到宣告了四個變數,除了 student%
的三項之外,還有一個 year
,並且(這裡是重點!),在 super-new
時,傳了 student%
所需要的三個變數給上層的 student%
類別。
在下方 alumni%
的 method 定義處,我們一樣給了一個 get-graduated-year
的 method,但是多了三個特別的 method:
get-name
的 method,因此用 define/override
宣告,這時我們可以回傳一個轉大寫的 name
值回去get-years-after-graduated
,但我們怎麼取到現在的年份呢?透過下方再定義了一個 current-year
的 methodcurrent-year
使用了 define/private
,使其存取範圍為物件內部限定,我們用這個方式進行了資訊的封裝,好讓在物件外部只能呼叫 get-years-after-graduated
,而不知道裡頭有個計算 current-year
的 method。以上,我們已經介紹完 Racket 的物件導向很基礎的部份,更深入的部份可以參考 Racket Guide[2] 與 Racket Reference[3]