鴨子類型

面向对象编程中的一种动态类型风格

鴨子類型(英語:duck typing)在程序設計中是動態類型的一種風格。在這種風格中,一個物件有效的語義,不是由繼承自特定的類或實現特定的接口,而是由「當前方法和屬性的集合」決定。這個概念的名字來源於由詹姆斯·惠特科姆·萊利英語James Whitcomb Riley提出的鴨子測試(見下面的「歷史」章節),「鴨子測試」可以這樣表述:

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

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

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

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

概念樣例

考慮用於一個使用鴨子類型的語言的以下偽代碼

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的調用都使用的物件(數字、列表和字符串)在繼承關係中沒有聯繫。只要物件支持「+」和「*」方法,操作就能成功。例如,翻譯成RubyPython語言,運行結果應該是:

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函數而言,物件是一個鴨子:

class Duck:
    def quack(self):
        print("这鸭子正在嘎嘎叫")

    def feathers(self):
        print("这鸭子拥有白色和灰色的羽毛")

class Person:
    def quack(self):
        print("这人正在模仿鸭子")

    def feathers(self):
        print("这人在地上拿起1根羽毛然后给其他人看")

def in_the_forest(duck):
    duck.quack()
    duck.feathers()

def game():
    donald = Duck()
    john = Person()
    in_the_forest(donald)
    in_the_forest(john)

game()

靜態語言中的鴨子類型

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

與其他類型系統的比較

結構類型系統

鴨子類型和結構類型英語Structural type system相似但與之不同。結構類型由類型的結構決定類型的兼容性和等價性,而鴨子類型只由結構中在運行時所訪問的部分決定類型的兼容性。Objective Caml語言使用結構類型系統。

接口

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

模板或泛型

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

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

批評

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

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

鴨子類型的提倡者,如吉多·范羅蘇姆,認為這個問題可以通過在測試和維護代碼庫前擁有足夠的了解來解決[5][6]

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

歷史

Alex Martelli很早(2000年)就在發布到comp.lang.python新聞組上的一則消息[7]中使用了這一術語。他同時對鴨子測試的錯誤的字面理解提出了提醒,以避免人們錯誤認為這個術語已經被使用。

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

實現

在Common Lisp中

Common Lisp提供了一個物件導向的擴展(Common Lisp物件系統,簡寫為CLOS)。在Common Lisp中,CLOS和Lisp的動態類型使鴨子類型成為一種通用的編程風格。

使用Common Lisp,用戶通常不需要查詢類型,因為如果一個函數不適用,系統會拋出一個運行時錯誤。這個錯誤可以被Common Lisp的條件系統處理。在類外定義的方法也可以為指定的物件定義。

(defclass duck () ())

(defmethod quack ((a-duck duck))
  (print "这鸭子正在嘎嘎叫"))

(defmethod feathers ((a-duck duck))
  (print "这鸭子有白色和灰色羽毛"))

(defclass person () ())

(defmethod quack ((a-person person))
  (print "这人正在模仿鸭子"))

(defmethod feathers ((a-person person))
  (print "这人在地上拿起1根羽毛然后给其他人看"))

(defmethod in-the-forest (duck)
  (quack duck)
  (feathers duck))

