什么是鸭子类型?

589

1
@Mitch 我尝试了一下,得到的结果是某种形式的继承。但是我并没有理解太多。如果我问错了问题,请原谅。 - sushil bharwani
4
@sushil bharwani:不,不生气。但人们期望你在发帖之前首先尝试搜索(即你要做的第一件事情)。 - Mitch Wheat
134
根据以上论点,似乎stackoverflow并不是必要的,因为我相信几乎每个可能想到的问题都已经在互联网上得到了回答,如果没有,通过给知识渊博的朋友发送电子邮件可能更容易获得答案,并且不会受到批评。我认为你们中的许多人都误解了stackoverflow的意义。 - rhody
61
我相信我曾经在某个地方读到,SO旨在成为“标准问题的存储库”,而且我非常确定你找不到比这个问题更标准的了。 - heltonbiker
8
如果它看起来像鸭子,游泳的时候像鸭子,叫声也像鸭子,那么它很可能就是一只鸭子。 - Mohammed H
显示剩余4条评论
19个回答

396

这是一个术语,用于动态语言中,这些语言没有强类型

这个想法是,为了调用对象上的现有方法,您不需要指定类型 - 如果在对象上定义了方法,您可以调用它。

名称来自短语“如果看起来像鸭子,叫起来像鸭子,那么它就是鸭子”。

维基百科有更多信息。


27
谨慎使用强类型。它的定义并不是那么清晰。鸭子类型也是如此。Google Go或Ocaml是具有结构次类型构造的静态类型语言。这些是鸭子类型的语言吗? - I GIVE CRAP ANSWERS
12
“鸭子类型”更好的说法是:“如果它说它是一只鸭子,那对我来说就足够了。”请参见 http://pyvideo.org/video/1669/keynote-3 28:30 或 http://www.youtube.com/watch?v=NfngrdLv9ZQ#t=1716。 - tovmeod
9
鸭子类型并不仅仅在动态语言中使用。Objective-C 不是一种动态语言,但它使用鸭子类型。 - eyuelt
14
Python和Ruby都是强类型语言,且均采用鸭子类型。强类型并不意味着不能使用鸭子类型。 - alanjds
11
我要给这个点个踩。Duck ducking与类型的强度无关,只与能否使用具有方法的任何对象有关,无论它是否实现了接口。 - Bite code
显示剩余7条评论

260
鸭子类型意味着一个操作并没有正式规定其操作数必须满足的要求,而是只是尝试使用给定的内容进行操作。
与其他人所说的不同,这并不一定涉及动态语言或继承问题。 示例任务:在对象上调用一些方法Quack
如果不使用鸭子类型,执行这个任务的函数f需要预先指定其参数必须支持某些方法Quack。常见的方法是使用接口。
interface IQuack { 
    void Quack();
}

void f(IQuack x) { 
    x.Quack(); 
}

调用f(42)会失败,但只要donaldIQuack子类型的实例,f(donald)就可以工作。

另一种方法是结构类型——但同样,方法Quack()在形式上被指定为任何不能证明它quack的东西都会导致编译器失败。

def f(x : { def Quack() : Unit }) = x.Quack() 

我们甚至可以编写:

f :: Quackable a => a -> IO ()
f = quack

在Haskell中,Quackable类型类确保了我们方法的存在。
正如我所说,鸭子类型系统并不指定要求,而只是尝试是否有效。
因此,像Python这样的动态类型系统总是使用鸭子类型。
def f(x):
    x.Quack()

如果 f 得到一个支持 Quack()x,一切都没问题,否则在运行时会崩溃。
但是,鸭子类型并不意味着动态类型 - 实际上,有一种非常流行但完全静态的鸭子类型方法,它不提出任何要求:
template <typename T>
void f(T x) { x.Quack(); } 

该函数并不明确说明它需要一些可以“Quack”的“x”,因此它只是在编译时尝试,如果一切正常,就可以了。

