浅谈 Rust 复合类型设计:对比 TS

前言

在前一篇文章:浅谈 Rust 类型设计:对比 TS 中,我主要围绕 Rust 的基础数据类型对比 TS 进行了介绍,从文章中,可以知道,为了保证内存安全,Rust 有更加强的类型要求,不能有任何的隐式类型转换;而且为了更加合理地使用内存,Rust 的数字类型也会划分出多种类型。

本篇文章,我主要围绕 Rust 复合数据类型进行介绍,同时还是会结合 TS 的一些类型进行比较。

复合类型(Compound Type)

字符串

在上一篇文章,我已经介绍过在 Rust 中,字符串是复合类型,而在 TS 中,字符串是基本类型。

Rust 中的字符是 Unicode 类型,每个字符占据 4 个字节内存空间 ;但是在字符串中不一样,字符串是 UTF-8 编码,也就是说字符串中的字符所占的字节数是变化的(1 - 4 字节)这样能尽量降低字符串的内存使用空间

跟我们在 TS 中定义一个字符串不同,在 Rust 中最常用的创建字符串方式如下:

rust 复制代码
let str: String = String::from("string1");

那么,如果我们照 TS 中的方式,定义一个变量:

rust 复制代码
let str = "string1";

那么它是什么?答案是,我们创建了一个字符串切片 ,类型是:&str

在 Rust 中,我们最常使用的方式,例如传递字符串变量其实更多是通过借用的方式,获取到字符串的引用:

rust 复制代码
fn say_hello(s: &str) {
    println!("{}",s);
}

let str = String::from("hello");

say_hello(&str);

在 Rust 中,字符串类型是可变的,而字符串切片是不可变的 。使用字符字面量创建的字符串切片,在编译时就知道了其内容,因此可以直接硬编码到可执行文件中,使得字符字面量运行快速高效,这得益字符串切片的不可变。

对于 String 类型,因为长度不可预测,所以需要在堆上分配一块可变的内存,并且这是在运行时完成的。

数组

数组在各种编程语言中都是最常见且常用的数据结构之一,在 TS 中定义一个数组:

typescript 复制代码
const arr: string[] = ['item1', 'item2'];
const arr1: (string | number) = ["item1", 1];
const arr3: Array<number> = [0, 1];

注意, 在 TS 中,arr1这种定义方式,就是使用联合类型,这个数组可以存储不同类型的值,这在其他语言中并不多见。

在 Rust 中,定义一个数组:

rust 复制代码
let arr = [1, 2, 3, 4, 5];

// 声明类型
let arr1: [i32; 5] = [1, 2, 3, 4, 5];

注意一个细节,在 Rust 中定义一个数组,是需要声明长度的

wtf?读到这里,写 TS 的读者有点疑惑,因为在 TS 中所有数组长度是可变的。在 Rust 中,由于数组的元素类型大小固定,且长度也是固定,因此数组是存储在栈上,性能也会非常好。

对于可变的数组,Rust 使用一种叫动态数组的数据类型声明:

rust 复制代码
let mut mutable_arr: Vec<i32> = Vec::new();
mutable_arr.push(0);

动态数组 Vector是存储在堆上,因此长度可以动态改变,所以在 Rust 中,我们需要根据实际使用场景选择数组还是动态数组。

合理,一切为了内存。

在本小节开始的时候,我提到 TS 中可以使用联合类型 声明数组类型,正常情况下,在 Rust 中是不允许的。但是,如果要做,也是可以的。其中一种方式就是使用智能指针 Box

rust 复制代码
trait Draw {
    fn draw(&self);
}

struct Button {
    id: u32,
}

impl Draw for Button {
    fn draw(&self) {
        println!("这是一个按钮 {}", self.id);
    }
}
struct Select {
    id: u32,
}

impl Draw for Select {
    fn draw(&self) {
        println!("这是一个 select {}", self.id);
    }
}

 let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::new(Selec 
   t { id: 2 })];

 for e in elems {
   e.draw()
 }

这里能使用 Box 同一化类型的原因在于:Vec 泛型要求是 Sized 的类型,而 Box 能通过创建指针的方式在堆上给数据指定一块内存,从而使得数据大小确定。

在这个例子我们还引入了 trait(特征),下面讲到结构体的时候会再对其进行介绍。

元组

在 TS 中,声明一个元组:

typescript 复制代码
const tuple: [string, number] = ["hello", 1];

TS 中的元组非常像上一小节介绍的 Rust 中的数组,看起来就是固定长度的。编译成 JS 后,就是一个简单的数组字面量:

javascript 复制代码
const tuple = ['hello', 1];

在 Rust 中,元组就可以由多种数据类型组合在一起,长度固定:

