在我看来,静态/强类型编程语言最有价值的地方在于它有助于重构:如果/当您更改任何API时,编译器会告诉您该更改已经破坏了什么。
我可以想象在运行时/弱类型语言中编写代码...但是我无法想象没有编译器的帮助进行重构,也无法想象在没有重构的情况下编写数万行代码。
这是真的吗?
在我看来,静态/强类型编程语言最有价值的地方在于它有助于重构:如果/当您更改任何API时,编译器会告诉您该更改已经破坏了什么。
我可以想象在运行时/弱类型语言中编写代码...但是我无法想象没有编译器的帮助进行重构,也无法想象在没有重构的情况下编写数万行代码。
这是真的吗?
我认为你混淆了类型检查的时间和类型检查的方式。运行时类型并不一定是弱类型。
静态类型的主要优点正是你所说的:它们是详尽的。只要让编译器做它的工作,你就可以放心所有的调用站点都符合该类型。
静态类型的主要限制在于它们在表达约束方面有限。这因语言而异,大多数语言具有相对简单的类型系统(C、Java),而其他一些语言具有极其强大的类型系统(Haskell、Cayenne)。
由于这种限制,单独使用类型是不够的。例如,在 Java 中,类型的作用基本上被限制在检查类型名称是否匹配上。这意味着你想要检查的任何约束的含义都必须被编码成某种命名方案,因此在 Java 代码中普遍存在各种间接性和样板文件。C++ 在这方面稍微好一些,因为模板允许更多的表达能力,但远不能达到使用相关类型所能实现的效果。我不确定更强大的类型系统的缺点是什么,尽管显然肯定存在一些,否则行业中会有更多的人使用它们。
即使你使用静态类型,也有可能不足以检查你关心的所有问题,因此你还需要编写测试。静态类型是否能够节省更多的样板代码所需的工作量这个问题争论已久,并且我认为对于所有情况都没有简单的答案。
至于你的第二个问题:
如何在运行时类型的语言中安全地重构?
答案是测试。你的测试必须涵盖所有要紧的情况。工具可以帮助你评估测试的详尽程度。覆盖率检查工具可以让你知道代码行是否被测试覆盖。测试变异工具(jester、heckle)可以让你知道测试逻辑是否不完整。验收测试可以让你知道你所写的内容是否符合要求,最后回归和性能测试可以确保产品的每个新版本维持上一个版本的质量。
在进行适当的测试而非依赖复杂的类型引用时,其中一个好处是调试变得更加简单。运行测试时,您会在测试中获得具体的失败断言,清楚地表达它们正在做什么,而不是晦涩难懂的编译器错误语句(想想C++模板错误)。
无论使用何种工具:编写让自己有信心的代码都需要付出努力。这可能需要编写大量测试。如果错误的代价非常高,例如航空航天或医疗控制软件,则可能需要使用形式化数学方法来证明软件的行为,这使得此类开发非常昂贵。
我完全同意你的观点。动态类型语言所谓的灵活性实际上是使代码难以维护的原因。实际上,如果数据类型在不重要的方式下发生变化而没有实际改变代码,那么是否有一种程序可以继续工作呢?
同时,您可以检查传递的变量类型,并在其不是预期类型时失败。您仍然需要运行代码来排除这些情况,但至少会有一些提示。
我认为Google的内部工具实际上对他们的JavaScript进行了编译和类型检查。我希望我也有这些工具。
public static double sqrt(double a)
。平方根不能作用于负数。在签名中哪里说了这一点?它没有。更糟糕的是,它甚至没有说明这个函数到底做什么。签名只说了它需要哪些类型的参数和返回值,却没有提供关于函数内部执行过程的任何信息。而有趣的代码恰恰是隐藏在这其中的。有些人尝试通过使用设计契约来完整地描述API,可以广泛地描述为嵌入运行时测试函数的输入、输出和副作用(或其缺乏)……但那是另外一个话题。在C# 3.0中使用var的好处之一是,您通常可以更改类型而不会破坏任何代码。类型仍然需要看起来相同 - 必须存在具有相同名称的属性,必须仍然存在具有相同或类似签名的方法。但是,即使不使用类似于ReSharper的东西,您也可以真正更改为非常不同的类型。