首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

强静态类型,我愿意坚守不变的选择……

作者 | Tom Hacohen       译者 | 弯月

责编 | 夏萌

出品 | CSDN(ID:CSDNnews)

我编写软件已有 20 多年了,随着时间一天天过去,我越来越确信强静态类型是一个好主意,而且几乎在所有场合都是正确的选择。

当然,无类型语言有一定的用途,例如在使用 REPL 时无类型语言就是一个更优的选择,或者无法使用类型语言的环境(例如 shell)中的一次性脚本。然而,几乎在所有其他情况下,强类型都是不二之选。

不使用类型有一些优势,例如加快开发速度,但与使用类型的所有优势相比,不使用类型的优势就显得微不足道了。对此我想说:

编写没有类型的软件可以让你全速前进。全速冲向悬崖。

关于强静态类型的问题很简单:你愿意多做一点工作,在编译时(或在非编译语言的类型检查时)检查不变量;还是少做一点工作在运行时强制检查;或者更糟糕的是,即使在运行时也没有强制检查(例如 JavaScript,1 +“2”== 12)。

运行时出错绝非明智之选。首先,这意味着你未能在开发过程中捕获这些错误。其次,这些错误是通过客户捕获的。虽然测试有帮助,但是考虑到无限的可能性,为每个可能输入错误的函数参数编写测试是不可能的。即便可以,测试错误类型所需付出的代价也远高于使用类型。

类型可以减少错误

类型可以为代码提供注释,这对人类和机器都有好处。使用类型是一种更严格地定义不同代码片段之间的合约的方式。

下面,我们来看四个例子。这些代码的功能相同,只不过定义合约的级别不同。

第一个函数甚至没有定义参数的数量,因此不阅读文档就很难知道这段代码的作用。我相信大多数人都会同意第一个函数面目可憎,而且大家都不会编写这样的代码。不过与类型非常相似,这里定义的是调用者与被调用者之间的合约。

至于第二个函数和第三个函数,由于使用了类型,第三个需要阅读的文档更少。代码更简单,但无可否认,优势也很有限。

在第二个函数和第三个函数中,我们假设年龄是一个数字。因此,按照如下方式修改代码绝对没问题:

问题是,有些地方需要使用该函数接受HTML中的用户输入(所以始终是字符串)。这就会导致以下的结果:

但使用类型的版本无法通过编译,因为该函数只能接受数字的年龄,不接受字符串。

调用者与被调用者之间的合约对于代码库很重要,因为这样调用者就可以知道被调用者何时发生变化。这对于开源库尤其重要,因为调用者和被调用者不是由同一组人编写的。如果没有这份合约,就不可能知道变化何时出现。

类型能带来更好的开发体验

IDE和其他开发工具也可以使用类型来极大地改善开发体验。如果在写代码时判断有误,这些工具可以通知你。这可以大幅降低认知负荷。你不再需要记住上下文中所有变量和函数的类型。编译器随时在身边,并在出现问题时告诉你。

此外,这也带来了一个额外的好处:重构更容易。你可以相信编译器会告诉你某个改动(例如上述示例中的变更)是否会破坏代码中的其他地方。

另外,类型还可以帮助新来的工程师了解代码库:

他们可以根据类型定义来了解代码的用途。

由于有些变更会触发编译错误,因此修改代码的难度更低。

下面,我们来考虑按照如下方式修改上面的代码:

我们很容易找到(或者通过IDE找到)Person 在什么地方被使用。你可以看到 Person 创建于 main 中,然后被 birthdayGreeting3 使用。但是,要想知道哪里用了birthdayGreeting2,你需要阅读全部代码。

反之亦然。在阅读 birthdayGreeting2 时,很难确定它接受一个 Person 作为参数。这类问题虽然可以通过详尽的文档解决,但是:(1) 如果通过类型就能解决,为什么要用文档呢?(2) 文档总会过时,但使用类型的话,代码本身就是文档。

下面的代码也一样:

我们都知道,变量名要有意义。类型也一样,它只是加强版的变量名而已。

类型系统编码了一切

