静态/强类型和重构
在我看来,静态/强类型编程语言最宝贵的一点是它有助于重构:如果/当你更改任何 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)
我认为您将检查类型的时间与检查类型的方式混为一谈。 运行时类型不一定很弱。
静态类型的主要优点正如你所说:它们是详尽的。 只需让编译器执行该操作,您就可以确信所有调用站点都符合该类型。
静态类型的主要限制是它们可以表达的约束受到限制。 这因语言而异,大多数语言具有相对简单的类型系统(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.
我完全同意你的观点。 动态类型语言应该擅长的灵活性实际上使得代码很难维护。 真的,是否有这样的东西,如果数据类型以不平凡的方式更改而无需实际更改代码,则程序可以继续工作?
同时,您可以检查传递的变量的类型,如果不是预期的类型,则会以某种方式失败。 您仍然需要运行代码来根除这些情况,但至少会告诉您一些信息。
我认为 Google 的内部工具实际上会对其 Javascript 进行编译并可能进行类型检查。 我希望我有这些工具。
I totally agree with your sentiment. The very flexibility that dynamically typed languages are supposed to be good at is actually what makes the code very hard to maintain. Really, is there such a thing as a program that continues to work if the data types are changed in a non trivial way without actually changing the code?
In the mean time, you could check the type of variable being passed, and somehow fail if its not the expected type. You'd still have to run your code to root out those cases, but at least something would tell you.
I think Google's internal tools actually do a compilation and probably type checking to their Javascript. I wish I had those tools.
首先,我是一名本地 Perl 程序员,因此一方面我从未使用静态类型网络进行编程。 OTOH 我从未与他们一起编程过,所以我无法谈论他们的好处。 我能说的是重构是什么样的。
我不认为缺乏静态类型是重构的问题。 我发现的问题是缺乏重构浏览器。 动态语言的问题是,在实际运行代码之前,您并不真正知道代码到底要做什么。 Perl 比大多数语言都更具备这一点。 Perl 还有一个额外的问题,那就是它的语法非常复杂,几乎无法解析。 结果:没有重构工具(尽管他们在这方面工作得非常快)。 最终的结果是我必须手动重构。 这就是引入错误的原因。
我通常会进行测试来捕捉它们。 我确实发现自己经常面对一堆未经测试和几乎无法测试的代码,存在先有鸡还是先有蛋的问题,即必须重构代码才能测试它,但又必须测试它才能重构它。 恶心。 此时,我必须编写一些非常愚蠢的高级“程序输出是否与之前相同”之类的测试,以确保我没有破坏某些东西。
正如 Java、C++ 或 C# 中所设想的那样,静态类型实际上只能解决一小部分编程问题。 它们保证您的接口传递带有正确标签的数据位。 但仅仅因为您获得了一个 Collection,并不意味着该 Collection 包含您认为它包含的数据。 因为你得到一个整数并不意味着你得到了正确的整数。 您的方法接受一个 User 对象,但该用户是否已登录?
经典示例:
public static double sqrt(double a)
是 Java 平方根函数的签名。 平方根不适用于负数。 签名里哪里有这么说? 事实并非如此。 更糟糕的是,它在哪里说明了该函数的作用? 签名仅说明它采用什么类型以及返回什么。 它没有说明中间发生的事情,而这就是有趣的代码所在的地方。 有些人尝试使用按契约设计来获取完整的 API,这可以广泛地描述为嵌入函数的输入、输出和副作用(或缺乏)的运行时测试......但那是另一个节目。API 不仅仅是函数签名(如果不是,您就不需要 Javadoc 中的所有描述性散文),并且重构也不仅仅是更改 API。
静态类型、静态编译、非动态语言给您带来的最大重构优势是能够编写重构工具来为您执行相当复杂的重构,因为它知道对您的方法的所有调用在哪里。 我非常羡慕 IntelliJ IDEA。
To start, I'm a native Perl programmer so on the one hand I've never programmed with the net of static types. OTOH I've never programmed with them so I can't speak to their benefits. What I can speak to is what its like to refactor.
I don't find the lack of static types to be a problem wrt refactoring. What I find a problem is the lack of a refactoring browser. Dynamic languages have the problem that you don't really know what the code is really going to do until you actually run it. Perl has this more than most. Perl has the additional problem of having a very complicated, almost unparsable, syntax. Result: no refactoring tools (though they're working very rapidly on that). The end result is I have to refactor by hand. And that is what introduces bugs.
I have tests to catch them... usually. I do find myself often in front of a steaming pile of untested and nigh untestable code with the chicken/egg problem of having to refactor the code in order to test it, but having to test it in order to refactor it. Ick. At this point I have to write some very dumb, high level "does the program output the same thing it did before" sort of tests just to make sure I didn't break something.
Static types, as envisioned in Java or C++ or C#, really only solve a small class of programming problems. They guarantee your interfaces are passed bits of data with the right label. But just because you get a Collection doesn't mean that Collection contains the data you think it does. Because you get an integer doesn't mean you got the right integer. Your method takes a User object, but is that User logged in?
Classic example:
public static double sqrt(double a)
is the signature for the Java square root function. Square root doesn't work on negative numbers. Where does it say that in the signature? It doesn't. Even worse, where does it say what that function even does? The signature only says what types it takes and what it returns. It says nothing about what happens in between and that's where the interesting code lives. Some people have tried to capture the full API by using design by contract, which can broadly be described as embedding run-time tests of your function's inputs, outputs and side effects (or lack thereof)... but that's another show.An API is far more than just function signatures (if it wasn't, you wouldn't need all that descriptive prose in the Javadocs) and refactoring is far more even than just changing the API.
The biggest refactoring advantage a statically typed, statically compiled, non-dynamic language gives you is the ability to write refactoring tools to do quite complex refactorings for you because it knows where all the calls to your methods are. I'm pretty envious of IntelliJ IDEA.
我想说重构超出了编译器可以检查的范围,即使在静态类型语言中也是如此。 重构只是改变程序的内部结构,而不影响外部行为。 即使在动态语言中,仍然有一些事情是您可以预期发生和测试的,您只是失去了编译器的一点帮助。
I would say refactoring goes beyond what the compiler can check, even in statically-typed languages. Refactoring is just changing a programs internal structure without affecting the external behavior. Even in dynamic languages, there are still things that you can expect to happen and test for, you just lose a little bit of assistance from the compiler.
在 C# 3.0 中使用 var 的好处之一是您可以经常更改类型而不会破坏任何代码。 该类型仍然需要看起来相同 - 具有相同名称的属性必须存在,具有相同或相似签名的方法必须仍然存在。 但你确实可以更改为完全不同的类型,即使不使用 ReSharper 之类的东西。
One of the benefits of using var in C# 3.0 is that you can often change the type without breaking any code. The type needs to still look the same - properties with the same names must exist, methods with the same or similar signature must still exist. But you can really change to a very different type, even without using something like ReSharper.