程式語言中的 DUCK TYPING

Kingmax發表於2019-06-27

如果一隻動物走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻動物就可以被稱為鴨子。

許多程式語言都支援 Duck Typing ,通常 Duck Typing 是動態程式語言用來實現多型的一種方式。

在理解 Duck Typing 前,先看一張圖片,這是曾經一度很火的大黃鴨

先問一個比較考三觀的問題:圖片中的大黃鴨,它是不是一隻鴨子呢?

這個問題,得看你從哪個角度去看,如果從人們常識的認知中的角度去看,它顯然不是一隻鴨子,因為它連最基本的生命都沒有。

但是從 Duck Typing 的角度來看,它就是一隻鴨子!

Duck Typing 的原話是,走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼它就是一隻鴨子。

這個原話是可以靈活理解的,就看我們怎麼定義鴨子的行為,我們可以說,能浮在水上游的,黃色的,可愛的就是鴨子,那麼,圖片中的大黃鴨,它就是一隻鴨子!

這就是所謂的 Duck Typing,它只關心事物的外部行為而非內部結構。它並不關心你這隻鴨子是長肉的還是充氣的。

在程式設計中,也常常用這種方式來描述事物。那麼不同的程式語言中,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 方法,還有其它方法需要實現。通常這種情況需要通過加註釋來說明。

C++ 不是動態語言,但是它也能支援 Duck Typing,它是通過模板來支援的。
示例程式碼:

template <class F>
string download(const F& fetcher){
    return fetcher.get("http://xxxx")
}

這段程式碼與 Python 的實現方法類似,這個 fetcher 隨便什麼型別都可以,只要實現一個 get 方法,就能通過編譯。
那麼這種實現方法有什麼缺點呢,就是,編譯時,才知道傳入的 fetcher 有沒有 get 方法。
但它比 python 好一點了,python 是執行時才知道,C++ 是編譯時就知道。
同樣,這種情況,還是需要註釋來說明。

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 方法以外,還有其它方法,那麼也要一一實現,非常不靈活。

在 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 的實現方法相對比較靈活,又不失型別檢查。總的來說,特點有:

  1. 即能同時實現多個介面
  2. 又具有 python , C++ 的 Duck Typing 靈活性
  3. 又具有 java 的型別檢查。

看看自己是不是一個靠譜的程式設計師,來做題試試。https://job.xyh.io

相關文章