rust 复制代码
let tup: (i32, f64, u8) = (500, 6.4, 1);

并且元素顺序固定,这样就可以如上面代码示例的方式解构出每个值,在 Rust 中,其实这就是一种模式匹配

也可以使用索引的方式访问元组中的元素:

rust 复制代码
let tup: (i32, f64, u8) = (500, 6.4, 1);

assert_eq!(tup.0, 500);

枚举

在 TS 中,枚举定义相对来说简单,枚举值相对来说也比较单一:

typescript 复制代码
enum Direction {
  Up,  // 0
  Down, // 1
  Left, // 2 
  Right, // 3
}

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

枚举值主要有数字和字符串类型,默认是数字,并且从 0 开始向上递增。

这里需要额外提一下的是,在 TS 类型设计中,会有值空间和类型空间 的概念。对于类型空间,所有类型注释、大多数定义的 TS 类型在编译成 JS 后就会被擦除,例如上面元组的例子。但是,枚举是可以当做值来使用,例如:

typescript 复制代码
enum Direction {
  Up,  // 0
  Down, // 1
  Left, // 2 
  Right, // 3
}

const direction = Direction.Up;

所以,对于枚举这种类型,横跨了值空间和类型空间,在编译成 JS 后代码如下:

javascript 复制代码
var Direction;
(function (Direction) {
  Direction[Direction["Up"] = 0] = "Up";
  Direction[Direction["Down"] = 1] = "Down";
  Direction[Direction["Left"] = 2] = "Left";
  Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
const direction = Direction.Up;

如果我们要把枚举当成值使用,就不能在 import 时带上 type这样的标识符:

typescript 复制代码
import type { Direction } from './constants';

// Error: Direction cannot be used as a value because it was imported using 'import type'
const direction = Direction.Up;

回到 Rust 中的枚举,Rust 中的枚举类型非常灵活,允许你定义各种类型的成员变量:

rust 复制代码
enum Direction {
  Up, 
  Down, 
  Left, 
  Right,
}

enum DirectionU8 {
  Up(u8), 
  Down(u8), 
  Left(String), 
  Right(String),
}

let direction = Direction::Up;
let direction1 = DirectionU8::Up(0);
let direction2 = DirectionU8::Left(String::from("left"));

上面的例子可以看出来,在 Rust 中,枚举成员可以灵活设置五花八门的类型,包括结构体

回到上面的动态数组中不能存放联合类型的数据例子,通过枚举,我们也可以实现同一化:

typescript 复制代码
trait Draw {
  fn draw(&self);
}

struct Button {
  id: u32,
}
impl Draw for Button {
  fn draw(&self) {
    println!("这是一个按钮 {}", self.id);
  }
}
struct Select {
  id: u32,
}

impl Draw for Select {
  fn draw(&self) {
    println!("这是一个 select {}", self.id);
  }
}

enum Elements {
  Button(Button),
  Select(Select)
}

let elems: Vec<Elements> = vec![Elements::Button(Button { id: 1 }), Elements::Select(Select { id: 2 })];

for e in elems {
  match e {
    Elements::Button(button) => button.draw(),
    Elements::Select(select) => select.draw(),
  }
}

在取枚举值的时候,我们需要使用 match 做一个模式匹配,取出具体的元素实例。

最后值得提一下的就是 Rust 标准库里的枚举类型 Option ,定义如下:

rust 复制代码
enum Option<T> {
  Some(T),
  None,
}

标准库大量的场景都会使用,在上一篇文章中,介绍 Rust 基础类型也提到过,它就是用来替代 TS 中的 undefinednull,也就是空值的情况。

结构体

在大多数编程语言中,我们都需要一种更加高级的数据结构来抽象业务模型。在 TS 中,这种数据结构还不止一种,常用的有 interfaceRecord等,但是一般用 interface组织代码比较多:。

typescript 复制代码
interface User {
  id: number;
  name: string;
}

type User = Record<string, User>;

const user: User = {
  id: 1,
  name: 'atticus'
}

在 Rust 中,一般使用 struct 结构体去抽象高级的数据结构:

rust 复制代码
struct User {
	id: u64,
  name: String,
}

let user = User { id: 1, name: String::from("atticus") };

assert_eq!(user.id, 1);

结构体还有一些变体,例如元组结构体和单元结构体:

rust 复制代码
// 元组结构体
struct Color(i32, i32, i32);

// 单元结构体
struct AlwaysEqual;

let black = Color(0, 0, 0);

如果想给数据模型添加行为了?

在 TS 中,可以直接在 interface中跟随其它字段一起定义:

typescript 复制代码
interface User {
  id: number;
  name: string;
  log(): void;
}

const user: User = {
  id: 1,
  name: 'atticus',
  log() {
    console.log(this.name);
  }
}

在 Rust 中,结构体只是用来定义字段的,如果要添加行为,则需要结合特征(trait) 来实现:

rust 复制代码
trait LogAble {
  fn log(&self);      
}

struct User {
  id: u64,
  name: String,
}

impl LogAble for User {
  fn log(&self) {
    println!("{}", &self.name);    
  }    
}

let user = User { id: 1, name: String::from("atticus") };

user.log();

所以,在 Rust 的语言设计中,属性和行为的定义是分离的, 我个人也更加喜欢这种编程范式。

在 Rust 标准库中,有非常多的内置的特征实现,例如基础数据类型一般都实现了 **Copy 特征,**能使用 println 打印的数据类型一般实现了 Display 和 Debug 特征。

在类型系统中,有两种常见的类型,一种是 structural ,另一种是 nominal 。在 TS 类型系统中,它背后的设计是 structural:如果两个数据类型有一样的字段和行为即可以互相赋值给对方

typescript 复制代码
class Foo {
  method(input: string): number { return 0; }
}
class Bar {
  method(input: string): number { return 1; }
}

// TS 编译通过
let foo: Foo = new Bar();

interface User {
  name: string,
  id: number
}

interface User1 {
  name: string,
  id: number
}

let user: User = {
  id: 1,
  name: 'atticus1'
}

const user1: User1 = {
  id: 2,
  name: 'atticus2'
}
// TS 编译通过
user = user1;

而 Rust 中的类型设计是 nominal ,看一个例子:

rust 复制代码
struct User {
  name: String,
  id: u32
}

struct User1 {
  name: String,
  id: u32
}

// ops,编译报错:mismatched types expected `User`, found `User1`
let user: User = User1 { name: String::from("atticus"), id: 1 };

TS 编译后的产物为 JS,为了兼容 JS 中既可以使用 OOP,也可以通过一定的 utils 实现 FP 的方式编程,所以其类型系统的设计需要更加灵活。而 Rust 作为一门新语言,没有任何历史包袱,为了尽可能保证类型安全,内存安全,在类型设计上需要更加严格。

小结

对于 Rust 中的复合类型,比如字符串和数组,为了尽可能提升性能,降低内存的使用,会有字符串切片和长度不可变数组的设计。

Rust 中的枚举非常强大,枚举成员可以存储复杂的数据结构,在例如想要同一化类型的时候,可以借助强大的枚举实现。只是在取值的时候,我们需要借助 match 即模式匹配来实现。作为 Rust 中最受欢迎的特性,这种范式就是基本操作。

作为 Rust 中最灵活、组织代码最重要的结构体,Rust 类型系统的属性为 nominal ,对类型要求非常严格。但是要为抽象数据结构实现行为,还需要借助特征(trait) ,这种属性和行为定义分离的设计,是我个人比较喜欢的编程范式。

最后

行百里路半九十,要熟练掌握 Rust 这门语言,学习其数据类型只是一小步。Rust 中还有很多有意思的特性值得研究,例如借用检查、生命周期、智能指针、宏等。在学习 Rust 的过程中,对比自己掌握的编程语言是一件非常有乐趣的事情。

社区很多人会觉得 Rust 学习曲线比较陡峭,我个人只赞同一半,我个人觉得学习 Rust 并不是难在对于 Rust 的一些语言机制、新的语言概念的理解。它难在学习一门系统级语言,对开发者的计算机功底的全面考验,例如内存和数据存储、并发、数据结构等知识,这部分也是我个人在学习 Rust 过程中不断补强的能力。

最后推荐 Sunface 大佬的中文教程,在帮助我入门 Rust 时提供了很大的作用:Rust语言圣经(Rust Course)

Reference

相关推荐
待磨的钝刨1 小时前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
前端青山6 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
从兄7 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
清灵xmf8 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
薛一半9 小时前
PC端查看历史消息,鼠标向上滚动加载数据时页面停留在上次查看的位置
前端·javascript·vue.js
过期的H2O210 小时前
【H2O2|全栈】JS进阶知识(四)Ajax
开发语言·javascript·ajax
MarcoPage10 小时前
第十九课 Vue组件中的方法
前端·javascript·vue.js
你好龙卷风!!!10 小时前
vue3 怎么判断数据列是否包某一列名
前端·javascript·vue.js
shenweihong12 小时前
javascript实现md5算法(支持微信小程序),可分多次计算
javascript·算法·微信小程序
巧克力小猫猿12 小时前
基于ant组件库挑选框组件-封装滚动刷新的分页挑选框
前端·javascript·vue.js