我正在对 Groovy 进行更多的研究和实验,并且我正在努力思考在 Groovy 中实现我在 Java 中不能/不做的事情的优缺点。动态编程对我来说仍然只是一个概念,因为我已经深入了解静态和强类型语言。

Groovy 使我能够 鸭型, ,但我实在看不出它的价值。鸭子类型如何比静态类型更高效?在我的代码实践中我可以做哪些事情来帮助我掌握它的好处?

我问这个问题时考虑的是 Groovy,但我知道这不一定是 Groovy 问题,所以我欢迎来自每个代码营的答案。

有帮助吗?

解决方案

接下来,哪个更好:EMACS 还是 vi?这是正在进行的宗教战争之一。

可以这样想:任何程序 正确的, ,如果语言是静态类型的,则将是正确的。静态类型的作用是让编译器有足够的信息来在编译时而不是运行时检测类型不匹配。如果您进行增量编程,这可能会很烦人,尽管(我认为)如果您清楚地思考您的程序,这并不重要;另一方面,如果您正在构建一个非常大的程序,例如操作系统或电话交换机,有数十人、数百人或数千人在处理它,或者具有非常高的可靠性要求,那么让编译器能够为您检测一大类问题,而无需测试用例来执行正确的代码路径。

这并不是说动态类型是一个新的、不同的东西:例如,C 是有效的动态类型,因为我总是可以强制转换 foo* 到一个 bar*. 。这只是意味着作为一名 C 程序员,我有责任永远不要使用适合于 bar* 当地址确实指向一个 foo*. 。但由于大型程序的问题,C 开发了像 lint(1) 这样的工具,并通过以下方式增强了其类型系统: typedef 并最终在 C++ 中开发了强类型变体。(当然,C++ 反过来又开发了围绕强类型的方法,包括各种类型的强制转换和泛型/模板以及 RTTI。

但另一件事是——不要将“敏捷编程”与“动态语言”混淆。 敏捷编程 是关于人们在项目中合作的方式:项目能否适应不断变化的需求,满足客户的需求,同时为程序员保持人性化的环境?它可以用动态类型语言来完成,而且通常是这样,因为它们可以更高效(例如,Ruby、Smalltalk),但它可以用 C 甚至汇编语言完成,并且已经成功完成。实际上, 拉力发展 甚至使用敏捷方法(特别是 SCRUM)来进行营销和文档编制。

其他提示

许多关于鸭子打字的评论并没有真正证实这些说法。不“不必担心”类型对于维护或使应用程序可扩展来说是不可持续的。在我的上一份合同中,我确实有一个很好的机会看到 Grails 的运行,而且看起来真的很有趣。每个人都对能够“创建应用程序”并开始工作所获得的收益感到高兴 - 遗憾的是,这一切都在后端赶上了你的脚步。

Groovy 对我来说似乎也是如此。当然,你可以编写非常简洁的代码,并且在我们如何使用属性、集合等方面肯定有一些很好的糖分......但不知道来回传递的到底是什么,所付出的代价只会变得越来越严重。有时您会挠头,想知道为什么该项目变成了 80% 的测试和 20% 的工作。这里的教训是“更小”并不意味着“更易读”的代码。抱歉,各位,它的逻辑很简单 - 您必须直观地了解越多,那么理解代码的过程就会变得越复杂。这就是为什么多年来 GUI 不再变得过于标志性——看起来确实很漂亮,但发生的事情并不总是显而易见的。

该项目的人们似乎很难“确定”所学到的经验教训,但是当您有方法返回类型 T 的单个元素、T 的数组、ErrorResult 或 null 时......这变得相当明显。

然而,使用 Groovy 为我做了一件事 - 很棒的计费时间!

如果您使用 Haskell,静态类型没有任何问题,它具有令人难以置信的静态类型系统。然而,如果您使用像 Java 和 C++ 这样的语言,它们的类型系统非常严重,那么鸭子类型绝对是一种改进。

想象一下尝试使用像“这样简单的东西”地图“在 Java 中(不,我不是说 数据结构)。即使是泛型也得不到很好的支持。

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

我最近最喜欢的论点 反对 鸭子类型来自 Grails 项目 DTO:

class SimpleResults {
    def results
    def total
    def categories
}

在哪里 results 原来是这样的 Map<String, List<ComplexType>>, ,只能通过跟踪不同类中的方法调用来发现它,直到找到它的创建位置。对于极度好奇的人来说, total 是大小的总和 List<ComplexType>categories 是的大小 Map

最初的开发人员可能已经很清楚了,但是糟糕的维护人员(ME)在追踪这个问题时损失了很多头发。

在您使用一段时间之前,要了解鸭子类型的价值有点困难。一旦习惯了它,您就会意识到不必处理接口或不必担心某些东西到底是什么类型,这会给您带来多大的负担。

恕我直言,当您遵守某些约定(例如以一致的方式命名变量和方法)时,鸭子类型的优势就会被放大。举个例子 肯·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 中尤其严重,因为 Java 中的语法并不像其应有的那样简洁(与 斯卡拉 例如)。这方面的一个反例是 升降机框架, ,他们说框架的 SLOC 计数类似于 导轨, ,但测试代码的行数较少,因为它们不需要在测试中实现类型检查。

这是一种鸭子类型可以节省工作量的场景。

这是一个非常简单的类

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 是一个接口,或者至少是一个抽象类,并创建它的完整模拟实现。这是劳动密集型的,或者您可以使用复杂的单一用途模拟框架。但鸭子类型是通用的,并且对我们有帮助。

由于鸭子类型,我们的单元测试可以提供任何旧对象来代替依赖项,只要它实现被调用的方法即可。

强调一下 - 您可以使用静态类型语言来完成此操作,并仔细使用接口和类层次结构。但通过鸭子打字,你可以用更少的思考和更少的击键来完成它。

这是鸭子类型的优点。这并不意味着动态类型是在所有情况下使用的正确范例。在我的 Groovy 项目中,当我觉得有关类型的编译器警告会对我有所帮助时,我喜欢切换回 Java。

和, TDD + 100% 代码覆盖率 + IDE 工具不断运行我的测试,我不再需要静态类型。没有强类型,我的单元测试变得如此简单(只需使用映射来创建模拟对象)。特别是,当您使用泛型时,您可以看到区别:

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

//Dynamic typing
def someMap = [:]   

这并不是说鸭子类型比静态类型更有效率,而只是不同而已。使用静态类型,您始终需要担心数据的类型是否正确,而在 Java 中,它是通过转换为正确的类型来显示的。使用鸭子类型,只要它有正确的方法,类型并不重要,因此它实际上消除了类型之间的强制转换和转换的许多麻烦。

@克里斯·邦奇

这并不是说静态类型比鸭子类型更高效,而只是不同。使用鸭子类型,您总是需要担心您的数据是否具有正确的方法,而在 Javascript 或 Ruby 中,它会通过大量方法测试显示出来。对于静态类型,只要它是正确的接口就没有关系,因此它实际上消除了类型之间的测试和转换的许多麻烦。

抱歉,但我不得不这样做......

我的想法:

动态类型或鸭子类型语言都是玩具。你无法获得 Intellisense,并且会损失编译时间(或编辑时间 - 当使用像 VS 这样的真实 IDE 时,而不是其他人认为是 IDE 的垃圾)代码验证。

远离所有非静态类型的语言,其他一切都只是简单的受虐狂。

对我来说,如果您将动态类型语言视为静态类型的一种形式,其中所有内容都继承自足够抽象的基类,那么它们并没有太大的不同。

正如许多人指出的那样,当你开始对此感到奇怪时,问题就会出现。有人指出一个函数返回单个对象、集合或 null。让函数返回特定类型,而不是多个类型。单个函数与集合函数使用多个函数。

归根结底,任何人都可能写出糟糕的代码。静态打字是一种很好的安全装置,但有时当你想感受风吹过头发时,头盔会妨碍你。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top