45

我经常看到“鸭子打字”这个短语在流传,甚至遇到一两个代码示例。我太懒了,没时间做自己的研究,有人可以简单地告诉我:

  • 'duck type' 和 old-skool 'variant type' 之间的区别,以及
  • 提供一个我可能更喜欢鸭子打字而不是变体打字的例子,以及
  • 提供一个我必须使用鸭子打字来完成的事情的例子?

鸭子打字插图由 The Register 提供

我并不是想通过怀疑这种“新”结构的力量来显得家常便饭,我也不是通过拒绝进行研究来回避这个问题,但我对我所看到的所有蜂拥而至的炒作感到震惊最近呢。对我来说,它看起来像没有打字(又名动态打字),所以我没有立即看到优势。

附录:感谢到目前为止的示例。在我看来,使用“O->can(Blah)”之类的东西相当于进行反射查找(这可能并不便宜),和/或与编译器可能会说的(O 是 IBlah)大致相同能够为您检查,但后者的优点是可以将我的 IBlah 接口与您的 IBlah 接口区分开来,而其他两个则没有。当然,每个方法都有很多微小的接口会变得混乱,但同样可以检查很多单独的方法......

...所以我还是不明白。这是一个很棒的节省时间的方法,还是一个全新的麻袋里的旧东西?需要鸭子打字的例子在哪里?

4

10 回答 10

32

在这里的一些答案中,我看到了一些不正确的术语使用,这导致人们提供错误的答案。

