鴨子型別

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

鴨子型別(英語: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()方法頁面存檔備份,存於網際網路檔案館

參閱

外部連結