鸭子类型系统

**鴨子型別**（英語：**duck typing**）在程式設計中是動態型別的一種風格。在這種風格中，一個物件有效的語意，不是由繼承自特定的類或實現特定的介面，而是由「當前方法和屬性的集合」決定。這個概念的名字來源於由詹姆斯・惠特科姆・萊利提出的鴨子測試（見下面的「歷史」章節），「鴨子測試」可以這樣表述：

　
: 「當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子，那麼這隻鳥就可以被稱為鴨子。」

在鴨子型別中，關注點在於物件的行為，能做什麼；而不是關注物件所屬的類型。例如，在不使用鴨子型別的語言中，我們可以編寫一個函式，它接受一個類型為「鴨子」的物件，並呼叫它的「走」和「叫」方法。在使用鴨子型別的語言中，這樣的一個函式可以接受一個任意類型的物件，並呼叫它的「走」和「叫」方法。如果這些需要被呼叫的方法不存在，那麼將引發一個執行時錯誤。任何擁有這樣的正確的「走」和「叫」方法的物件都可被函式接受的這種行為引出了以上表述，這種決定類型的方式因此得名。

鴨子型別通常得益於「不」測試方法和函式中參數的類型，而是依賴文件、清晰的代碼和測試來確保正確使用。

在常規類型中，我們能否在一個特定場景中使用某個物件取決於這個物件的類型，而在鴨子型別中，則取決於這個物件是否具有某種屬性或者方法 —— 即只要具備特定的屬性或方法，能通過鴨子測試，就可以使用。

## 概念樣例

考慮用於一個使用鴨子型別的語言的以下虛擬碼：

```
**function** calculate(a, b, c) => **return** (a+b)*c

example1 = calculate (1, 2, 3)
example2 = calculate ([1, 2, 3], [4, 5, 6], 2)
example3 = calculate ('apples ', 'and oranges, ', 3)

**print** **to\_string** example1
**print** **to\_string** example2
**print** **to\_string** example3
```

在樣例中，每次對 `calculate` 的呼叫都使用的物件（數字、列表和字串）在繼承關係中沒有聯絡。只要物件支援「+」和「\*」方法，操作就能成功。例如，翻譯成 Ruby 或 Python 語言，執行結果應該是：

```
9
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
apples and oranges, apples and oranges, apples and oranges, 
```

這樣，鴨子型別在不使用繼承的情況下使用了多型。唯一的要求是 `calculate` 函式需要作為參數的物件擁有「+」和「\*」方法。以下樣例（Python 語言）體現了鴨子測試。就 `in_the_forest` 函式而言，物件是一個鴨子：

## 靜態語言中的鴨子型別

一些通常的靜態語言如 Boo 和 C# 第四版，有一些額外的類型註解，它們指示編譯器將類的型別檢查安排在執行時而不是編譯時，並在編譯器的輸出中包含用於執行時型別檢查的代碼。這些附加的內容允許這些語言享受鴨子型別的大多數益處，僅有的缺點是需要在編譯時辨識和指定這些動態類。

## 與其他型別系統的比較

### 結構型別系統

鴨子型別和結構類型相似但與之不同。結構類型由類型的結構決定類型的相容性和等價性，而鴨子型別只由結構中在執行時所存取的部分決定類型的相容性。Objective Caml 語言使用結構型別系統。

### 介面

介面可以提供鴨子型別的一些益處，但鴨子型別與之不同的是沒有顯式定義任何介面。例如，如果一個第三方 Java 庫實現了一個使用者不允許修改的類，使用者就無法把這個類別的實例用作一個自己定義的介面的實現，而鴨子型別允許這樣做。

### 模板或泛型

模板函式或方法在一個靜態型別上下文中應用鴨子測試；這同時帶來了靜態和動態型別檢查的一般優點和缺點。同時，由於在鴨子型別中，只有「在執行時被實際呼叫的」方法需要被實現，而模板要求實現「在編譯時不能證明不可到達的」所有方法，因此鴨子型別更具有可伸縮性。

實例包括帶有模板的 C++ 語言和 Java 語言的泛型。

## 批評

關於鴨子型別常常被參照的一個批評是它要求程式設計師在任何時候都必須很好地理解他 / 她正在編寫的代碼。在一個強靜態型別的、使用了類型繼承樹和參數型別檢查的語言中，給一個類提供未預測的物件類型更為困難。例如，在 Python 中，你可以建立一個稱為 Wine 的類，並在其中需要實現 press 方法。然而，一個稱為 Trousers 的類可能也實現 press () 方法。為了避免奇怪的、難以檢測的錯誤，開發者在使用鴨子型別時需要意識到每一個「press」方法的可能使用，即使在語意上和他 / 她所正在編寫工作的代碼沒有任何關係。

本質上，問題是：「如果它走起來像鴨子並且叫起來像鴨子」，它也可以是一隻正在模仿鴨子的龍。儘管它們可以模仿鴨子，但也許你不總是想讓龍進入池塘。

鴨子型別的提倡者，如吉多・范羅蘇姆，認為這個問題可以通過在測試和維護代碼庫前擁有足夠的了解來解決。

對鴨子型別的批評傾向於成為關於動態型別和靜態型別的爭論的更廣闊的觀點的特殊情形。

## 歷史