我们喜欢类型。我们会尽可能在类型系统中写入更多信息,这样就能在编译时找出所有错误,并尽可能提升开发者体验。

例如,Redis 是一个基于字符串的协议,本身并没有类型。我们用 Redis 作为缓存。问题在于,Redis 层会丢失所有的类型信息,这会导致 bug。

考虑以下的代码:

这段代码里有几个bug:

第二个键的名称错了一个字母。

尝试将 person加载到 pet 类型中。

为了避免这类错误,我们做了两件事。第一,我们要求键必须是特定的类型(而不是普通的字符串),创建该类型需要调用一个特殊的函数。第二,我们强制键必须和值组成一组。

所以上面的例子就变成了这样:

这就好得多了,上面说的两个 bug 不可能存在了。但我们还可以做得更好!

考虑下面的函数:

这个函数有几个问题。首先,我们并不太清楚 id 是谁的 id。是 person?还是 pet?很容易用错,像下面这样:

第二,我们失去了可发现性。很难知道 Pet 与 Person 之间有联系。

所以我们给每种 id 定义了一个类型,以确保不会用错。调整后的代码如下:

与前面的例子相比,这种写法显然更好。

但仍然有个问题。如果 API 接受 id,我们怎么知道它是合法的?所有的 pet id 都以 pet_ 为前缀,后面跟一个 Ksuid,例如:pet_25SVqQSCVpGZh5SmuV0A7X0E3rw。

我们希望能够告诉客户,他们给 API 传递了错误的 id,例如在需要pet id 的地方传递了 person id。解决方法之一就是进行验证,但验证很容易忘记。

所以我们强制要求 PetId 必须先进行验证才能创建。这样我们就可以知道,所有创建了 PetId 的代码都验证了其合法性。这就意味着,如果 pet 在数据库中不存在时给客户返回 404 Not Found 的情况的确是该 id 不存在。如果 id 本身不合法,就会返回 422 或 400。

为什么并非每个人都喜欢类型?

关于类型的主要争议在于:

降低开发速度。

陡峭的学习曲线和复杂的类型。

大量工作量和样板代码。

首先,我要声明,即使以上都是真的,前面提到的好处带来的价值也远胜于这些麻烦。何况我并不同意某些观点。

首先是开发速度。对于创建原型来说,不使用类型的速度更快。你可以随意删除一段代码,编译器不会报错。如果你不知道某些字段的正确值,则可以设置错误的值,等等。

但正如上文所述,“编写没有类型的软件可以让你全速前进。全速冲向悬崖。”问题在于这样的编程方法太激进了,而且会带来不必要的技术债务。当你的代码无法正常运行时(无论是在本地、在测试环境还是在生产环境中),你需要付出数倍的努力去调试。

至于学习曲线,没错,多学一样东西的确要花时间。但我想说,绝大多数人不需要成为类型的专家。他们可以从最简单的类型表达式开始,看看他们有没有遇到学习障碍。但是,如果只需要最简单的类型,就几乎不会有任何学习障碍。

此外,我们本来就要学习如何编程、如何使用框架(React、Axum等),以及许多知识。我不认为类型系统会带来太大的学习压力。

关于学习曲线的最后一点,也是很重要的一点:我坚信,不使用类型带来的平缓学习曲线,远不如类型带来的好处。特别是,学习类型是一次性的努力。

关于最后一点:使用类型带来的工作量和样板代码。我坚信,这些努力远远比不上不使用类型带来的麻烦。

不使用类型,就需要编写大量的文档和大量的测试,才能达到基本的检查水平。文档会过时,测试用例也会过时;不论哪种情况,你都需要大量的努力,这远远超过了添加类型的工作量。阅读有类型的代码会更容易,因为你随时能看到类型,不需要去查阅函数的文档。

结束语

许多争论,例如 vim 还是 emacs、tab 还是空格等,我都认为双方都有道理。但在类型方面,类型的代价非常低,而好处非常明显,我不明白为什么有些人选择不使用类型。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OFiA0blfluiY21_riSdGVoyQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券