5
你的意思是:void f(IQuack x) { x.Quak(); } 而不是 K.Quack,因为函数 f 的参数是 IQuack x 而不是 IQuack k,这是一个非常小的错误,但我觉得需要纠正 :) - dominicbri7
2
根据维基百科,你最后的例子是“结构类型”,而不是“鸭子类型”。 - Brilliand
好的,似乎有一个单独的问题可以讨论这个:https://dev59.com/aXI-5IYBdhLWcg3wQV4Z - Brilliand
4
如果我理解你的话,支持鸭子类型和不支持的语言之间的区别只是在于使用鸭子类型时,你不必指定函数接受的对象的类型吗? 例如def f(x)而不是 def f(IQuack x) - PProteus

199

简单解释

什么是鸭子类型?

“如果它走起来像鸭子,叫起来像……等等” - 是的,但那是什么意思?!

我们关注的是“对象”能做什么,而不是它们是什么

让我们通过一个例子来解释:

Explanation of Duck Typing

请参阅以下详细信息:
鸭子类型功能的示例:
想象一下,我有一根魔法棒。它有特殊的力量。如果我挥动魔法棒并对一辆汽车说“开车!”,那么它就会开动!
它适用于其他东西吗?不确定:所以我试试对一辆卡车使用它。哇 - 它也能开动!然后我试试对飞机、火车和1号木杆(它们是人们用来“开球”的一种高尔夫球杆)。它们都能开动!
但是,如果我试试对一个茶杯使用它呢?错误:咔嚓-砰!那个不太好。茶杯不能开车!!当然了!
这基本上就是鸭子类型的概念。它是一个“先试试再买”的系统。如果它起作用,一切都好。但是如果它失败了,就像手里还握着一颗手榴弹,它会在你的脸上爆炸。
换句话说,我们关注的是对象“能做什么”,而不是对象“是什么”。
那么像C#或Java等语言呢?
如果我们关心的是物体的实际属性,那么我们的魔术只能对预设的、授权的类型起作用——在这种情况下是汽车,但对于其他可以驾驶的物体,如卡车、摩托车、嘟嘟车等,魔术就会失败。它对卡车不起作用,因为我们的魔术棒只期望它对汽车起作用。
换句话说,在这种情况下,魔术棒非常关注物体的本质(它是一辆汽车吗?),而不是物体的功能(例如汽车、卡车等是否能够行驶)。
要让卡车行驶的唯一方法是让魔术棒同时期望卡车和汽车(可能通过“实现一个公共接口”来实现)。这可以通过一种叫做“多态性”的技术或使用“接口”来实现——它们有点类似。如果你喜欢卡通并且想要解释,请查看我的关于接口的卡通
总结:关键要点
鸭子类型的重要之处在于对象实际上能够做什么,而不是对象是什么

代码示例

但是高尔夫球杆怎么能像汽车一样“驾驶”呢?它们不是不同的吗?如果你使用像Ruby这样的语言,我们不关心对象是什么:

class Car
   def drive
      "I"m driving a Car!"
   end
end

class GolfClub
   def drive
      "I"m driving a golf club!"
   end 
end

def test_drive(item)   
   item.drive # don't care what it is, all i care is that it can "drive"
end

car = Car.new
test_drive(car) #=> "I'm driving a Car"

club = GolfClub.new
test_drive(club) #=> "I"m driving a GolfClub"

# welcome to duck typing!

PS. 我想省略学术废话:这个答案并不科学,但希望通过类比给你一个模糊的理解。
PPS. 如果你想笑一笑,有些时间可以抽空看看:马特·达蒙在《心灵捕手》中对鸭子类型的解释 Good Will Hunting ;)

