1、什麼是鴨子類型(duck typing)
百度百科是這樣解釋的:
這是程式設計中的一種類型推斷風格,這種風格适用于動态語言(比如PHP、Python、Ruby、Typescript、Perl、Objective-C、Lua、Julia、JavaScript、Java、Groovy、C#等)和某些靜态語言(比如Golang,一般來說,靜态類型語言在編譯時便已确定了變量的類型,但是Golang的實作是:在編譯時推斷變量的類型),支援"鴨子類型"的語言的解釋器/編譯器将會在解析(Parse)或編譯時,推斷對象的類型。
有沒有感覺很難了解?下面我們就來用通俗的語言介紹一下鴨子類型(Duck Typing)。
如果一隻動物走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麼這隻動物就可以被稱為鴨子。
許多程式設計語言都支援 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 的類型檢查。
本文完~