鸭子类型与静态类型相比有哪些优势?

32

我正在研究和尝试更多使用Groovy,试图理解在Java中不能或不会实现的东西采用Groovy的利弊。对于动态编程,由于我一直深入静态和强类型语言,它仍然只是一个概念。

Groovy使我能够进行鸭子类型,但我真的看不到其价值所在。鸭子类型比静态类型更具生产力吗?我在我的代码练习中可以做哪些事情来帮助我理解其优点?

我关注Groovy时提出了这个问题,但我知道这不一定是一个Groovy问题,所以欢迎每个编码者的回答。


2
鸭子类型和静态类型是正交的概念。 - J D
10个回答

25
许多对于鸭子类型的评论并没有真正证实其主张。不必担心类型对于维护或扩展应用程序来说是不可持续的。在我的上一个合同中,我有很好的机会看到Grails的实际应用情况,这真的很有趣。每个人都对能够“创建应用程序”并开始工作感到高兴-可悲的是,这一切都在后端追赶你。

对我来说,Groovy似乎也是如此。当然,您可以编写非常简洁的代码,并且确实有一些漂亮的语法糖,使我们可以使用属性、集合等...但是不知道到底传递了什么成本只会变得越来越糟糕。在某个时候,您会扪心自问为什么项目已经变成了80%的测试和20%的工作。这里的教训是,“更小”并不能使代码“更易读”。对不起各位,这是简单的逻辑-您需要凭直觉知道的越多,理解该代码的过程就越复杂。这就是为什么GUI在这些年中已经不再过分地使用图标-看起来很漂亮,但正在发生什么并不总是显而易见。

在那个项目中,人们似乎很难“抓住”所学到的知识,但是当您的方法返回类型T的单个元素、T的数组、ErrorResult或null时,这变得相当明显。

然而,与Groovy一起工作给了我一个好处 - 惊人的可计费时间!


1
这是有史以来最实用的答案。 - abchau

12

鸭子类型会削弱大多数现代IDE的静态检查能力,这些能力可以在您输入时指出错误。一些人认为这是一个优点。我希望IDE/编译器尽快告诉我我犯了一个愚蠢的程序员错误。

我最近最喜欢用来反对鸭子类型的论据来自于一个Grails项目DTO:

class SimpleResults {
    def results
    def total
    def categories
}

results的内容类似于Map<String, List<ComplexType>>,只有通过在不同类中跟踪方法调用的轨迹才能发现它的创建位置。对于那些好奇心强烈的人来说,totalList<ComplexType>大小的总和,而categories则是Map的大小。

对于最初的开发者来说可能很清楚,但是可怜的维护人员(我)为此花费了很多精力。


1
虽然显式类型声明可以很有教育意义,但反射通常是一个不错的替代品。打开一个交互式会话并询问这些对象它们的类型。 - Nick Retallack
1
你的例子指出了文档不清晰的问题,而不是动态类型(这里也没有鸭子类型)。在静态类型下,我会知道(因为声明了)results 是一个 Map<String, List<ComplexType>>,而 totalcategories 都是 int。然而,这并没有告诉我如何使用这个类。 - Ben
1
这段代码的问题不在于打字,而在于那些没有描述性的引用名称。如果开发人员将结果命名为像mapOfFooToListOfBar这样富有表现力的东西,那么你就永远不会遇到这个问题。几乎任何东西都以结果或总数命名是一种代码异味,也是一个拙劣工匠的标志。这段代码还受原始执念的困扰。 - jeremyjjbrown
1
@jeremyjjbrown,当然这是代码异味,不专业和劣质工匠的标志 - 但它“有效”,仅仅因为Groovy中的“鸭子类型”。 是的,描述性名称会有所帮助。 记住,我们正在谈论那些在开发人员离开后必须接手此代码的可怜家伙,即使拥有长名称也不足以知道如何操作_results_的内容而无需进一步调查(请注意:时间,金钱和资源)。 - Ken Gentle
@ Ken,“如何操作结果内容”。所以我猜没有测试。我现在也遇到了和你完全相同的问题(今天),这个有着没有任何测试的代码是用普通Java写的,难以维护。静态类型并没有增加它的可维护性。但如果你的意思是Duck typing让开发者变得更加不负责任,那么我完全同意你的看法。 - jeremyjjbrown
@jeremyjjbrown 我们完全一致。 :) - Ken Gentle

10

在您使用鸭子类型之前,有点难以看出它的价值。一旦您习惯了它,就会意识到不必处理接口或担心某些东西的确切类型有多么轻松自在。


7

接下来,哪个更好:EMACS还是vi?这是一个长期争论的话题。

