• 强静态类型,真的无敌


    我写软件已经有20多年了,随着时间的推移,我越来越确信强静态类型不仅是一个好主意,而且几乎总是正确的选择。

    非类型化语言(或语言变体)肯定有用途,例如,当使用REPL时,或者在已经无可救药的无类型环境(例如shell)中使用一次性脚本时,它们会更好。然而,在几乎所有其他情况下,强类型都是首选。

    不使用类型是有好处的,比如更快的开发速度,但与所有的好处相比,它们就显得微不足道了。对此,我要说:

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

    关于强静态类型的问题很简单:你是愿意多做一点工作,在编译时检查不变量(或非编译语言的类型检查时间),还是愿意少做一点工作,在运行时强制执行它们,或者更糟糕的是,即使在运行时也不强制执行(JavaScript,我在看着你…)。

    在运行时出错是一个糟糕的想法。首先,这意味着在开发过程中你不会总是抓住它们。其次,当你抓住他们的时候,它会以面向客户的方式发生。是的,测试有帮助,但是考虑到无限的可能性,为每一个可能的错误类型函数参数编写测试是不可能的。即使可以,拥有类型也比测试错误类型容易得多。

    1、类型导致更少的错误

    类型还为代码提供注释,使人类和机器都受益。拥有类型是一种更严格地定义不同代码段之间协定的方法。

    请考虑以下四个示例。它们都做完全相同的事情,只是契约定义级别不同。

    1. // Params: Name (a string) and age (a number).
    2. function birthdayGreeting1(...params) {
    3. return `${params[0]} is ${params[1]}!`;
    4. }
    5. // Params: Name (a string) and age (a number).
    6. function birthdayGreeting2(name, age) {
    7. return `${name} is ${age}!`;
    8. }
    9. function birthdayGreeting3(name: string, age: number): string {
    10. return `${name} is ${age}!`;
    11. }

    第一个甚至没有定义参数的数量,因此如果不阅读文档,很难知道它的作用。我相信大多数人都会同意第一个是令人讨厌的,不会写这样的代码。虽然它的思想与类型非常相似,但它是关于定义调用者和被调用者之间的契约。

    至于第二个和第三个,由于类型的原因,第三个将需要更少的文档。代码更简单,但不可否认,优点相当有限。好吧,直到你真正更改这个函数前......

    在第二个和第三个函数中,作者假设年龄是一个数字。因此,更改代码绝对没问题,如下所示:

    1. // Params: Name (a string) and age (a number).
    2. function birthdayGreeting2(name, age) {
    3. return `${name} will turn ${age + 1} next year!`;
    4. }
    5. function birthdayGreeting3(name: string, age: number): string {
    6. return `${name} will turn ${age + 1} next year!`;
    7. }

    问题是使用此代码的某些位置接受从HTML输入(因此始终是字符串)收集的用户输入。这将导致:

    1. > birthdayGreeting2("John", "20")
    2. "John will turn 201 next year!"

    虽然类型化版本将无法正确编译,因为此函数将年龄除外,否则年龄是数字,而不是字符串。

    在调用方和被调用方之间建立协定对于代码库非常重要,这样调用方就可以知道被调用方何时更改。这对于开源库尤其重要,因为调用方和被调用方不是由同一组人编写的。没有这个合同,就不可能知道事情在发生时是如何变化的。

    2、类型带来更好的开发体验

    IDE和其他开发工具也可以使用类型来极大地改善开发体验。如果你的任何期望是错误的,你将在编写代码时得到通知。这大大降低了认知负荷。你不再需要记住上下文中所有变量和函数的类型。编译器将与你同在,并在出现问题时告诉你。

    这也带来了一个非常好的额外好处:更容易重构。你可以相信编译器会让你知道你所做的更改(例如上面示例中的更改)是否会破坏代码中其他地方所做的假设。

    类型还可以使新工程师更容易加入代码库或库:

    • 他们可以遵循类型定义来了解事物的使用位置。
    • 修改东西要容易得多,因为更改会触发编译错误。

    让我们考虑对上述代码进行以下更改:

    1. class Person {
    2. name: string;
    3. age: number;
    4. }
    5. function birthdayGreeting2(person) {
    6. return `${person.name} will turn ${person.age + 1} next year!`;
    7. }
    8. function birthdayGreeting3(person: Person): string {
    9. return `${person.name} will turn ${person.age + 1} next year!`;
    10. }
    11. function main() {
    12. const person: Person = { name: "Hello", age: 12 };
    13. birthdayGreeting2(person);
    14. birthdayGreeting3(person);
    15. }

    很容易查看(或使用IDE查找)所有使用过的位置。你可以看到它被启动,你可以看到它被使用。然而,为了知道它的用途,你需要阅读整个代码库。

    这样做的另一方面是,在看的时候,很难知道它期望a作为参数。其中一些问题可以通过详尽的文档来解决,但是:(1)如果使用类型可以实现更多的功能,为什么还要费心呢?(2)文档过时,这里的代码是document。

    这与你不编写代码的方式非常相似:

    1. // a is a person
    2. function birthdayGreeting2(a) {
    3. b = a.name;
    4. c = a.age;
    5. return `${b} will turn ${c + 1} next year!`;
    6. }

    你可能希望使用有用的变量名。类型也是一样的,它只是steriods上的变量名。

    3、我们对类型系统中的所有内容进行编码

    在Svix,我们喜欢类型。事实上,我们尝试在类型系统中对尽可能多的信息进行编码,以便在编译时捕获所有可以在编译时捕获的错误;同时也要压缩开发者体验改进的额外里程。

    例如,Redis是一个基于字符串的协议,没有固有的类型。我们使用Redis进行缓存(以及其他功能)。问题是,我们所有的优秀的类型优势将在Redis层丢失,并且可能发生bug。

    考虑下面这段代码:

    1. pub struct Person {
    2. pub id: String,
    3. pub name: String,
    4. pub age: u16,
    5. }
    6. pub struct Pet {
    7. pub id: String,
    8. pub owner: String,
    9. }
    10. let id = "p123";
    11. let person = Person::new("John", 20);
    12. cache.set(format!("person-{id}"), person);
    13. // ...
    14. let pet: Pet = cache.get(format!("preson-{id}"));

    代码片段中有几个bug:

    • 第二个键名称有个拼写错误。
    • 我们正在尝试将一个人装入宠物类型。

    为了避免这样的问题,我们在Svix做了两件事。首先,我们要求键是某种类型的(不是泛型字符串),要创建这种类型,需要调用一个特定的函数。我们做的第二件事,是将键与值强制配对。

    所以上面的例子看起来像这样:

    1. pub struct PersonCacheKey(String);
    2. impl PersonCacheKey {
    3. fn new(id: &str) -> Self { ... }
    4. }
    5. pub struct Person {
    6. pub id: String,
    7. pub name: String,
    8. pub age: u16,
    9. }
    10. pub struct PetCacheKey;
    11. pub struct Pet {
    12. pub id: String,
    13. pub owner: String,
    14. }
    15. let id = "p123";
    16. let person = Person::new(id, "John", 20);
    17. cache.set(PersonCacheKey::new(id), person);
    18. // ...
    19. // Compilation will fail on the next line
    20. let pet: Pet = cache.get(PersonCacheKey::new(id));

    这已经好多了,并且不可能出现前面提到的任何错误。虽然我们可以做得更好!

    请考虑以下函数:

    1. pub fn do_something(id: String) {
    2. let person: Person = cache.get(PersonCacheKey::new(id));
    3. // ...
    4. }

    它有几个问题。首先是不太清楚id应该用来做什么。是一个人吗?一个宠物吗?很容易意外地用错误的名称调用它,就像下面的例子一样。

    1. let pet = ...;
    2. do_something(pet.id); // <-- should be pet.owner!

    第二,我们正在失去可发现性。很难知道宠物与人有关系。

    因此,在Svix,我们为每个类型都有一个特殊的类型,以确保没有错误。调整后的代码如下所示:

    1. pub struct PersonId(String);
    2. pub struct PetId(String);
    3. pub struct Person {
    4. pub id: PersonId,
    5. pub name: String,
    6. pub age: u16,
    7. }
    8. pub struct Pet {
    9. pub id: PetId,
    10. pub owner: PersonId,
    11. }

    这确实比我们之前的例子要好得多。

    4、那么为什么不是每个人都喜欢类型呢?

    反对类型一方论证的主要依据是:

    • 开发速度
    • 学习曲线和类型复杂性
    • 所需的工作量和样板

    首先,我认为即使上述所有情况都是真的,上面提到的优势也值得麻烦。

    其次是开发速度。没有类型的原型设计肯定要快得多。你可以注释掉代码片段,并且不会让编译器向你抱怨。你可以为某些字段设置错误的值,直到你准备好找出正确的字段等。

    虽然就像我上面说的:“编写没有类型的软件可以让你全速前进。全速向悬崖走去。”问题在于,这只是激进且不必要的技术债务。当你需要调试代码无法正常工作的原因时(无论在本地、测试套件或生产环境中),你都需要多次支付这笔费用。

    至于学习曲线:是的,学习更多的东西需要时间。不过我得说,大多数人不需要成为类型专家。他们可以使用非常简单的类型表达式过日子,并询问他们是否曾经遇到瓶颈。然而,如果你让事情保持简单,你可能很少会碰到一个。

    此外,人们已经被要求学习如何编码,学习框架(React,Axum等),以及许多其他东西。我认为学习负担并不像人们想象的那么重。

    最后,但并非最不重要的是,关于学习曲线:我坚信,不必了解类型而减少学习曲线的好处远远小于在特定代码库上使用类型脚本的好处。特别是因为学习类型是一次性的成本。

    最后一点是关于在代码库中使用类型所需的工作量和样板。我坚信,比起不写类型所需要的工作量,这种工作量实际上要少得多。

    不使用类型需要大量的文档和测试,才能达到基本的健康水平。文档可能会过时,测试也会过时;无论哪种方式,它们都比添加正确的类型需要更多的努力。阅读带有类型的代码也更容易,因为你可以内联获取类型,而不是在函数文档中获取类型,在函数文档中,它的格式不一致,并且增加了很多干扰。

    是的,在不支持推理的语言中,类型可能是一种痛苦,例如Java可能很乏味:

    1. Person person1 = newPerson();
    2. Person person2 = newPerson();
    3. Person child = makeChild(person1, person2);

    而其他具有推理功能的语言(如 Rust)则要好得多:

    1. let person1 = new_person();
    2. let person2 = new_person();
    3. let child = make_child(person1, person2);

    因此,拥有合适的工具肯定会有所帮助。说到工具,为了获得类型的好处,你可能需要使用支持语言感知的现代代码完成的代码编辑器(或 IDE)。

    5、结语

    我可以在许多话题上看到双方的争论,比如vs.,制表符vs.空格,甚至更具争议性的主题。尽管在这种情况下,与收益相比,成本是如此之低,以至于我不明白为什么有人会选择不使用类型。我不知道自己忽略了什么,我只知道,强类型是我愿意死在上面的一座山。

    6、快速开发工具推荐

    这是一款基于SpringBoot+Vue的前后端分离的项目,麻雀虽小,五脏俱全,开箱即用!

    JNPF开发平台的前端采用Vue.js,这是一种流行的前端JavaScript框架,用于构建用户界面。Vue.js具有轻量级、可扩展性强和生态系统丰富等特点,被广泛应用于构建单页面应用程序。

    后端采用SpringBoot,这是一种基于Java的开源框架,用于简化Spring应用的初始搭建以及开发过程。SpringBoot通过自动配置和约定大于配置的原则,简化了Spring应用的配置和开发。此外,JNPF还采用MyBatis-Plus作为持久层框架,它是一个功能强大的MyBatis扩展,可以大大简化数据库操作的开发。

    核心功能:表单引擎、可视化引擎、BI引擎、流程引擎、权限引擎、门户引擎、大屏引擎、接口中心、物联平台

    尝试自己开发一个应用!应用地址:https://www.jnpfsoft.com/?csdn

  • 相关阅读:
    git基础操作命令
    如何选择百科营销机构?如何挑选百科代建公司?
    浅谈Rust--学习心得及rust的优势与劣势
    Apache ShardingSphere 5.1.0 正式发布
    Swiper实现轮播效果
    基于 kubeadm 的 k8s(1.24.x) 安装过程
    一、Hadoop初始化配置(final+ubuntu保姆级教程)
    【7 查找】二分查找k值。
    Socket网络编程
    JavaScript完整体系
  • 原文地址:https://blog.csdn.net/sdasdas12/article/details/133773838