前言
这几年随着越来越多前端基建项目用 Rust 重写,也预示着 Rust 非常有可能成为前端基建的未来,从社区的趋势看,或者已经是了。参加 2023 年杭州 FEDay, 已知字节除了开源的构建工具 Rspack,连跨端的一些技术栈也开始使用 Rust,当然也这依赖字节内部本身有不错的 Rust 生态。开源社区方面,Rollup 团队也开始着手开始布局 rolldown-rs,即 Rollup 的 Rust 版本。Vercel 团队也是有重量级工具使用 Rust 编写,例如 Turbopack。在结束了框架之争、构建工具之争、JS 语言层面核心特性的稳定化后,Web 前端开发也终于从刀耕火种的时代进入趋于稳定的时代。
随着前端应用日益复杂,开始面临一些新的问题,例如巨石应用下的微前端架构引入的几十上百个子应用的构建问题、代码 Lint 和 Prettier 美化等,导致跑一个完整的 CI 工作流动不动就需要花费十几或者几十分钟。无论使用缓存还是并行去执行一些任务,Webpack 本身和社区也是提供了很多方案,字节 Infra 团队也基于 Webpack 做了各种尝试,详情可以看这篇文章:Bundler 的设计取舍:为什么要开发 Rspack,最终发现都没法很好解决巨石应用的构建性能问题。回归到语言特性,JS 本身设计出来只是一个用于运行在浏览器端的脚本语言,作者可能也没想过有一天,JS 需要背负这么多的使命,从服务端到编译再到跨端,最后我们还是得承认,JS 不是万能的。
于是,社区把目光投向了 Rust ,无论是当初语言设计的定位,还是目前的生态去看,它是一个全新的选择。
语言特性
为了接下来更好地理解 Rust 的类型设计,我们先从语言特性出发。
Rust 有以下关键的语言特性:
- 内存安全,无 GC 且无需手动管理内存,这就依赖编译时的检查,必须提前规避掉不安全的变量引用问题
- 高性能 ,编译器基于 LLVM ,这使得 Rust 代码可以通过高度优化的机器代码来实现优异性能
- 语言级安全性,强大的 cargo 编译器,在编译时提前避免潜在的 runtime 安全问题
- 高并发性,Rust 在语言层面上支持并发编程,它的并发模型是基于"Actors"模式的,它允许在不同的线程之间安全地共享数据,而无需使用锁或其他同步机制
- 社区生态,目前因为 Rust 可以应用在多个场景,例如用于开发前端基建工具、数据库、云原生、系统工具、操作系统、区块链等,使得 Rust 社区非常活跃
我个人已经学习 Rust 一段时间,有如下比较喜欢的一些点:
- cargo 编译器的强大,无论是语法问题、未使用变量、编写文档、单测等方方面面,cargo 编译一条龙服务全部包揽;
- 内置 Option 枚举 ,没有 JS
null
或者其他语言中的空指针等问题,避免 10 亿美元故事的困扰 - 模式匹配,语言内置的策略模式
- 特征(Trait)和 Struct,没有 Class,不需要理解 OOP 中复杂的各种概念,反而推荐使用更 FP 的方式编程
介绍完语言特性,下面进入本文的正题。
基础类型(Primitive Type)
数字类型
在 TS 中,数字只有一种类型:number
:
typescript
let num1: number = 255;
let num2: number = 3.142592;
简单粗暴!
而 Rust 从内存使用 考虑,根据可存储的数字范围将数字类型划分为整型、无符号整型、浮点型:
rust
let num: u8 = 255;
let num1: u64 = 1024;
let f1: f64 = 3.141592;
而且根据不同的使用场景,将整型划分为以下几种:
- 8 位 ,
i8
,u8
- 16 位 ,
i16
,u16
- 32 位 ,
i32
,u32
- 64 位 ,
i64
,u64
- 128 位 ,
i128
,u128
- 视计算器架构而定的 ,
isize
,usize
,若电脑 CPU 是 32 位的,则这两个类型是 32 位的
而浮点类型,根据精确度的要求分为:f32
和 f64
。
有多个数字类型的区分,在做一些数字运算的时候相对麻烦,为了方便,有时候不得不依赖 as
做类型转换,但是一定要确保类型兼容,转换是符合预期的。例如实现一个将浮点数小数部分也转换为整数的方法,比较简单的做法:
typescript
fn f64_to_int (value: f64, digits: u32) -> i64 {
let base: i32 = 10;
(value * base.pow(digits) as f64).round() as i64
}
assert_eq!(f64_to_int(12.12, 2), 1212);
// 预期外的转换
(300_i32 as i8) // get 44
布尔类型
布尔类型在任何编程语言应该都不太意外的都一样,只有两个值,在 TS 中:
typescript
let isUsed: boolean = true;
let isNotUsed: boolean = false;
Rust 中:
rust
let is_used: bool = true;
let is_not_used: bool = false;
布尔类型大多时候用于 if
控制语句,因为语言实现问题,在 TS 中,有比较多的隐式类型转换(准确说是编译后的 JS),因此下面这种使用方式是可以的:
typescript
// TS 编译通过
if (2) {
console.log('hello');
}
在 Rust 中,在编译时就报错了:
rust
if 2 {
print!("hello");
}
// error[E0308]: mismatched types
// expected `bool`, found integer
一切为了内存安全,不应该有任何隐式转换。
字符类型
请注意这里是字符类型 ,并不是字符串类型,在 TS 中,只有一个字符串类型,没有字符类型的说法。
typescript
let str: string = 'abc';
str = '中';
str = '😻';
在 Rust 中,字符类型 和字符串类型 是两种不同的类型,而且字符串类型是一种复合类型(Compound Type) ,不属于基础类型。下一章节,我们再详细介绍。
Rust 的字符不仅仅是 ASCII 编码,所有的 Unicode 值都可以作为 Rust 字符:
rust
let mut c = 'a';
c = '国';
c = '😻';
注意一个细节,Rust 中的字符类型是用单引号 ' 包裹;在 TS 中,这是没有任何区别的,从习惯上,我个人使用单引号更多。
单元类型
考虑以下场景,如果我想定义一个函数的类型,但是这个函数不要求返回任何数据,这在实际场景中非常常见。例如打印日志或者事件回调函数,在 TS 中,可以通过以下方式定义函数的类型:
typescript
type ClickHandler = (e: MouseEvent<HTMLButtonElement>) => void;
我们称 void
为 TS 中的单元类型。
在 Rust 中,使用 ()
表示单元类型 ,Rust 中的 main
函数就是典型的不返回任何数据的函数:
rust
fn main () {
print!("hello");
}
但是在 TS 中,虽然类型设计上不要求返回任何类型,就算你返回一个数据也是可以的:
typescript
type ClickHandler = (e: MouseEvent<HTMLButtonElement>) => void;
// TS 编译通过
const onClick: ClickHandler = (e) => {
console.log(e.target);
return e.target;
}
在 Rust 中,则不能这样做:
rust
struct A {
value: String,
}
trait PrintSome {
fn print(value: String) -> ();
}
impl PrintSome for A {
fn print(value: String) {
print!("{:?}", value);
value
}
}
// Error: mismatched types
// expected `()`, found `String`
合理,一切为了内存安全,你返回数据意味着 Rust 在编译时就要做更多的编译时检查,例如借用检查,生命周期标注,来避免悬垂引用。
总结
在 Rust 中,就只有以上 4 种基础类型,限于篇幅,下一篇文章我会继续聊 Rust 中的复合类型。
在 TS 中,基础类型还包括 undefined
、null
、symbol
、bigInt
等。
语言设计上,Rust 的语言设计,就规避了 undefined
、null
的引入,而是通过枚举 Option
解决空值问题。而 symbol
是 TS 中因为语言问题,引入的一种特殊类型,而 bigInt
在 Rust 中众多的数字类型早已覆盖掉。
内存安全一词在文中反复出现,对于无 GC ,无须手动管理内存的 Rust,编译时就要去保证内存安全,因此这也会体现在语言的类型设计和一些 Rust 语言语法机制上,例如借用检查、生命周期标注等。