在看ruby的时候,发现ruby有一种 duck typing 编程风格,这属于语言思想,科普一下
在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定。这个概念的名字来源于由James Whitcomb Riley提出的鸭子测试(见下面的“历史”章节),“鸭子测试”可以这样表述:
在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为鸭的对象,并调用它的走和叫方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的走和叫方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的走和叫方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名。
鸭子类型通常得益于不测试方法和函数中参数的类型,而是依赖文档、清晰的代码和测试来确保正确使用。从静态类型语言转向动态类型语言的用户通常试图添加一些静态的(在运行之前的)类型检查,从而影响了鸭子类型的益处和可伸缩性,并约束了语言的动态特性。
目录[隐藏]
|
考虑用于一个使用鸭子类型的语言的以下伪代码:
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
函数而言,对象是一个鸭子:
class Duck: def quack(self): print "Quaaaaaack!" def feathers(self): print "The duck has white and gray feathers." class Person: def quack(self): print "The person imitates a duck." def feathers(self): print "The person takes a feather from the ground and shows it." def in_the_forest(duck): duck.quack() duck.feathers() def game(): donald = Duck() john = Person() in_the_forest(donald) in_the_forest(john) game()
一些通常的静态,语言如Boo和C#第四版,有一些额外的类型注解,它们指示编译器将类的类型检查安排在运行时而不是编译时,并在编译器的输出中包含用于运行时类型检查的代码[3][4]。这些附加的内容允许这些语言享受鸭子类型的大多数益处,仅有的缺点是需要在编译时识别和指定这些动态类。
鸭子类型和结构类型相似但与之不同。结构类型由类型的结构决定类型的兼容性和等价性,而鸭子类型只由结构中在运行时所访问的部分决定类型的兼容性。Objective Caml语言使用结构类型系统。
接口可以提供鸭子类型的一些益处,但鸭子类型与之不同的是没有显式定义任何接口。例如,如果一个第三方Java库实现了一个用户不允许修改的类,用户就无法把这个类的实例用作一个自己定义的接口的实现,而鸭子类型允许这样做。
模板函数或方法在一个静态类型上下文中应用鸭子测试;这同时带来了静态和动态类型检查的一般优点和缺点。同时,由于在鸭子类型中,只有在运行时被实际调用的方法需要被实现,而模板要求实现在编译时不能证明不可到达的所有方法,因此鸭子类型更具有可伸缩性。
实例包括带有模板的C++语言和Java语言的泛型。
常常被引用的一个批评是:
鸭子类型的提倡者,如Guido van Rossum,认为这个问题可以通过在测试和维护代码库前拥有足够的了解来解决[5][6]。
对鸭子类型的批评倾向于成为关于动态类型和静态类型的争论的更广阔的观点的特殊情形。
Alex Martelli很早(2000年)就在发布到comp.lang.python新闻组上的一则消息中使用了这一术语。他同时对鸭子测试的错误的字面理解提出了提醒,以避免人们错误认为这个术语已经被使用。
web应用程序脚本语言ColdFusion允许函数参数被指定为类型为any。对于这种参数,任意对象都可被传入,函数调用在运行时被动态绑定。如果对象没有实现一个被调用的函数,一个可被捕获并优雅地处理的运行时例外将被抛出。在ColdFusion 8中,这也可以被一个已定义的事件onMissingMethod()而不是例外处理器处理。另一个可替代的参数类型WEB-INF.cftags.component限制传入参数是一个ColdFusion组件(CFC),在一个不正确的对象传入时它提供了更好的错误消息。
Objective-C,C和Smalltalk的一个交错,像Smalltalk一样,允许用户声明一个对象的类型为“id”并向它发送任何信息。发送者可以测试一个对象以了解它能不能对一个消息响应,对象可以在收到消息的时候决定响应与否,如果发送者发送了一个接收者不能响应的消息,一个例外会被抛出。因此,鸭子类型在Objective-C中被完全支持。
鸭子类型在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()
orisinstance()
. 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 can't quack()"
这个写法的优势在于它鼓励结构化处理其他来自类的错误(这样的话,例如,一个不能完成quack的Duck子类可以抛出一个“QuackException”,这个例外可以简单地添加到包裹它的代码,并不需要影响更多的代码的逻辑。同时,对于其他不同类的对象存在不兼容的成员而造成的命名冲突,它也能够处理(例如,假设有一个医学专家Mallard有一个布尔属性将他分类为“quack=True”,试图执行Mallard.quack()将抛出一个TypeError)。
在更实际的实现类似file的行为的例子中,人们更倾向于使用Python的异常处理机制来处理各种各样的可能因为各种程序员无法控制的环境和operating system问题而发生的I/O错误。在这里,“鸭子类型”产生的例外可以在它们自己的子句中捕获,与操作系统、I/O和其他可能的错误分别处理,从而避开复杂的检测和错误检查逻辑。
Common Lisp提供了一个面向对象的扩展(Common Lisp对象系统,简写为CLOS)。在Common Lisp中,CLOS和Lisp的动态类型使鸭子类型成为一种通用的编程风格。
使用Common Lisp,用户通常不需要查询类型,因为如果一个函数不适用,系统会抛出一个运行时错误。这个错误可以被Common Lisp的条件系统处理。在类外定义的方法也可以为指定的对象定义。
(defclass duck () ()) (defmethod quack ((a-duck duck)) (print "Quaaaaaack!")) (defmethod feathers ((a-duck duck)) (print "The duck has white and gray feathers.")) (defclass person () ()) (defmethod quack ((a-person person)) (print "The person imitates a duck.")) (defmethod feathers ((a-person person)) (print "The person takes a feather from the ground and shows it.")) (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 "The cat imitates a duck.")) #<STANDARD-METHOD QUACK (CAT)> 1 > (continue) "The cat imitates a duck."
通过这种方法,软件可以通过扩展只有部分工作的使用鸭子类型的代码来开发。