强静态类型,真的无敌

2023年 10月 11日 68.4k 0

作者丨Tom Hacohen

编译丨千山

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

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

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

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

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

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

1、类型导致更少的错误

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

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

// Params: Name (a string) and age (a number).
function birthdayGreeting1(...params) {
    return `${params[0]} is ${params[1]}!`;
}

// Params: Name (a string) and age (a number).
function birthdayGreeting2(name, age) {
    return `${name} is ${age}!`;
}

function birthdayGreeting3(name: string, age: number): string {
    return `${name} is ${age}!`;
}

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

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

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

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

function birthdayGreeting3(name: string, age: number): string {
    return `${name} will turn ${age + 1} next year!`;
}

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

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

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

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

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

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

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

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

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

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

class Person {
  name: string;
  age: number;
}

function birthdayGreeting2(person) {
    return `${person.name} will turn ${person.age + 1} next year!`;
}

function birthdayGreeting3(person: Person): string {
    return `${person.name} will turn ${person.age + 1} next year!`;
}

function main() {
  const person: Person = { name: "Hello", age: 12 };

  birthdayGreeting2(person);

  birthdayGreeting3(person);
}

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

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

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

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

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

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

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

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

考虑下面这段代码:

pub struct Person {
    pub id: String,
    pub name: String,
    pub age: u16,
}

pub struct Pet {
    pub id: String,
    pub owner: String,
}


let id = "p123";
let person = Person::new("John", 20);
cache.set(format!("person-{id}"), person);
// ...
let pet: Pet = cache.get(format!("preson-{id}"));

代码片段中有几个bug:

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

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

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

pub struct PersonCacheKey(String);

impl PersonCacheKey {
    fn new(id: &str) -> Self { ... }
}

pub struct Person {
    pub id: String,
    pub name: String,
    pub age: u16,
}

pub struct PetCacheKey;

pub struct Pet {
    pub id: String,
    pub owner: String,
}


let id = "p123";
let person = Person::new(id, "John", 20);
cache.set(PersonCacheKey::new(id), person);
// ...
// Compilation will fail on the next line
let pet: Pet = cache.get(PersonCacheKey::new(id));

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

请考虑以下函数:

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

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

let pet = ...;
do_something(pet.id); //

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论