什么是duck typing
Suppose you see a bird walking around in a farm yard. This bird has no label that says 'duck'. But the bird certainly looks like a duck. Also, he goes to the pond and you notice that he swims like a duck. Then he opens his beak and quacks like a duck. Well, by this time you have probably reached the conclusion that the bird is a duck, whether he's wearing a label or not.
–Richard Cunningham Patterson Jr., United States ambassador to Guatemala in 1950
以上是一段对于所谓鸭子测试的最有名的阐述。意思是对于事物类型的判断, 不取决于事物本身预设的标签(label), 而取决于判断者判断时需要用到的条件, 如果事物拥有符合条件的属性,那么在判断者眼中它就是那种类型。举例来说,对于什么是鸭子, 在小孩眼中只要长得像鸭子的都是鸭子,不管是玩具还是图画还是鸭嘴兽, 而在吃货眼中吃起来是鸭子味道的才是,不管是八宝鸭还是周黑鸭还是鸭霸王。
在程序设计中,鸭子类型(英语:Duck typing)是动态类型和某些静态语言的一种对象推断风格。这种风格适用于动态语言(比如PHP、Python、Ruby、Typescript、Perl、Objective-C、Lua、Julia、JavaScript、Java、Groovy、C#等)和某些静态语言(比如Golang,一般来说,静态类型语言在编译时便已确定了变量的类型,但是Golang的实现是:在编译时推断变量的类型),支持"鸭子类型"的语言的解释器/编译器将会在解析(Parse)或编译时,推断对象的类型。
在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为鸭的对象,并调用它的走和叫方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的走和叫方法。对于动态类型的语言来说, 如果这些需要被调用的方法不存在,那么将引发一个运行时错误。而对于golang来说, 如果这些被调用的方法不存在,编译时就会报错。
python(动态语言)的duck typing
假如有个叫say_quack的Python函数, 它接受一个接口参数,参数的类型不固定,只要有quack方法就可以啦。
def say_quack(duck)
duck.quack()
class RealDcuk:
def quack(self):
print("quack quack")
class ToyDuck:
def quack(self):
print("squee squee")
duck = RealDuck()
say_quack(duck)
toyDuck = ToyDuck()
say_quack(duck)
可以看出动态语言的duck typing非常灵活方便,类型的检测和使用不依赖于编译器的静态检测,而是依赖文档、清晰的代码和测试来确保正确使用。这样其实是牺牲了安全性来换取灵活性。 假设你没有认真看文档,不知道say_quack方法的duck参数是需要quack方法, 你编写了一个Dog类,它只有一个run方法, 你把它的对象当成参数给say_quack编译时也是不会报错的。只有在运行时才会报错, 这样就存在很大的安全隐患。有没有一种折中(tradeoff), 兼顾这种duck typing的灵活性和静态检测的安全性呢?
go语言接口的隐式实现
假如你有个golang的接口叫Duck:
type Duck interface {
quack()
}
任何拥有quack方法的类型, 都隐式地(implicitly)实现了Duck接口, 并能当做Duck接口使用。
package main
import (
"fmt"
)
type Duck interface {
quack()
}
type RealDuck struct {
}
func (d RealDuck) quack() {
fmt.Println("quack quack")
}
type ToyDuck struct {
}
func (d ToyDuck) quack() {
fmt.Println("squee squee")
}
func sayQuack(d Duck) {
d.quack()
}
func main() {
realDuck := RealDuck{}
toyDuck := ToyDuck{}
sayQuack(realDuck)
sayQuack(toyDuck)
}
如果你有一个Dog类型, 它没有quack方法, 当你用它做sayQuack参数时, 编译时就会报错。另外来说, 如果接口使用者定义了一个新的接口也拥有quack方法, 那上面的RealDuck和ToyDuck也可以当做新的接口来使用。
这样就达到了一个灵活性和安全性的平衡。因为go对接口的实现是隐式的, 所以它的接口类型在使用之前是不固定的, 它可以灵活的变成各种接口类型,只要它满足使用者的对接口的要求。 又因为使用者使用接口时在编译时就对接口实现者有没有满足接口需求进行了检测,所以又兼顾了安全性。