静态/强类型和重构

6

在我看来,静态/强类型编程语言最有价值的地方在于它有助于重构:如果/当您更改任何API时,编译器会告诉您该更改已经破坏了什么。

我可以想象在运行时/弱类型语言中编写代码...但是我无法想象没有编译器的帮助进行重构,也无法想象在没有重构的情况下编写数万行代码。

这是真的吗?


你仍然会进行重构,只是使用单元测试来替代IDE/编译器。一旦你习惯了它,它真的不那么糟糕。 - Sasha Chedygov
5个回答

12

我认为你混淆了类型检查的时间和类型检查的方式。运行时类型并不一定是弱类型。

静态类型的主要优点正是你所说的:它们是详尽的。只要让编译器做它的工作,你就可以放心所有的调用站点都符合该类型。

静态类型的主要限制在于它们在表达约束方面有限。这因语言而异,大多数语言具有相对简单的类型系统(C、Java),而其他一些语言具有极其强大的类型系统(Haskell、Cayenne)。

由于这种限制,单独使用类型是不够的。例如,在 Java 中,类型的作用基本上被限制在检查类型名称是否匹配上。这意味着你想要检查的任何约束的含义都必须被编码成某种命名方案,因此在 Java 代码中普遍存在各种间接性和样板文件。C++ 在这方面稍微好一些,因为模板允许更多的表达能力,但远不能达到使用相关类型所能实现的效果。我不确定更强大的类型系统的缺点是什么,尽管显然肯定存在一些,否则行业中会有更多的人使用它们。

即使你使用静态类型,也有可能不足以检查你关心的所有问题,因此你还需要编写测试。静态类型是否能够节省更多的样板代码所需的工作量这个问题争论已久,并且我认为对于所有情况都没有简单的答案。

至于你的第二个问题:

如何在运行时类型的语言中安全地重构?

答案是测试。你的测试必须涵盖所有要紧的情况。工具可以帮助你评估测试的详尽程度。覆盖率检查工具可以让你知道代码行是否被测试覆盖。测试变异工具(jester、heckle)可以让你知道测试逻辑是否不完整。验收测试可以让你知道你所写的内容是否符合要求,最后回归和性能测试可以确保产品的每个新版本维持上一个版本的质量。

在进行适当的测试而非依赖复杂的类型引用时,其中一个好处是调试变得更加简单。运行测试时,您会在测试中获得具体的失败断言,清楚地表达它们正在做什么,而不是晦涩难懂的编译器错误语句(想想C++模板错误)。

无论使用何种工具:编写让自己有信心的代码都需要付出努力。这可能需要编写大量测试。如果错误的代价非常高,例如航空航天或医疗控制软件,则可能需要使用形式化数学方法来证明软件的行为,这使得此类开发非常昂贵。


请问您能否详细说明静态类型系统缺少了什么我实际上需要的东西?据我所知,我并没有感到任何可以归因于类型系统的缺失,自从几年前学习C++以来也是如此。我很可能会错过某些东西而没有注意到它,如果是这样,您指出来对我来说将非常有价值。 - John Saunders
@John:Lambda the Ultimate 上的某位可能会给你更好的答复,但依我有限的理解,C++ 类型中的一个弱点在于对聚合体的约束。通过 Alexandrescue 的 C++ 模板黑客技巧能够让你走得更远,但我认为仍然有相当一部分容器/数组相关的代码需要借助运行时检查。例如,如果我想约束向量的形状呢?或者将字符串的长度限制在特定范围内?这些都可以用依赖类型来表达。 - Jason Watkins
我没有使用过带有“依赖类型”的编程语言……我对“运行时类型检查”的理解更像 JavaScript! - ChrisW
感谢您向我介绍了“突变测试”的概念。 - ChrisW

6

我完全同意你的观点。动态类型语言所谓的灵活性实际上是使代码难以维护的原因。实际上,如果数据类型在不重要的方式下发生变化而没有实际改变代码,那么是否有一种程序可以继续工作呢?

