关于编程语言:什么是鸭子打字?

What is duck typing?

我在网上阅读软件上的随机主题时遇到了"鸭子打字"这个词,但我并不完全理解。

什么是"鸭子打字"?


它是动态语言中使用的一个术语,没有强类型。

其思想是,不需要类型就可以在对象上调用现有方法-如果在对象上定义了方法,则可以调用它。

这个名字来源于"如果它长得像鸭子,嘎嘎叫得像鸭子,那就是鸭子"。

维基百科有更多的信息。


鸭子打字意味着一个操作并没有正式地指定它的操作数必须满足的要求,而是用给定的条件来尝试它。

与其他人所说的不同,这不一定与动态语言或继承问题有关。

示例任务:对对象调用某个方法Quack

在不使用duck类型的情况下,执行此任务的函数f必须事先指定其参数必须支持某些方法Quack。一种常见的方法是使用接口

1
2
3
4
5
6
7
interface IQuack {
    void Quack();
}

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

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

另一种方法是结构类型化——但同样,方法Quack()被正式指定为任何不能证明它的东西,Quack的提前将导致编译器失败。

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

我们甚至可以写

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

在haskell中,Quackable类型类确保了方法的存在。

那么,鸭子打字是如何改变这种情况的呢?

好吧,正如我所说,鸭式打字系统并没有规定要求,只是在有任何效果的情况下进行尝试。

因此,像python那样的动态类型系统总是使用duck类型:

1
2
def f(x):
    x.Quack()

如果f得到一个支持Quack()x,一切都很好,如果不是,它将在运行时崩溃。

但是duck类型根本不意味着动态类型——事实上,有一种非常流行但完全静态的duck类型方法,它也没有给出任何要求:

1
2
template <typename T>
void f(T x) { x.Quack(); }

这个函数没有以任何方式告诉我们它想要一些可以执行Quackx,所以它只是在编译时尝试,如果一切正常,就可以了。


简单说明(无代码)

关于这个问题的语义的讨论是相当微妙的(而且非常学术),但这里有一个一般的想法:

鸭子打字

(如果它像鸭子一样走路,像鸭子一样嘎嘎叫,那么它就是鸭子。)—是的!但这意味着什么??!这一点最好用示例说明:

示例:动态类型语言

想象一下我有一根魔杖。它有特殊的力量。如果我挥动魔杖说"开车!"去一辆车,好吧,它会开车!

它对其他事情有用吗?不确定:所以我在卡车上试试。哇-它也在开车!然后我在飞机上、火车上和一片树林里试一试(它们是人们用来"驾驶"高尔夫球的一种高尔夫俱乐部)。他们都开车!

但它会起作用吗,比如说,茶杯?错误:KAAAA-booooooom!结果不太好。====>茶杯不能开车!啊!?

这基本上就是鸭子打字的概念。这是一个先试再买的系统。如果一切顺利的话。但如果失败了,它会在你脸上爆炸。

换句话说,我们感兴趣的是对象可以做什么,而不是对象是什么。

示例:静态类型语言

如果我们关心的对象实际上是什么,那么我们的魔术将只适用于预设,授权类型-在这种情况下,汽车,但将失败的其他对象可以驱动:卡车,轻便摩托车,燕尾服等。它不会工作在卡车上,因为我们的魔术棒期待它只适用于汽车。

换句话说,在这个场景中,魔杖非常仔细地观察物体是什么(它是一辆车吗?)而不是物体能做什么(例如汽车、卡车等是否可以驾驶)。

唯一能让卡车驱动的方法就是你能以某种方式让魔法棒期望卡车和汽车(也许通过"实现一个公共接口")。如果你不知道这意味着什么,那就暂时忽略它。

总结:关键要点

在duck类型中,重要的是对象实际上可以做什么,而不是对象是什么。


