鸭子类型

鸭子类型(英语: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新闻组上的一则消息页面存档备份,存于互联网档案馆)中使用了这一术语。他同时对鸭子测试的错误的字面理解提出了提醒,以避免人们错误认为这个术语已经被使用。

“换言之,不要检查它是不是一个鸭子:检查它像不像一个鸭子地,等等。取决于你需要哪个像鸭子的行为的子集来使用语言。”

实现

在ColdFusion中

web应用程序脚本语言ColdFusion允许函数参数被指定为类型为any。对于这种参数,任意对象都可被传入,函数调用在运行时被动态绑定。如果对象没有实现一个被调用的函数,一个可被捕获并优雅地处理的运行时异常将被抛出。在ColdFusion 8中,这也可以被一个已定义的事件onMissingMethod()而不是异常处理器处理。另一个可替代的参数类型WEB-INF.cftags.component限制传入参数是一个ColdFusion组件(CFC),在一个不正确的对象传入时它提供了更好的错误消息。

在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();
        }
    }
}

在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)

"这猫正在模仿鸭子"

通过这种方法,软件可以通过扩展只有部分工作的使用鸭子类型的代码来开发。

在Objective-C中

Objective-C,C和Smalltalk的一个交错,像Smalltalk一样,允许用户声明一个对象的类型为“id”并向它发送任何信息。发送者可以测试一个对象以了解它能不能对一个消息响应,对象可以在收到消息的时候决定响应与否,如果发送者发送了一个接收者不能响应的消息,一个异常会被抛出。因此,鸭子类型在Objective-C中被完全支持。

在Python中

鸭子类型在Python中被广泛使用。Python术语表页面存档备份,存于互联网档案馆)这样定义鸭子类型:

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页面存档备份,存于互联网档案馆)实现了一个用于访问gzip压缩的数据的类似file的对象。cStringIO允许把一个Python字符串视作一个文件。套接字(socket)也和文件共同拥有许多相同的方法。然而套接字缺少tell()方法页面存档备份,存于互联网档案馆),不能用于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和其他可能的错误分别处理,从而避开复杂的检测和错误检查逻辑。

参考文献

  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). 

参阅

  • 鸭子测试(duck test)

外部链接