静态/强类型和重构
在我看来,静态/强类型编程语言最宝贵的一点是它有助于重构:如果/当你更改任何 API 时,编译器会告诉你该更改破坏了哪些内容。
我可以想象用运行时/弱类型语言编写代码......但我无法想象没有编译器帮助的重构,我无法想象在没有重构的情况下编写数万行代码。
这是真的?
It seems to me that the most invaluable thing about a static/strongly-typed programming language is that it helps refactoring: if/when you change any API, then the compiler will tell you what that change has broken.
I can imagine writing code in a runtime/weakly-typed language ... but I can't imagine refactoring without the compiler's help, and I can't imagine writing tens of thousands of lines of code without refactoring.
Is this true?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

发布评论
评论(5)
首先,我是一名本地 Perl 程序员,因此一方面我从未使用静态类型网络进行编程。 OTOH 我从未与他们一起编程过,所以我无法谈论他们的好处。 我能说的是重构是什么样的。
我不认为缺乏静态类型是重构的问题。 我发现的问题是缺乏重构浏览器。 动态语言的问题是,在实际运行代码之前,您并不真正知道代码到底要做什么。 Perl 比大多数语言都更具备这一点。 Perl 还有一个额外的问题,那就是它的语法非常复杂,几乎无法解析。 结果:没有重构工具(尽管他们在这方面工作得非常快)。 最终的结果是我必须手动重构。 这就是引入错误的原因。
我通常会进行测试来捕捉它们。 我确实发现自己经常面对一堆未经测试和几乎无法测试的代码,存在先有鸡还是先有蛋的问题,即必须重构代码才能测试它,但又必须测试它才能重构它。 恶心。 此时,我必须编写一些非常愚蠢的高级“程序输出是否与之前相同”之类的测试,以确保我没有破坏某些东西。
正如 Java、C++ 或 C# 中所设想的那样,静态类型实际上只能解决一小部分编程问题。 它们保证您的接口传递带有正确标签的数据位。 但仅仅因为您获得了一个 Collection,并不意味着该 Collection 包含您认为它包含的数据。 因为你得到一个整数并不意味着你得到了正确的整数。 您的方法接受一个 User 对象,但该用户是否已登录?
经典示例: public static double sqrt(double a)
是 Java 平方根函数的签名。 平方根不适用于负数。 签名里哪里有这么说? 事实并非如此。 更糟糕的是,它在哪里说明了该函数的作用? 签名仅说明它采用什么类型以及返回什么。 它没有说明中间发生的事情,而这就是有趣的代码所在的地方。 有些人尝试使用按契约设计来获取完整的 API,这可以广泛地描述为嵌入函数的输入、输出和副作用(或缺乏)的运行时测试......但那是另一个节目。
API 不仅仅是函数签名(如果不是,您就不需要 Javadoc 中的所有描述性散文),并且重构也不仅仅是更改 API。
静态类型、静态编译、非动态语言给您带来的最大重构优势是能够编写重构工具来为您执行相当复杂的重构,因为它知道对您的方法的所有调用在哪里。 我非常羡慕 IntelliJ IDEA。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
我认为您将检查类型的时间与检查类型的方式混为一谈。 运行时类型不一定很弱。
静态类型的主要优点正如你所说:它们是详尽的。 只需让编译器执行该操作,您就可以确信所有调用站点都符合该类型。
静态类型的主要限制是它们可以表达的约束受到限制。 这因语言而异,大多数语言具有相对简单的类型系统(c、java),而其他语言则具有极其强大的类型系统(haskell、cayenne)。
由于这种限制,类型本身是不够的。 例如,在 java 中,类型或多或少仅限于检查类型名称匹配。 这意味着您想要检查的任何约束的含义都必须编码到某种命名方案中,因此 Java 代码中存在大量的间接和样板。 C++ 稍微好一点,因为模板允许更多的表达能力,但与依赖类型可以做的事情还有些差距。 我不确定更强大的类型系统的缺点是什么,但显然肯定有一些或更多的人会在工业中使用它们。
即使您使用静态类型,很可能它的表现力不足以检查您关心的所有内容,因此您也需要编写测试。 静态类型是否比样板文件中所需的工作量更省力,这个争论已经持续了很长时间,而且我认为没有一个适用于所有情况的简单答案。
至于你的第二个问题:
我们如何在运行时类型语言中安全地重构?
答案是测试。 您的测试必须涵盖所有重要的情况。 工具可以帮助您衡量测试的详尽程度。 覆盖率检查工具可让您了解测试是否覆盖了代码行。 测试变异工具(jester、heckle)可以让您知道您的测试在逻辑上是否不完整。 验收测试让您知道您编写的内容是否符合要求,最后回归和性能测试可确保产品的每个新版本都保持上一个版本的质量。
与依赖复杂的类型间接寻址相比,进行适当的测试的好处之一是调试变得更加简单。 运行测试时,您会在测试中得到特定的失败断言,这些断言清楚地表达了它们正在做什么,而不是迟钝的编译器错误语句(想想 C++ 模板错误)。
无论您使用什么工具:编写您有信心的代码都需要付出努力。 它很可能需要编写大量测试。 如果错误的惩罚非常,例如航空航天或医疗控制软件,您可能需要使用正式的数学方法来证明软件的行为,这使得此类开发极其昂贵。
I think you're conflating when types are checked with how they're checked. Runtime typing isn't necessarily weak.
The main advantage of static types is exactly what you say: they're exhaustive. You can be confident all call sites conform to the type just by letting the compiler do it's thing.
The main limitation of static types is that they're limited in the constraints they can express. This varies by language, with most languages having relatively simple type systems (c, java), and others with extremely powerful type systems (haskell, cayenne).
Because of this limitation types on their own are not sufficient. For example, in java types are more or less restricted to checking type names match. This means the meaning of any constraint you want checked has to be encoded into a naming scheme of some sort, hence the plethora of indirections and boiler plate common to java code. C++ is a little better in that templates allow a bit more expressiveness, but don't come close to what you can do with dependent types. I'm not sure what the downsides to the more powerful type systems are, though clearly there must be some or more people would be using them in industry.
Even if you're using static typing, chances are it's not expressive enough to check everything you care about, so you'll need to write tests too. Whether static typing saves you more effort than it requires in boilerplate is a debate that's raged for ages and that I don't think has a simple answer for all situations.
As to your second question:
How can we re-factor safely in a runtime typed language?
The answer is tests. Your tests have to cover all the cases that matter. Tools can help you in gauging how exhaustive your tests are. Coverage checking tools let you know wether lines of code are covered by the tests or not. Test mutation tools (jester, heckle) can let you know if your tests are logically incomplete. Acceptance tests let you know what you've written matches requirements, and lastly regression and performance tests ensure that each new version of the product maintains the quality of the last.
One of the great things about having proper testing in place vs relying on elaborate type indirections is that debugging becomes much simpler. When running the tests you get specific failed assertions within tests that clearly express what they're doing, rather than obtuse compiler error statements (think c++ template errors).
No matter what tools you use: writing code you're confident in will require effort. It most likely will require writing a lot of tests. If the penalty for bugs is very high, such as aerospace or medical control software, you may need to use formal mathematical methods to prove the behavior of your software, which makes such development extremely expensive.