Alex Martelli 很早（2000 年）就在發布到 comp.lang.python 新聞群組上的一則訊息（頁面存檔備份，存於網際網路檔案館）中使用了這一術語。他同時對鴨子測試的錯誤的字面理解提出了提醒，以避免人們錯誤認為這個術語已經被使用。

　
: 「換言之，不要檢查它**是不是**一個鴨子：檢查它**像不像**一個鴨子地**叫**，等等。取決於你需要哪個像鴨子的行為的子集來使用語言。」

## 實現

### 在 ColdFusion 中

web 應用程式手稿語言 ColdFusion 允許函式參數被指定為類型為 _any_。對於這種參數，任意物件都可被傳入，函式呼叫在執行時被動態繫結。如果物件沒有實現一個被呼叫的函式，一個可被擷取並優雅地處理的執行時異常將被丟擲。在 ColdFusion 8 中，這也可以被一個已定義的事件 onMissingMethod () 而不是例外處理器處理。另一個可替代的參數類型 _WEB-INF.cftags.component_ 限制傳入參數是一個 ColdFusion 組件（CFC），在一個不正確的物件傳入時它提供了更好的錯誤訊息。

### 在 C# 中

C# 4.0 實現了動態成員查詢（dynamic member lookup）實現了鴨子型別化。注意下例中類別方法 InTheForest 的參數類型被聲明為 dynamic。

### 在 Common Lisp 中

Common Lisp 提供了一個物件導向的擴充（Common Lisp 物件系統，簡寫為 CLOS）。在 Common Lisp 中，CLOS 和 Lisp 的動態型別使鴨子型別成為一種通用的編程風格。

使用 Common Lisp，使用者通常不需要查詢類型，因為如果一個函式不適用，系統會丟擲一個執行時錯誤。這個錯誤可以被 Common Lisp 的條件系統處理。在類外定義的方法也可以為指定的物件定義。

Common Lisp 通常的開發風格（像 SLIME 一樣使用 Lisp REPL）也允許互動式修復：

通過這種方法，軟體可以通過擴充只有部分工作的使用鴨子型別的代碼來開發。

### 在 Objective-C 中

Objective-C，C 和 Smalltalk 的一個交錯，像 Smalltalk 一樣，允許使用者聲明一個物件的類型為「id」並向它傳送任何資訊。傳送者可以測試一個物件以了解它能不能對一個訊息回應，物件可以在收到訊息的時候決定回應與否，如果傳送者傳送了一個接收者不能回應的訊息，一個異常會被丟擲。因此，鴨子型別在 Objective-C 中被完全支援。

### 在 Python 中

鴨子型別在 Python 中被廣泛使用。Python 術語表（頁面存檔備份，存於網際網路檔案館）這樣定義鴨子型別：

在 Python 中，鴨子型別的最典型例子就是類似 file 的類。這些類可以實現 `file` 的一些或全部方法，並可以用於 `file` 通常使用的地方。例如，`GzipFile`（頁面存檔備份，存於網際網路檔案館）實現了一個用於存取 gzip 壓縮的資料的類似 file 的物件。`cStringIO` 允許把一個 Python 字串視作一個檔案。通訊端（socket）也和檔案共同擁有許多相同的方法。然而通訊端缺少 `tell()` 方法（頁面存檔備份，存於網際網路檔案館），不能用於 `GzipFile` 可以使用的所有地方。這體現了鴨子型別的可伸縮性：一個類似 file 的物件可以實現它有能力實現的方法，且只能被用於它有意義的情形下。

EAFP 原則描述了例外處理的使用。例如相對於檢查一個自稱為類似 Duck 的物件是否擁有一個 _quack()_ 方法（使用 `if hasattr(mallard, "quack"): ...`），人們通常更傾向於用例外處理把對 _quack_ 的呼叫嘗試包裹起來：

這個寫法的優勢在於它鼓勵結構化處理其他來自類的錯誤（這樣的話，例如，一個不能完成 quack 的 Duck 子類可以丟擲一個「QuackException」，這個異常可以簡單地添加到包裹它的代碼，並不需要影響更多的代碼的邏輯。同時，對於其他不同類的物件存在不相容的成員而造成的命名衝突，它也能夠處理（例如，假設有一個醫學專家 Mallard 有一個布林屬性將他分類為「quack=True」，試圖執行 Mallard.quack () 將丟擲一個 TypeError）。

在更實際的實現類似 file 的行為的例子中，人們更傾向於使用 Python 的例外處理機制來處理各種各樣的可能因為各種程式設計師無法控制的環境和作業系統問題而發生的 I/O 錯誤。在這裡，「鴨子型別」產生的異常可以在它們自己的子句中擷取，與作業系統、I/O 和其他可能的錯誤分別處理，從而避開複雜的檢測和錯誤檢查邏輯。

## 參考文獻

## 參閱

* 鴨子測試 (duck test)

## 外部連結

* Duck Typing: Ruby（頁面存檔備份，存於網際網路檔案館）
* How to duck type? - the psychology of static typing in Ruby（頁面存檔備份，存於網際網路檔案館）
* Python 術語表中的 duck-typing 詞條（頁面存檔備份，存於網際網路檔案館）
* Dr. Dobbs June 01 2005: "Templates and Duck Typing"（頁面存檔備份，存於網際網路檔案館）
* Javascript 'typeof' limitations and duck typing
* Python from a Java perspective - Part 2 - How duck typing influences class design and design principles（頁面存檔備份，存於網際網路檔案館）