所以,在我给出答案之前,我将提供一些定义:

  1. 强类型

    如果语言强制执行程序的类型安全,则它是强类型的。这意味着它保证了两件事:一种叫做进步的东西,另一种叫做保存的东西。进步基本上意味着所有“有效类型”的程序实际上都可以由计算机运行,它们可能会崩溃,或者抛出异常,或者运行无限循环,但它们实际上可以运行。保存意味着如果一个程序是“有效类型的”,那么它将始终是“有效类型的”,并且没有变量(或内存位置)包含不符合其分配类型的值。

    大多数语言都有“进度”属性。然而,有许多不满足“保存”属性。一个很好的例子是 C++(和 C 也是)。例如,在 C++ 中可以强制任何内存地址表现得就像它是任何类型一样。这基本上允许程序员在他们想要的任何时候违反类型系统。这是一个简单的例子:

    struct foo
    {
        int x;
        iny y;
        int z;
    }
    
    char * x = new char[100];
    foo * pFoo = (foo *)x;
    foo aRealFoo;
    *pFoo = aRealFoo;
    

    此代码允许某人获取一个字符数组并向其写入一个“foo”实例。如果 C++ 是强类型的,这是不可能的。如果您尝试将字符数组强制转换为“foo”实例,则类型安全语言(如 C#、Java、VB、lisp、ruby、python 和许多其他语言)会引发异常。

  2. 弱类型

    如果某些东西不是强类型,那么它就是弱类型。

  3. 静态类型

    如果在编译时验证了语言的类型系统,则该语言是静态类型的。静态类型语言既可以像 C 一样是“弱类型”,也可以像 C# 那样是强类型。

  4. 动态类型

    动态类型语言是一种在运行时验证类型的语言。许多语言在某种程度上混合了静态类型和动态类型。例如,C# 将在运行时动态验证许多强制转换,因为在编译时无法检查它们。其他示例是 Java、VB 和 Objective-C 等语言。

    还有一些语言是“完全”或“大部分”动态类型的,如“lisp”、“ruby”和“small talk”

  5. 鸭打字

    Duck 类型与静态、动态、弱或强类型完全正交。无论对象的底层类型标识如何,编写代码都将与对象一起工作是一种实践。例如,下面的 VB.NET 代码:

    function Foo(x as object) as object
        return x.Quack()
    end function
    

    无论传递给“Foo”的对象是什么类型,只要定义了一个名为“Quack”的方法,它都可以工作。也就是说,如果物体看起来像鸭子,走路像鸭子,说话像鸭子,那么它就是鸭子。鸭打字有多种形式。可以有静态鸭子类型、动态鸭子类型、强鸭子类型和弱鸭子类型。C++ 模板函数是“弱静态鸭子类型”的一个很好的例子。“JaredPar's”帖子中的示例显示了“强静态鸭子类型”的示例。VB 中的后期绑定(或 Ruby 或 Python 中的代码)启用“强动态鸭子类型”。

  6. 变体

    变体是一种动态类型的数据结构,可以保存一系列预定义的数据类型,包括字符串、整数类型、日期和 com 对象。然后,它定义了一系列用于分配、转换和操作存储在变体中的数据的操作。变体是否为强类型取决于使用它的语言。例如,VB 6 程序中的变体是强类型的。VB 运行时确保用 VB 代码编写的操作符合变体的类型规则。通过 VB 中的变体类型将字符串添加到 IUnknown 将导致运行时错误。然而,在 C++ 中,变体是弱类型的,因为所有 C++ 类型都是弱类型的。

好的....既然我已经了解了定义,我现在可以回答您的问题:

VB 6 中的一个变体允许进行鸭子类型的一种形式。有比变体更好的方法来进行鸭子类型(Jared Par 的示例是最好的方法之一),但是您可以使用变体进行鸭子类型。也就是说,您可以编写一段代码来对对象进行操作,而不管其底层类型标识如何。

但是,使用变体执行此操作并不能真正提供很多验证。像 JaredPar 描述的那样,静态类型的鸭子类型机制提供了鸭子类型的好处,以及来自编译器的一些额外验证。这真的很有帮助。

于 2008-11-14T08:47:15.720 回答
19

简单的答案是变体是弱类型,而鸭子类型是强类型。

鸭子打字可以很好地概括为“如果它像鸭子一样走路,看起来像鸭子,像鸭子一样行动,那么它就是一只鸭子。” 计算机科学术语认为鸭子是以下接口。

interface IDuck {
  void Quack();
}

现在让我们来看看达菲

class Daffy {
  void Quack() {
    Console.WriteLine("Thatsssss dispicable!!!!");
  }
}

在这种情况下,Daffy 实际上不是 IDuck。然而它的行为就像一只鸭子。既然很明显 Daffy 实际上是一只鸭子,为什么还要让 Daffy 实现 IDuck。

这就是 Duck 类型的用武之地。它允许在具有 IDuck 和 IDuck 引用的所有行为的任何类型之间进行类型安全转换。

IDuck d = new Daffy();
d.Quack();

现在可以在完全类型安全的情况下在“d”上调用 Quack 方法。此分配或方法调用中不可能出现运行时类型错误。

于 2008-11-14T07:09:13.670 回答
5

鸭子类型只是动态类型或后期绑定的另一个术语。使用在运行时期间可能实际定义或未定义的任何成员访问(例如,obj.Anything)解析/编译的变体对象是鸭子类型。

于 2008-11-14T03:47:03.483 回答
4

可能没有什么需要鸭式打字,但在某些情况下它可能很方便。假设您有一个方法从某个 3rd 方库中获取并使用密封类 Duck 的对象。并且您想让该方法可测试。Duck 有一个非常大的 API(有点像 ServletRequest),你只需要关心其中的一小部分。你如何测试它?

一种方法是使该方法采用一些嘎嘎作响的东西。然后,您可以简单地创建一个 quacking 模拟对象。

于 2008-11-16T05:00:44.507 回答
3

尝试阅读关于鸭子类型的维基百科文章的第一段。
鸭子在维基百科上打字

我可以有一个定义方法 Run() 的接口 (IRunnable)。
如果我有另一个类具有这样的方法:
public void RunSomeRunnable(IRunnable rn) { ... }

在鸭子类型友好的语言中,我可以将任何具有 Run() 方法的类传递给 RunSomeRunnable() 方法。
在静态类型语言中,传递给 RunSomeRunnable 的类需要显式实现 IRunnable 接口。

“如果它像鸭子一样运行()”

变体至少更像 .NET 中的对象。

于 2008-11-14T03:57:17.317 回答
2

@肯特弗雷德里克

您的示例当然可以通过使用显式接口在没有鸭子类型的情况下完成......更丑的是,但这并非不可能。

就个人而言,我发现在接口中定义明确的合同对于执行质量代码比依赖鸭子打字要好得多……但这只是我的意见,并持保留态度。

public interface ICreature { }
public interface IFly { fly();}
public interface IWalk { walk(); }
public interface IQuack { quack(); }
// ETC

// Animal Class
public class Duck : ICreature, IWalk, IFly, IQuack
{
    fly() {};
    walk() {};
    quack() {};
}

public class Rhino: ICreature, IWalk
{
    walk();
}

// In the method
List<ICreature> creatures = new List<ICreature>();
creatures.Add(new Duck());
creatures.Add(new Rhino());   

foreach (ICreature creature in creatures)
{
    if (creature is IFly)        
         (creature as IFly).fly();        
    if (creature is IWalk) 
         (creature as IWalk).walk();         
}
// Etc
于 2008-11-14T04:30:29.903 回答
2

关于您需要使用鸭子打字来完成的示例的请求,我认为不存在这样的事情。我认为它就像我在考虑是否使用递归或是否使用迭代一样。有时一个比另一个工作得更好。

以我的经验,鸭式打字使代码更具可读性和更容易掌握(对于程序员和读者而言)。但我发现更传统的静态类型消除了很多不必要的类型错误。根本没有办法客观地说一个比另一个更好,甚至说什么情况下一个比另一个更有效。

我说如果你习惯使用静态类型,那么就使用它。但是你至少应该尝试鸭式打字(如果可能的话,在一个重要的项目中使用它)。

更直接地回答你:

...所以我还是不明白。这是一个很棒的节省时间的方法,还是一个全新的麻袋里的旧东西?

两者都是。你仍然在解决同样的问题。你只是用不同的方式来做。有时这就是你需要做的所有事情来节省时间(即使没有其他理由强迫自己考虑以不同的方式做某事)。

它是拯救全人类免于灭绝的灵丹妙药吗?不,任何告诉你其他情况的人都是狂热者。

于 2008-11-16T04:04:28.613 回答
1

我认为鸭式打字的核心是它是如何使用的。一个人使用实体的方法检测和自省来知道如何处理它,而不是提前声明它将是什么(你知道如何处理它)。

这在 OO 语言中可能更实用,其中原语不是原语,而是对象。

我认为总结它的最好方法,在变体类型中,实体是/可以是任何东西,它是什么是不确定的,而不是实体看起来像任何东西,但你可以通过询问它来弄清楚它是什么.

这是我不相信没有鸭式打字的事情。

sub dance { 
     my $creature = shift;
     if( $creature->can("walk") ){ 
         $creature->walk("left",1);
         $creature->walk("right",1); 
         $creature->walk("forward",1);
         $creature->walk("back",1);
     }
     if( $creature->can("fly") ){ 
          $creature->fly("up"); 
          $creature->fly("right",1); 
          $creature->fly("forward",1); 
          $creature->fly("left", 1 ); 
          $creature->fly("back", 1 ); 
          $creature->fly("down");
     } else if ( $creature->can("walk") ) { 
         $creature->walk("left",1);
         $creature->walk("right",1); 
         $creature->walk("forward",1);
         $creature->walk("back",1);
     } else if ( $creature->can("splash") ) { 
         $creature->splash( "up" ) for ( 0 .. 4 ); 
     }
     if( $creature->can("quack") ) { 
         $creature->quack();
     }
 }

 my @x = ();  
 push @x, new Rhinoceros ; 
 push @x, new Flamingo; 
 push @x, new Hyena; 
 push @x, new Dolphin; 
 push @x, new Duck;

 for my $creature (@x){

    new Thread(sub{ 
       dance( $creature ); 
    }); 
 }

任何其他方式都需要您对函数进行类型限制,这将删除不同的物种,需要您为不同的物种创建不同的函数,使得代码维护起来非常麻烦。

这真的很糟糕,只是试图表演好的编舞。

于 2008-11-14T03:48:32.750 回答
1

一个变体(至少我在 VB6 中使用过它们)拥有一个单一的、定义明确的、通常是静态类型的变量。例如,它可能包含一个整数、浮点数或字符串,但变体整数用作整数,变体浮点数用作浮点数,变体字符串用作字符串。

Duck 类型改为使用动态类型。在鸭子类型下,如果变量恰好支持 int、float 或 string 在特定上下文中支持的特定方法,则该变量可能可用作 int、float 或 string。

变体与鸭子类型的示例:

对于 Web 应用程序,假设我希望我的用户信息来自 LDAP 而不是来自数据库,但我仍然希望我的用户信息可供基于数据库和 ORM 的 Web 框架的其余部分使用。

使用变体:没有运气。我可以创建一个可以包含 UserFromDbRecord 对象或 UserFromLdap 对象的变体,但是期望来自 FromDbRecord 层次结构的对象的例程将无法使用 UserFromLdap 对象。

使用鸭子类型:我可以使用我的 UserFromLdap 类并添加几个方法,使其表现得像 UserFromDbRecord 类。我不需要复制整个 FromDbRecord 接口,对于我需要使用的例程就足够了。如果我做对了,这是一个非常强大和灵活的技术。如果我做错了,它会产生非常混乱和脆弱的代码(如果 DB 库或 LDAP 库发生更改,可能会损坏)。

于 2008-11-14T03:52:32.927 回答
0

你可以用鸭子打字做的一切,你也可以用接口做。鸭式打字既快速又舒适,但有些人认为它可能会导致错误(如果两个不同的方法/属性名称相同)。接口是安全且明确的,但人们可能会说“为什么要陈述显而易见的事情?”。休息是火焰。每个人都选择适合他的东西,没有人是“正确的”。

于 2010-02-12T11:26:05.070 回答