浅谈 Rust 类型设计:对比 TS

前言

这几年随着越来越多前端基建项目用 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 位的

而浮点类型,根据精确度的要求分为:f32f64

有多个数字类型的区分,在做一些数字运算的时候相对麻烦,为了方便,有时候不得不依赖 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 中,基础类型还包括 undefinednullsymbolbigInt等。

语言设计上,Rust 的语言设计,就规避了 undefinednull的引入,而是通过枚举 Option 解决空值问题。而 symbol是 TS 中因为语言问题,引入的一种特殊类型,而 bigInt在 Rust 中众多的数字类型早已覆盖掉。

内存安全一词在文中反复出现,对于无 GC ,无须手动管理内存的 Rust,编译时就要去保证内存安全,因此这也会体现在语言的类型设计和一些 Rust 语言语法机制上,例如借用检查、生命周期标注等。

Reference

相关推荐
u***276134 分钟前
TypeScript 与后端开发Node.js
javascript·typescript·node.js
Rust语言中文社区2 小时前
【Rust日报】Dioxus 用起来有趣吗?
开发语言·后端·rust
小灰灰搞电子2 小时前
Rust Slint实现颜色选择器源码分享
开发语言·后端·rust
Source.Liu4 小时前
【Chrono库】Unix-like 系统时区处理实现(src/offset/local/unix.rs)
rust·time
avi91115 小时前
Lua高级语法-第二篇
lua·游戏开发·编程语言·语法糖
今天没有盐6 小时前
Python算法实战:从滑动窗口到数学可视化
python·pycharm·编程语言
I***26156 小时前
数据库操作与数据管理——Rust 与 SQLite 的集成
数据库·rust·sqlite
用户600071819109 小时前
【翻译】TypeScript中可区分联合类型的省略
typescript
元Y亨H9 小时前
RustDesk 自建远程桌面服务器部署指南
rust
@大迁世界1 天前
相信我兄弟:Cloudflare Rust 的 .unwrap() 方法在 330 多个数据中心引发了恐慌
开发语言·后端·rust