可以这样想:任何一个程序如果是正确的,在静态类型语言中也会是正确的。静态类型让编译器在编译时就有足够的信息来检测类型不匹配,而不是在运行时才发现。如果你正在进行增量式编程,这可能会很烦人,但我认为如果你对程序思考清楚了,这并不重要;另一方面,如果你正在构建一个非常大的程序,例如操作系统或电话交换机,并且有几十个、几百个或成千上万个人在其中工作,或者具有非常高的可靠性要求,那么编译器能够在不需要测试用例来执行恰当的代码路径的情况下,为您检测大量问题是很有用的。

动态类型并不是什么新鲜事物:例如,C语言实际上就是动态类型,因为我总是可以将foo*强制转换为bar*。这意味着作为C程序员,从此我就有责任永远不使用适用于bar*的代码,当地址实际上指向foo*时。但由于大型程序的问题,C语言发展出了像lint(1)这样的工具,用typedef加强了其类型系统,并最终在C++中开发出了一个强类型变体。(当然,C++又开发出了各种类型的转换、泛型/模板和运行时类型识别等方法来规避强类型。)

不过还有一件事——不要将“敏捷编程”与“动态语言”混淆。 敏捷编程是关于项目中人们如何合作的方式:项目是否能够适应变化的需求以满足客户的需求,同时为程序员提供人性化的环境?可以使用动态类型语言来完成,并且通常也会这样做,因为它们可能更具生产力(例如Ruby、Smalltalk),但它也可以在C甚至汇编语言中成功地完成。事实上,Rally Development甚至使用敏捷方法(尤其是SCRUM)来进行营销和文档编写。


15
任何正确的程序都将在静态类型下正确是不正确的。静态类型是对运行时行为的保守近似。这就是为什么一些带有强制类型转换的程序仍然可以是类型正确的(通过保留类型检查器无法证明的不变量)。 - Doug McClean
8
抱歉,C语言的弱类型系统并不等同于动态类型。它们之间差别很大,没有迟绑定的概念。像示例中那样进行指针转换将导致程序假定不同的底层结构,从而导致错误而不是功能。 - postfuturist
道格,我相信这是一个定理。假设相反:那么你有一个程序,它是正确的,即满足后置条件,但存在一条语句,其中没有静态类型是正确的。但这等价于在正确的程序中存在一条没有定义语义的语句,矛盾。 - Charlie Martin
抱歉,史蒂夫,这也不正确。考虑一个指向不同结构体的void*的情况。 - Charlie Martin
1
敏捷编程的两个方面是充足的单元测试和无情的重构。至少有一些轶事证据表明,动态类型语言在这两个领域都很有帮助。话虽如此,静态类型确实有助于IDE辅助重构。 - slim

7
如果你使用Haskell这样拥有令人难以置信的静态类型系统的语言,那么静态类型编程并没有任何问题。然而,如果你使用像Java和C++这样具有可怕的类型系统的语言,那么鸭子类型绝对是一种改进。
试想一下,在Java中尝试使用类似"map"这样简单的东西(不,我不是指数据结构)。即使泛型也得到了相当差的支持。

1
Haskell确实拥有出色的类型系统——如果更多的编程语言采用类似的机制,那将是非常棒的。 - mipadi
Java的Map类型并没有问题。问题在于Java缺乏一种流畅的语法来处理Map。这是Groovy所解决的问题,但这与Groovy的鸭子类型实际上并不相关。 - slim
@slim:我不是在谈论“map”类型,而是在谈论“map”高阶函数。我会在我的答案中添加一个链接。 - Nick Retallack
2
哦,好的。在Groovy中,这个方法叫做collect():List doubled = x.collect { it * 2 }。而在Scala中,它被称为map(),并且具有强类型。我不明白Java的类型系统会如何妨碍这一点。Java很快就会支持闭包,我相信Collections框架也会有collect/map方法。 - slim
有人真的能在工作中使用Haskell吗?我甚至不确定我和一些Java开发者一起工作,他们是否能够学习Haskell。 - jeremyjjbrown
我在过去两年中被承包来参与一个项目的工作,在使用PHP遇到了糟糕的问题之后,我转而开始使用Haskell进行开发。不过你说得没错,大多数程序员可能无法有效地学习如何使用Haskell。在我能够做出任何有用的东西之前,我读了六周的教程。 - Andrew Thaddeus Martin

6

通过使用TDD+100%代码覆盖率 + IDE工具来不断运行我的测试,我已经不再感觉需要静态类型。没有强类型,我的单元测试变得非常容易(只需使用Maps创建模拟对象)。特别是当您使用泛型时,您可以看到不同之处:

//Static typing 
Map<String,List<Class1<Class2>>> someMap = [:] as HashMap<String,List<Class1<Class2>>>

