天天看點

程式設計語言中的鴨子類型 Duck Typing

程式設計語言中的鴨子類型 Duck Typing

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,它隻關心事物的外部行為而非内部結構。它并不關心你這隻鴨子是長肉的還是充氣的。

在程式設計中,也常常用這種方式來描述事物。那麼不同的程式設計語言中,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 的實作方法相對比較靈活,又不失類型檢查。總的來說,特點有:

  1. 即能同時實作多個接口
  2. 又具有 python , C++ 的 Duck Typing 靈活性
  3. 又具有 java 的類型檢查。

本文完~