假设您正在设计一个简单的函数,它获取一个Bird类型的对象,并调用它的walk()方法。您可以想到两种方法:

  • 这是我的函数,我必须确保它只接受Bird,否则它们的代码将无法编译。如果有人想使用我的功能,他必须知道我只接受Bird
  • 我的函数得到任何objects,我只是调用对象walk()方法。所以,如果object能做到walk()是正确的,如果不能做到,我的功能就会失效。因此,在这里,对象是Bird或其他任何东西并不重要,重要的是它可以walk()(这是duck类型)
  • 必须考虑到duck类型在某些情况下可能很有用,例如python经常使用duck类型。

    有用阅读

    • 对于Java、Python、JavaScript,有一些很好的例子网址:https://en.wikipedia.org/wiki/duck_typing
    • 这里还有一个很好的答案,描述了动态打字还有它的缺点:动态打字的生产率应该提高多少?


    维基百科有一个相当详细的解释:

    http://en.wikipedia.org/wiki/duck_打字

    duck typing is a style of dynamic
    typing in which an object's current
    set of methods and properties
    determines the valid semantics, rather
    than its inheritance from a particular
    class or implementation of a specific
    interface.

    重要的一点是,对于duck类型,开发人员更关心的是被消费的对象部分,而不是实际的底层类型。


    我看到很多重复旧习惯用法的答案:

    If it looks like a duck and quacks like a duck, it's a duck

    然后深入解释一下你可以用duck输入法做什么,或者是一个看起来进一步模糊了概念的例子。

    我找不到那么多帮助。

    这是我找到的关于鸭子打字的最简单的英语答案:

    duck类型意味着一个对象是由它能做什么来定义的,而不是由它是什么。

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


    鸭子打字不是类型暗示!

    基本上,为了使用"duck-typing",您将不会以特定类型为目标,而是使用一个公共接口,以更广泛的子类型为目标(不讨论继承,当我指的是子类型时,我指的是适合相同配置文件的"东西")。

    您可以想象一个存储信息的系统。为了写入/读取信息,您需要某种存储和信息。

    存储类型可以是:文件、数据库、会话等。

    接口将让您知道可用的选项(方法),而不管存储类型如何,这意味着此时什么都不实现!换句话说,接口不知道如何存储信息。

    每个存储系统都必须通过实现非常相同的方法来了解接口的存在。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    interface StorageInterface
    {
       public function write(string $key, array $value): bool;
       public function read(string $key): array;
    }


    class File implements StorageInterface
    {
        public function read(string $key): array {
            //reading from a file
        }

        public function write(string $key, array $value): bool {
             //writing in a file implementation
        }
    }


    class Session implements StorageInterface
    {
        public function read(string $key): array {
            //reading from a session
        }

        public function write(string $key, array $value): bool {
             //writing in a session implementation
        }
    }


    class Storage implements StorageInterface
    {
        private $_storage = null;

        function __construct(StorageInterface $storage) {
            $this->_storage = $storage;
        }

        public function read(string $key): array {
            return $this->_storage->read($key);
        }

        public function write(string $key, array $value): bool {
            return ($this->_storage->write($key, $value)) ? true : false;
        }
    }

    因此,现在,每当您需要写入/读取信息时:

    1
    2
    3
    4
    5
    6
    7
    $file = new Storage(new File());
    $file->write('filename', ['information'] );
    echo $file->read('filename');

    $session = new Storage(new Session());
    $session->write('filename', ['information'] );
    echo $session->read('filename');

    在本例中,您最终使用存储构造函数中的duck类型:

    1
    function __construct(StorageInterface $storage) ...

    希望能有所帮助;)


    观察语言本身可能会有所帮助;它经常帮助我(我不是英语母语者)。

    duck typing中:

    1)typing这个词并不意味着在键盘上打字(就像我心目中的持久图像一样),它意味着确定"那是什么类型的东西?"

    2)duck这个词表达了这种决定是如何完成的;这是一种"松散"的决定,比如:"如果它像鸭子一样走路……那就是一只鸭子。它是"松散的",因为它可能是一只鸭子,也可能不是,但它是否真的是一只鸭子并不重要;重要的是我能用它做我能用鸭子做的事,期待鸭子表现出的行为。我可以给它吃面包屑,它会朝我冲过来,或者冲着我冲锋,或者后退……但它不会像灰熊一样吞噬我。


    鸭子打字:

    If it talks and walks like a duck, then it is a duck

    这通常被称为诱拐(诱拐推理或也被称为追溯,我认为是一个更清晰的定义):

    • 从C(结论,我们看到的)和R(规则,我们知道的),我们接受/决定/假设P(前提,性质),换句话说,一个给定的事实

      …医学诊断的基础

      和鸭子在一起:C=散步,交谈,R=像鸭子,P=它是鸭子

    返回编程:

    • 对象o具有方法/属性mp1和接口/类型t需要/定义MP1

    • 对象o具有方法/属性mp2,接口/类型t需要/定义mp2

    所以,不仅仅是接受MP1…在任何对象上,只要它满足MP1的某些定义…,编译器/运行时也应该可以使用断言O的类型T。

    那么,上面的例子就是这样吗?鸭子打字根本就不是打字吗?或者我们应该称之为隐式输入?


    我知道我没有给出一般性的答案。在Ruby中,我们不声明变量或方法的类型——所有的东西都只是某种对象。所以规则是"类不是类型"

    在Ruby中,类从不(好的,几乎从不)是类型。相反,对象的类型更多地是由该对象可以做什么来定义的。在Ruby中,我们称之为鸭子打字。如果一个物体像鸭子一样走路,像鸭子一样说话,那么口译员很乐意把它当作鸭子对待。

    例如,您可能正在编写一个将歌曲信息添加到字符串中的例程。如果你来自C语言或Java背景,你可能会想写这个:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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的duck打字,你会写一些简单得多的东西:

    1
    2
    3
    4
    5
    6
    def append_song(result, song)
        result << song.title <<" (" << song.artist <<")"
    end

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

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


    用鸭子打字技术进行树遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def traverse(t):
        try:
            t.label()
        except AttributeError:
            print(t, end="")
        else:
            # Now we know that t.node is defined
            print('(', t.label(), end="")
            for child in t:
                traverse(child)
            print(')', end="")


    我觉得把动态输入法、静态输入法和duck输入法混在一起很困惑。duck类型是一个独立的概念,即使是像go这样的静态类型语言,也可以有一个实现duck类型的类型检查系统。如果类型系统将检查(声明的)对象的方法而不是类型,则可以称为duck类型语言。