vs

//Dynamic typing
def someMap = [:]   

5

在我看来,鸭子类型的优势在于遵循一些约定时会更加突出,比如以一致的方式命名变量和方法。以Ken G的例子为例,我认为最好是这样写:

class SimpleResults {
    def mapOfListResults
    def total
    def categories
}

假设您定义了一个涉及名为'calculateRating(A,B)'的操作的合同,其中A和B遵循另一个合同。 伪代码如下:

Long calculateRating(A someObj, B, otherObj) {

   //some fake algorithm here:
   if(someObj.doStuff('foo') > otherObj.doStuff('bar')) return someObj.calcRating());
   else return otherObj.calcRating();

}

如果您想在Java中实现此功能,则A和B都必须实现某种接口,该接口应类似于以下内容:
public interface MyService {
    public int doStuff(String input);
}

此外,如果您想将计算评分的合同推广到更多场景(比如说您有另一个用于评分计算的算法),您还需要创建一个接口:
public long calculateRating(MyService A, MyServiceB);

使用鸭子类型,你可以放弃接口,只依赖于运行时,A和B都能正确响应doStuff()调用。不需要特定的契约定义。这对你有利,但也可能对你不利。缺点是你必须特别小心,以保证你的代码在其他人更改它时不会出错(即,其他人必须意识到方法名称和参数的隐式契约)。请注意,在Java中,语法不如它本应该的那么简洁(例如,与Scala相比)。一个反例是Lift框架,他们说框架的源代码行数与Rails类似,但测试代码的行数较少,因为他们不需要在测试中实现类型检查。

3
这里有一个场景,鸭子类型可以减少工作量。
这是一个非常简单的类:
class BookFinder {
    def searchEngine

    def findBookByTitle(String title) {
         return searchEngine.find( [ "Title" : title ] ) 
    }
}

现在进行单元测试:

void bookFinderTest() {
    // with Expando we can 'fake' any object at runtime.
    // alternatively you could write a MockSearchEngine class.
    def mockSearchEngine = new Expando()
    mockSearchEngine.find = {
        return new Book("Heart of Darkness","Joseph Conrad")
    }

    def bf = new BookFinder()
    bf.searchEngine = mockSearchEngine
    def book = bf.findBookByTitle("Heart of Darkness")
    assert(book.author == "Joseph Conrad"
}

由于缺乏静态类型检查,我们可以用Expando替换SearchEngine。如果进行静态类型检查,我们必须确保SearchEngine是一个接口或至少是一个抽象类,并创建完整的模拟实现。这很费力,或者你可以使用一个复杂的单一目的模拟框架。但鸭子类型是通用的,并且帮助了我们。
由于鸭子类型,我们的单元测试可以提供任何旧对象来代替依赖项,只要它实现了被调用的方法。
需要强调的是,您可以在静态类型语言中使用接口和类层次结构进行此操作。但是,使用鸭子类型可以更轻松地完成,减少思考和击键。
这是鸭子类型的优点。这并不意味着动态类型是在所有情况下使用的正确范例。在我的Groovy项目中,我喜欢在感到编译器关于类型的警告将帮助我时切换回Java。

2
-1:您正在谈论一种特定的“名义”静态类型系统,并错误地假设您的观察结果适用于所有静态类型系统。它们并不适用。 - J D

2
对我来说,如果你将动态类型语言视为一种从足够抽象的基类继承的静态类型,则它们并没有太大的不同。
问题出现在当你开始变得奇怪时,正如许多人所指出的那样。有人指出一个函数返回一个单一对象、一个集合或者一个空值。应该让函数返回一个特定的类型,而不是多个类型。对于单一与集合,应该使用多个函数。
归根结底,任何人都可能写出糟糕的代码。静态类型是一个很好的安全设备,但有时候头盔会妨碍你想要感受风吹拂头发的感觉。

1

鸭子类型并不比静态类型更高效,只是不同而已。使用静态类型时,您总是需要担心数据是否为正确的类型,在Java中,这通过将其强制转换为正确的类型来显示。使用鸭子类型时,只要具有正确的方法,类型就无关紧要,因此它真正消除了许多强制转换和类型之间的转换的麻烦。


1
有大量的轶事证据表明,鸭子类型比静态类型更具生产力。 - postfuturist
1
@postfuturist:仅适用于名义静态类型系统。 - J D
你说鸭子类型并不更具生产力,但最后又说“它只是消除了很多类型转换和强制转换的麻烦”。我认为这很明显更具生产力(假设你不必花费大量时间在一个编程实践不佳的程序员身后进行调试),而你的回答也很好地解释了为什么。 - dallin

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