1
我觉得有趣的前提是你更关心行为,而不是定义。毫无疑问,BDD 在像 Ruby 这样的语言中非常成功。 - Pablo Olmos de Aguilera C.
6
最明确的解释。我已经厌倦了“鸭子”解释,你似乎也是(我用粗体强调):“(“如果它走起来像鸭子,嘎嘎叫起来像鸭子,那么它就是一只鸭子。”)- 是的!但这是什么意思?!” 现在我明白了:不要先检查对象的类型,而是直接尝试方法适用于任何对象。 - Gabriel Staples
1
我对此感到太有热情了,不得不添加自己的答案 :)。 - Gabriel Staples
此外,我认为你应该把你的“摘要”放在开头。那是最重要的要点。也许可以把它留到最后,但同时也要把它的副本放在开头。 - Gabriel Staples
但是汽车的驱动方式不同于高尔夫球杆的驱动方式,不是吗? - hitautodestruct

47
考虑你正在设计一个简单的函数,该函数获取一个类型为 Bird 的对象并调用其 walk() 方法。你可以考虑两种方法:
  1. 这是我的函数,我必须确保它仅接受 Bird 类型或代码将无法编译。如果有人想使用我的函数,他们必须知道我只接受 Bird
  2. 我的函数获取任何 对象,然后只需调用对象的 walk() 方法。因此,如果 object 可以 walk(),那么就是正确的。如果不能,我的函数将失败。因此,重要的不是对象是 Bird 还是其他任何东西,而是它能够 walk()(这就是 鸭子类型)。
必须考虑到 鸭子类型 在某些情况下可能很有用。例如,Python 经常使用 鸭子类型

有用的阅读材料


2
很好的解释,有什么优点吗? - sushil bharwani
5
这个答案简单明了,对于初学者来说可能是最好的。阅读这个答案以及它上面的答案(如果它被移动了,则是那个谈论汽车和茶杯的答案)。 - DORRITO

32

我看到很多答案都在重复那句老话:

如果它看起来像鸭子,叫起来像鸭子,那么它就是鸭子

然后进入对鸭子类型可以做什么的解释,或者是一个似乎更加混淆概念的例子。

我并不觉得这有太大帮助。

以下是我找到的最好的一份简单易懂的关于鸭子类型的解释:

鸭子类型指的是对象是通过其行为定义的,而不是通过其本身的属性或类型定义的。

这意味着我们不太关心一个对象的类/类型,而更关心可以在其上调用哪些方法以及可以对其执行哪些操作。我们不关心它的类型,我们关心它能做什么


1
太棒了!有些人可能还会说这意味着一种技术,即对象的类型不是由其继承关系决定,而是由其属性(例如字段、方法等)决定。相关链接:https://stackoverflow.com/a/4205164/5113030(*Chris Baxter的回答...*) - undefined

29

不要成为一个江湖骗子;我支持你:

"鸭子类型" := "尝试方法,而不是检查类型"

注意::= 可以读作 "被定义为"

"鸭子类型" 的意思是: 直接在任何对象上尝试方法(函数调用),而不是首先检查对象的类型以查看该方法是否是这种类型的有效调用。

我们称之为"尝试方法,而不是检查类型" 类型,"方法调用类型检查" 或简称"方法调用类型检查"

在下面更详细的解释中,我将详细解释这一点,并帮助您理解荒谬、深奥和晦涩难懂的术语"鸭子类型"。


更详细的解释:

挂掉!

Python实现了上述概念。考虑这个例子函数:

def func(a):
    a.method1()
    a.method2()

当对象(输入参数 a)进入函数 func() 时,函数应尝试(在运行时)调用该对象上指定的任何方法(即:以上示例中的 method1()method2()),而不是首先检查 a 是否为某个具有这些方法的“有效类型”。
因此,它是一种基于动作的运行时尝试,而不是基于类型的编译时或运行时检查。
现在看看这个愚蠢的例子:
def func(duck_or_duck_like_object):
    duck_or_duck_like_object.quack()
    duck_or_duck_like_object.walk()
    duck_or_duck_like_object.fly()
    duck_or_duck_like_object.swim()