(defmethod game ()
  (let ((donald (make-instance 'duck))
        (john (make-instance 'person)))
    (in-the-forest donald)
    (in-the-forest john)))

(game)

Common Lisp通常的開發風格(像SLIME一樣使用Lisp REPL)也允許交互式修復:

? (defclass cat () ())
#<STANDARD-CLASS CAT>
? (quack (make-instance 'cat))
> Error: There is no applicable method for the generic function:
>          #<STANDARD-GENERIC-FUNCTION QUACK #x300041C2371F>
>        when called with arguments:
>          (#<CAT #x300041C7EEFD>)
> If continued: Try calling it again
1 > (defmethod quack ((a-cat cat))
        (print "这猫正在模仿鸭子"))

#<STANDARD-METHOD QUACK (CAT)>
1 > (continue)

"这猫正在模仿鸭子"

通過這種方法,軟件可以通過擴展只有部分工作的使用鴨子類型的代碼來開發。

在Python中

鴨子類型在Python中被廣泛使用。Python術語表[8]這樣定義鴨子類型:

Pythonic programming style that determines an object's type by inspection of its method or attribute signature rather than by explicit relationship to some type object ("If it looks like a duck and quacks like a duck, it must be a duck.") By emphasizing interfaces rather than specific types, well-designed code improves its flexibility by allowing polymorphic substitution. Duck-typing avoids tests using type() or isinstance(). Instead, it typically employs the EAFP (Easier to Ask Forgiveness than Permission) style of programming.

在Python中,鴨子類型的最典型例子就是類似file的類。這些類可以實現file的一些或全部方法,並可以用於file通常使用的地方。例如,GzipFile[9]實現了一個用於訪問gzip壓縮的數據的類似file的物件。cStringIO[10]允許把一個Python字符串視作一個文件。套接字(socket)也和文件共同擁有許多相同的方法。然而套接字缺少tell()方法[11],不能用於GzipFile可以使用的所有地方。這體現了鴨子類型的可伸縮性:一個類似file的物件可以實現它有能力實現的方法,且只能被用於它有意義的情形下。

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

try:
    mallard.quack()
except (AttributeError, TypeError):
    print("mallard并沒有quack()函数")

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

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

在C#中

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

using System;

namespace DuckTyping 
{    
    public class Duck 
    {
        public void Quack()    { Console.WriteLine("这鸭子正在嘎嘎叫"); }
        public void Feathers() { Console.WriteLine("这鸭子拥有白色与灰色羽毛"); }
    }

    public class Person 
    {
        public void Quack()    { Console.WriteLine("这人正在模仿鸭子"); }
        public void Feathers() { Console.WriteLine("这人在地上拿起1根羽毛然后给其他人看"); }
    }

    internal class Program 
    {
        private static void InTheForest(dynamic duck) 
        {
            duck.Quack();
            duck.Feathers();
        }

        private static void Game() 
        {
            Duck donald = new Duck();
            Person john = new Person();
            InTheForest(donald);
            InTheForest(john);
        }

        private static void Main() 
        {
            Game();
        }
    }
}

在Objective-C中

Objective-C,C和Smalltalk的一個交錯,像Smalltalk一樣,允許用戶聲明一個物件的類型為「id」並向它發送任何信息。發送者可以測試一個物件以了解它能不能對一個消息響應,物件可以在收到消息的時候決定響應與否,如果發送者發送了一個接收者不能響應的消息,一個異常會被拋出。因此,鴨子類型在Objective-C中被完全支持。

在ColdFusion中

web應用程序腳本語言ColdFusion允許函數參數被指定為類型為any。對於這種參數,任意物件都可被傳入,函數調用在運行時被動態綁定。如果物件沒有實現一個被調用的函數,一個可被捕獲並優雅地處理的運行時異常將被拋出。在ColdFusion 8中,這也可以被一個已定義的事件onMissingMethod()而不是異常處理器處理。另一個可替代的參數類型WEB-INF.cftags.component限制傳入參數是一個ColdFusion組件(CFC),在一個不正確的物件傳入時它提供了更好的錯誤消息。

在JavaScript中

class Duck 
{
    constructor() {
        this.quack = () => console.info("这鸭子正在嘎嘎叫");
        this.feathers = () => console.info("这鸭子拥有白色与灰色羽毛");
    }
}

class Person 
{
    constructor() {
        this.quack = () => console.info("这人正在模仿鸭子");
        this.feathers = () => console.info("这人在地上拿起一根羽毛然后给其他人看");
    }
}

function InTheForest(duck) 
{
    duck.quack();
    duck.feathers();
}

function game() 
{
    var donald = new Duck();
    var john = new Person();
    InTheForest(donald);
    InTheForest(john);
}

game();

參考文獻

  1. ^ Davis, Robin S. Who's Sitting on Your Nest Egg?. : 7 [2010-01-30]. (原始內容存檔於2014-06-28). 
  2. ^ Heim, Michael. Exploring Indiana Highways. : 68 [2010-01-30]. (原始內容存檔於2014-06-28). 
  3. ^ Boo: Duck Typing. [2009-07-15]. (原始內容存檔於2008-10-06). 
  4. ^ Anders Hejlsberg Introduces C# 4.0 at PDC 2008. [2009-07-15]. (原始內容存檔於2009-12-16). 
  5. ^ Bruce Eckel. Strong Typing vs. Strong Testing. mindview. [2009-07-15]. (原始內容存檔於2009-03-06). 
  6. ^ Bill Venners. Contracts in Python. A Conversation with Guido van Rossum, Part IV. Artima. [2009-07-15]. (原始內容存檔於2009-04-29). 
  7. ^ polymorphism (was Re: Type checking in python?)頁面存檔備份,存於網際網路檔案館
  8. ^ Python術語表頁面存檔備份,存於網際網路檔案館
  9. ^ GzipFile頁面存檔備份,存於網際網路檔案館
  10. ^ cStringIO
  11. ^ tell()方法頁面存檔備份,存於網際網路檔案館

參閱

外部連結