如果一隻動物走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻動物就可以被稱爲鴨子。
許多編程語言都支持 Duck Typing ,通常 Duck Typing 是動態編程語言用來實現多態的一種方式。
在理解 Duck Typing 前,先看一張圖片,這是曾經一度很火的大黃鴨
先問一個比較考三觀的問題:圖片中的大黃鴨,它是不是一隻鴨子呢?
這個問題,得看你從哪個角度去看,如果從人們常識的認知中的角度去看,它顯然不是一隻鴨子,因爲它連最基本的生命都沒有。
但是從 Duck Typing 的角度來看,它就是一隻鴨子!
Duck Typing 的原話是,走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼它就是一隻鴨子。
這個原話是可以靈活理解的,就看我們怎麼定義鴨子的行爲,我們可以說,能浮在水上游的,黃色的,可愛的就是鴨子,那麼,圖片中的大黃鴨,它就是一隻鴨子!
這就是所謂的 Duck Typing,它只關心事物的外部行爲而非內部結構。它並不關心你這隻鴨子是長肉的還是充氣的。
在編程中,也常常用這種方式來描述事物。那麼不同的編程語言中,Duck Typing 是怎麼樣實現的呢?
1. Python 中的 Duck Typing
先看一個函數:
def download(fetcher):
return fetcher.get("http://xxx");
有一個 download 函數,傳過來一個 fetcher 參數,fetcher 是可以獲取一個 url 鏈接的資源的。
這個 fetcher 就是一個 Duck Typing 的對象,使用者約定好這個 fetcher 會有一個 get 函數就可以了。
顯然這個 download 函數會有以下問題:
運行時才知道傳入的 fetcher 有沒有 get 函數。那麼站在 download 函數的使用者的角度上看,我怎麼知道需要給 fetcher 實現 get 方法呢?我不可能去閱讀 download 函數的代碼,實際情況中,可能 download 函數的代碼很長,可能 fetcher 不只要實現 get 方法,還有其它方法需要實現。通常這種情況需要通過加註釋來說明。
2. C++ 中的 Duck Typing
C++ 不是動態語言,但是它也能支持 Duck Typing,它是通過模板來支持的。
示例代碼:
template <class F>
string download(const F& fetcher){
return fetcher.get("http://xxxx")
}
這段代碼與 Python 的實現方法類似,這個 fetcher 隨便什麼類型都可以,只要實現一個 get 方法,就能通過編譯。
那麼這種實現方法有什麼缺點呢,就是,編譯時,才知道傳入的 fetcher 有沒有 get 方法。
但它比 python 好一點了,python 是運行時才知道,C++ 是編譯時就知道。
同樣,這種情況,還是需要註釋來說明。
3. Java 中的類似代碼
Java 沒有 Duck Typing,它只有類似的代碼。Java 的 duck typing :
<F extends FetcherInterface>
String download(F fetcher){
return fetcher.get("http://xxxx")
}
它同樣也用了模板類型。模板 F 必須 extends FetcherInterface ,有了這個限定,就能逼着 download 函數的使用者對 fetcher 實現 get 方法,它解決了需要註釋來說明的缺點。
傳入的參數必須實現 FetcherInterface 接口,就沒有運行時發現錯誤,編譯時發現錯誤的問題。
但是,它嚴格上來說不是 Duck Typing 。
如果 download 函數只依賴 fetcher 的 get 方法,而 FetcherInterface 接口必須要實現除 get 方法以外,還有其它方法,那麼也要一一實現,非常不靈活。
4. Go 中的 Duck Typing
在 Java 的 Duck Typing 類似代碼中,如果 fetcher 參數需要同時實現兩個或以上的接口方法時,Java 是沒有辦法做到的。但 Go 語言可以做到。
type Fetcher interface {
Get(url string) string
}
type Saver interface {
Save(content string)
}
type FetcherAndSaver interface {
Fetcher
Saver
}
func download(f Fetcher) string {
return f.Get("http://xxxx")
}
func save(f saver) {
f.Save("some thing")
}
func downloadAndSave(f FetcherAndSaver) {
content := f.Get("http://xxxx")
f.Save(content)
}
# 實現者
type MyFetcherAndSaver struct {
}
func (f MyFetcherAndSaver) Get(url string) string {
...
}
func (f MyFetcherAndSaver) Save(content string) {
...
}
func main() {
f := MyFetcherAndSaver{}
download(f)
save(f)
downloadAndSave(f)
}
這裏定義了三個接口,只要有 Get 方法的就是 Fetcher,只要有 Save 方法的就是 Saver,同時有 Get 方法和 Save 方法就是 FetcherAndSaver 。
實現者 MyFetcherAndSaver 並不需要聲明它實現了哪些接口,只要它有相關接口的所定義的方法,那麼它的實例,就即能作爲 Fetcher 接口來使用,又能作爲 Saver 接口來使用,也能作爲 FetcherAndSaver 接口來使用。
Go 的實現方法相對比較靈活,又不失類型檢查。總的來說,特點有:
- 即能同時實現多個接口
- 又具有 python , C++ 的 Duck Typing 靈活性
- 又具有 java 的類型檢查。