因此,愚蠢的短语诞生了:
“如果它像鸭子一样走路和叫喊,那么它就是一只鸭子。”
使用“鸭子类型”的程序将简单地尝试在对象上调用任何方法(在上面的示例中:quack()、walk()、fly()和swim()),甚至不知道对象的类型!它只是尝试这些方法!如果它们有效,那太好了,对于所有“鸭子类型”语言而言或关心的是,它(传递到函数中的对象)是一只鸭子!——因为所有(类似鸭子的)方法都可以在它上面工作。
(总结我的话):
“鸭子类型”语言不会检查它的类型(无论是在编译时还是运行时)——它不关心。它只会在运行时尝试这些方法。如果它们有效,那太好了。如果它们没有,那么它将抛出运行时错误。
这就是鸭子类型。
我很厌倦这个荒谬的“鸭子”解释(因为没有这个完整的解释,它根本就没有意义!),其他人也是如此。例如,从BKSpurgeon的答案(我加粗的重点):
(“如果它像鸭子一样走路和叫喊,那么它就是一只鸭子。”)-是的!但这是什么意思?”)
现在我明白了:在不检查对象类型的情况下尝试任何对象。
我将称之为“运行时检查,程序只是尝试调用方法而不知道对象是否具有这些方法,而不是首先检查对象类型作为知道对象是否具有这些方法的手段”,因为这更有意义。但是……这太长了,所以人们宁愿通过说荒谬但引人注目的事情来相互困惑数年,如“鸭子类型”。
让我们改称之为:“尝试方法,不检查类型”类型。或者,也许是:“方法调用类型检查”(简称“方法调用类型”),或者“通过方法调用间接类型检查”,因为它使用给定方法的调用作为“证明”对象是正确类型的证据,而不是直接检查对象的类型。
请注意,这种“方法调用类型检查”(有时会令人困惑地称为“鸭子类型”)是一种动态类型。但是,并非所有的动态类型都必须是“方法调用类型检查”,因为动态类型或运行时类型检查也可以通过实际检查对象的类型来完成,而不仅仅是尝试在不知道其类型的情况下调用函数中调用对象的方法。

另请阅读:

  1. https://en.wikipedia.org/wiki/Duck_typing --> 搜索页面中的“run”、“run time”和“runtime”。

6
谢谢您!我的意思是“太棒了!!”这太惊人了,应该得到更多的点赞! - bermick
谢谢,这更有意义了。那么 JavaScript 是鸭子类型的吗? - manish
1
@manish,我不知道JavaScript,所以我不能确定。如果它是一种脚本语言,具有弱、未指定类型,在函数调用时传递参数时无法自动检查错误(无论是在编译时还是运行时),那么答案是“是”。Python符合这个描述。当你向一个函数传递错误类型的参数时,它不会抛出错误,而是会在你尝试调用该对象类型上的一些无效方法并失败时,抛出运行时错误。 - Gabriel Staples

21

维基百科有一个非常详细的解释:

http://en.wikipedia.org/wiki/Duck_typing

鸭子类型是一种动态类型的风格,其对象的当前方法和属性集决定了有效的语义,而不是它继承于特定类或实现特定接口。

重要的注意点可能是,在使用鸭子类型时,开发人员更关心对象的哪些部分被使用,而不是实际底层类型是什么。


4
我知道我没有给出通用的答案。在Ruby中,我们不声明变量或方法的类型——一切都是某种对象。 因此,规则是“类不是类型”。
在Ruby中,类通常(几乎)不是类型。相反,对象的类型更多地由该对象能够做什么来定义。在Ruby中,我们称之为鸭子类型。如果一个对象走起路来像鸭子,叫起来也像鸭子,那么解释器会愉快地把它当作鸭子对待。
例如,您可能正在编写一个将歌曲信息添加到字符串中的例程。如果您来自C#或Java背景,您可能会尝试编写以下代码:
def append_song(result, song)
    # test we're given the right parameters 
    unless result.kind_of?(String)
        fail TypeError.new("String expected") end
    unless song.kind_of?(Song)
        fail TypeError.new("Song expected")
end

result << song.title << " (" << song.artist << ")" end
result = ""

append_song(result, song) # => "I Got Rhythm (Gene Kelly)"

