在我看来,静态/强类型编程语言最宝贵的地方在于它有助于重构:如果/当您更改任何 API 时,编译器会告诉您该更改破坏了什么。
我可以想象用运行时/弱类型语言编写代码……但我无法想象没有编译器的帮助就进行重构,也无法想象在没有重构的情况下编写数万行代码。
这是真的?
在我看来,静态/强类型编程语言最宝贵的地方在于它有助于重构:如果/当您更改任何 API 时,编译器会告诉您该更改破坏了什么。
我可以想象用运行时/弱类型语言编写代码……但我无法想象没有编译器的帮助就进行重构,也无法想象在没有重构的情况下编写数万行代码。
这是真的?
我认为您将检查类型与检查方式混为一谈。运行时类型不一定很弱。
静态类型的主要优点正是您所说的:它们是详尽的。只需让编译器执行此操作,您就可以确信所有调用站点都符合该类型。
静态类型的主要限制是它们受限于它们可以表达的约束。这因语言而异,大多数语言具有相对简单的类型系统(c、java),而其他语言具有极其强大的类型系统(haskell、cayenne)。
由于这种限制,类型本身是不够的。例如,在 java 中,类型或多或少地仅限于检查类型名称是否匹配。这意味着您要检查的任何约束的含义都必须编码到某种命名方案中,因此 java 代码常见的过多间接和样板。C++ 稍微好一点,因为模板允许更多的表现力,但不能接近你可以用依赖类型做的事情。我不确定更强大的类型系统的缺点是什么,但显然肯定会有一些或更多的人在工业中使用它们。
即使您使用的是静态类型,也可能不足以检查您关心的所有内容,因此您也需要编写测试。静态类型是否比样板代码节省了更多的精力,这是一个长期激烈的争论,我认为并不是所有情况都有一个简单的答案。
至于你的第二个问题:
我们如何在运行时类型语言中安全地重构?
答案是测试。您的测试必须涵盖所有重要的情况。工具可以帮助您衡量测试的详尽程度。覆盖检查工具让您知道测试是否覆盖了代码行。测试变异工具(jester、heckle)可以让您知道您的测试是否在逻辑上不完整。验收测试让您知道您编写的内容符合要求,最后回归和性能测试确保产品的每个新版本都保持上一个版本的质量。
进行适当的测试与依赖复杂的类型间接相关的好处之一是调试变得更加简单。运行测试时,您会在测试中获得特定的失败断言,这些断言清楚地表达了它们在做什么,而不是钝的编译器错误语句(想想 c++ 模板错误)。
无论您使用什么工具:编写您有信心的代码都需要付出努力。它很可能需要编写大量测试。如果错误的惩罚非常高,例如航空航天或医疗控制软件,您可能需要使用正式的数学方法来证明您的软件的行为,这使得此类开发非常昂贵。
我完全同意你的观点。动态类型语言应该擅长的灵活性实际上是使代码难以维护的原因。真的,如果数据类型以非平凡的方式更改而没有实际更改代码,是否有程序继续工作?
同时,您可以检查传递的变量的类型,如果它不是预期的类型,则以某种方式失败。你仍然需要运行你的代码来根除这些情况,但至少会告诉你一些事情。
我认为 Google 的内部工具实际上会对其 Javascript 进行编译和类型检查。我希望我有这些工具。
首先,我是一名本地 Perl 程序员,所以一方面我从未使用静态类型网络进行过编程。OTOH 我从来没有和他们一起编程,所以我不能谈论他们的好处。我能说的是重构是什么样的。
我不认为缺少静态类型是重构的问题。我发现一个问题是缺少重构浏览器。动态语言的问题是,在实际运行代码之前,您并不真正知道代码真正要做什么。Perl 比大多数都拥有这一点。Perl 的另一个问题是语法非常复杂,几乎无法解析。结果:没有重构工具(尽管他们在这方面工作得非常迅速)。最终结果是我必须手动重构。这就是引入错误的原因。
我有测试来捕捉它们……通常。我确实发现自己经常面对一堆未经测试和几乎无法测试的代码,面临着鸡/蛋问题,即必须重构代码才能测试它,但必须测试它才能重构它。伊克。在这一点上,我必须编写一些非常愚蠢的高级“程序是否输出与以前相同的东西”之类的测试,以确保我没有破坏某些东西。
正如 Java 或 C++ 或 C# 所设想的那样,静态类型实际上只能解决一小部分编程问题。它们保证您的接口传递带有正确标签的数据位。但仅仅因为你得到一个 Collection 并不意味着 Collection 包含你认为它包含的数据。因为你得到一个整数并不意味着你得到了正确的整数。您的方法需要一个用户对象,但该用户是否已登录?
经典示例:public static double sqrt(double a)
是Java 平方根函数的签名。平方根不适用于负数。签名的什么地方这么写的?它没有。更糟糕的是,它在哪里说明该功能甚至是做什么的?签名只说明它采用什么类型以及返回什么。它没有说明中间发生的事情,这就是有趣的代码所在的地方。有些人试图通过使用按合同设计来捕获完整的 API ,这可以广泛地描述为嵌入对函数的输入、输出和副作用(或缺乏)的运行时测试......但这是另一个展示。
API 不仅仅是函数签名(如果不是,您将不需要 Javadocs 中的所有描述性散文),重构甚至不仅仅是更改 API。
静态类型、静态编译、非动态语言给您的最大重构优势是能够编写重构工具来为您进行相当复杂的重构,因为它知道对您的方法的所有调用在哪里。我很羡慕IntelliJ IDEA。
我想说重构超出了编译器可以检查的范围,即使在静态类型的语言中也是如此。重构只是改变程序的内部结构而不影响外部行为。即使在动态语言中,仍然有一些事情你可以期待发生和测试,你只是失去了编译器的一点帮助。
在 C# 3.0 中使用var的好处之一是您可以经常更改类型而不会破坏任何代码。类型需要看起来仍然相同 - 必须存在具有相同名称的属性,具有相同或相似签名的方法必须仍然存在。但是你真的可以改变成一个非常不同的类型,即使不使用 ReSharper 之类的东西。