同时,您可以检查传递的变量类型,并在其不是预期类型时失败。您仍然需要运行代码来排除这些情况,但至少会有一些提示。

我认为Google的内部工具实际上对他们的JavaScript进行了编译和类型检查。我希望我也有这些工具。


1
你基本上是在谈论Google Web Toolkit (http://code.google.com/webtoolkit/),对吗? - Matthew Flaschen
真的吗?有这样一种程序吗,即使在非常规的情况下更改数据类型而不实际更改代码,它仍然可以继续工作?是的,在任何支持结构类型(也称为鸭子类型)的语言中,您经常会看到这种情况。这并不一定是静态或动态的。例如,C++通用函数只关心它们使用的类型的特定方面,因此许多参数类型的更改是可能的,而不需要更改函数。 - Jason Watkins

2
首先,我是一位本地的Perl程序员,因此一方面我从未使用过静态类型的网络进行编程。另一方面,我也从未使用过它们,因此无法谈论它们的好处。我能说到的是重构时所遇到的问题。
我认为缺乏静态类型并不是关于重构的问题。我发现问题在于缺乏重构的“浏览器”。动态语言存在这样一个问题,即在实际运行之前,你无法真正知道代码会做什么。Perl比大多数语言更加如此。Perl还有一个很复杂,几乎无法解析的语法。结果是:没有重构工具(尽管 他们正在快速开发中)。最终结果是我必须手动重构。这就引入了bug。
我有测试来捕捉它们......通常。但我经常发现自己面对着一堆未经测试和几乎无法测试的代码,存在先有鸡还是先有蛋的问题,必须重构代码以进行测试,但又必须测试才能重构。很恶心。此时,我不得不编写一些非常愚蠢、高级别的“程序是否输出与之前相同”的测试,以确保我没有破坏任何东西。
在Java、C++或C#中设想的静态类型实际上只解决了一小类编程问题。它们保证接口传递了正确标签的数据位。但仅仅因为你得到了一个集合并不意味着该集合包含你认为的数据。因为你得到了一个整数并不意味着你得到了正确的整数。你的方法需要一个用户对象,但这个用户是否已登录?
经典例子:Java平方根函数的签名public static double sqrt(double a)。平方根不能作用于负数。在签名中哪里说了这一点?它没有。更糟糕的是,它甚至没有说明这个函数到底做什么。签名只说了它需要哪些类型的参数和返回值,却没有提供关于函数内部执行过程的任何信息。而有趣的代码恰恰是隐藏在这其中的。有些人尝试通过使用设计契约来完整地描述API,可以广泛地描述为嵌入运行时测试函数的输入、输出和副作用(或其缺乏)……但那是另外一个话题。
API远不止是函数签名(否则,你就不需要Javadocs中那么多的描述性文字了),重构也不仅仅是改变API而已。
静态类型、静态编译、非动态语言最大的重构优势是能够编写重构工具来完成相当复杂的重构,因为它知道所有调用你方法的位置。我非常羡慕IntelliJ IDEA

1
“API不仅仅是函数签名而已”,如果我在不改变方法签名的情况下改变了方法的含义,那么我只需要更改方法的名称:编译器将帮助/要求我查找程序中其余使用旧名称并期望旧含义的所有位置。 - ChrisW

1
我认为重构超出了编译器可以检查的范围,即使在静态类型语言中也是如此。重构只是改变程序的内部结构而不影响外部行为。即使在动态语言中,仍然有一些你可以预期发生并进行测试的事情,只是你会失去一点编译器的帮助。

1

在C# 3.0中使用var的好处之一是,您通常可以更改类型而不会破坏任何代码。类型仍然需要看起来相同 - 必须存在具有相同名称的属性,必须仍然存在具有相同或类似签名的方法。但是,即使不使用类似于ReSharper的东西,您也可以真正更改为非常不同的类型。


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