拥抱Ruby的鸭子类型,你可以写出更简单的代码:
def append_song(result, song)
    result << song.title << " (" << song.artist << ")"
end

result = ""
append_song(result, song) # => "I Got Rhythm (Gene Kelly)"

您无需检查参数类型。如果它们支持<<(在结果的情况下)或标题和艺术家(在歌曲的情况下),一切都会正常工作。如果不支持,您的方法将抛出异常(就像您检查类型时所做的那样)。但是,如果不进行检查,则您的方法变得更加灵活。您可以将数组、字符串、文件或任何其他使用<<添加的对象传递给它,它都能正常工作。


4

看看这门语言本身可能会有所帮助;对我来说通常很有帮助(我不是以英语为母语的人)。

鸭子类型中:

1)单词typing并不是指在键盘上打字(这是我一直以来的印象),而是指确定“那个东西是什么类型的?

2)单词duck表达了如何进行这种确定;它是一种“松散”的确定,就像:“如果它走起路来像只鸭子…那它就是只鸭子”。它是“松散”的,因为这个东西可能是一只鸭子,也可能不是,但它是否真的是一只鸭子并不重要;重要的是我可以像对待鸭子一样对待它,并期望它表现出鸭子的行为。我可以喂它面包屑,这个东西可能会朝我走过来、冲向我或者后退……但它不会像灰熊一样把我吞掉。


4

马特·达蒙在《心灵捕手》中解释鸭子类型

以下是翻译文本。视频链接在此处(点击访问).

查基:好吧,我们会有问题吗?

克拉克:没有问题。我只是希望你能给我一些关于鸭子类型实际上是什么的见解。我的观点是,鸭子类型没有很好地定义,强类型也是如此。

威尔:[打断他]…强类型也是如此。当然那是你的观点。你是一年级的研究生: 你刚刚读完了一篇关于鸭子类型的文章,可能是在StackOverflow网站上,而且你会被说服到下个月,直到你到达Gang of Four,然后你会谈论Google Go和Ocaml是具有结构子类型构造的统计类型语言。这将持续到明年,直到你可能在这里背诵Matz的话,谈论Pre-Ruby 3.0时代的乌托邦以及子类型分配对GC的内存分配影响。

克拉克:[吃惊]实际上我不会这么做,因为Matz严重低估了Ruby 3.0的GC对性能的影响。

威尔:"Matz极度低估了Ruby 3.0的GC对性能的影响。你从Donald Knuth的《计算机编程艺术》第98页上得到的,对吧?是啊,我也读过。你要抄袭整个东西给我们看吗?你有自己的想法吗?还是说这就是你的事情,你来到StackOverflow网站,阅读一些关于r/ruby的晦涩段落,然后假装它是你自己的想法,只是为了打动一些女孩,让我的朋友难堪?

[克拉克目瞪口呆]

威尔:看看像你这样的人的悲哀之处在于,大约50年后,你会开始自己思考,并得出这样的结论:人生有三件必然的事情。第一,别那样做。第二,如果它走起路来像只鸭子,那么它就是只鸭子。第三,你花了15万美元去接受教育,但实际上你可以通过Ben Koshy在StackOverflow网站上免费获得答案。

克拉克:是的,但我会拥有学位,而你将通过React在驱动器通道为我的孩子提供便宜的HTML服务,并在去滑雪之旅的路上。

威尔:[微笑]是啊,也许吧。但至少我不会没有创意。

(片刻沉默)

威尔:你有问题3吗?我想我们可以走出去解决问题。

克拉克:没有问题。

一些时间后:

威尔: 你喜欢苹果吗?

克拉克有点懵了。

威尔: 你喜欢这些苹果怎么样?(啪地一声,威尔把一封来自谷歌公司的录取信拍在窗户上。)我收到了谷歌的录取通知!(向克拉克展示录取信,上面显示着他的面试答案:一只行走、说话、表现得像……鹅的鸭子的图片。)

字幕翻译.

完.

(这是对旧回答的注脚:)

3 Advent of Code问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接