Skip to content

2.Rust 基础篇

学习篇(滑到最后看目录学习):三角兽新系列!拥抱未来语言Rust (qq.com)

github:reganzm/hug_rust: 拥抱rust (github.com)

1.创建 Rust 项目

安装完开发环境后,我们就可以创建项目开发了

1.随便合适的地方,比如桌面,创建项目

shift + 鼠标右键 ,选择 此处打开命令行 ,运行以下命令:

bash
cargo new hello-rust

2.使用 VSCode 打开项目

此时,文件夹目录是这样的,Cargo 已经帮我们创建好默认项目了,还创建了个 git 的本地仓库,还有一些配置文件,以后会说到。你只要知道 src/main.rs 为编写应用代码的地方。

image-20240323235356139

3.运行项目

新建项目已经为我们创建好了默认的 Hello World 项目,我们直接在 vscode 的终端中运行 cargo run 命令,就可以看到系统输出了 Hello World。

bash
cargo run

4.编写 Hello-Rust

Cargo.toml 文件是一个管理项目配置的文件,包括项目依赖等相关配置,我们在后面会详细介绍该文件,现在我们来添加一个依赖,

image-20240323235528105

然后在 vscode 终端运行

bash
cargo build

终端就会有以下输出,可以看到 cargo 会自动为我们添加依赖,并且安装好依赖所依赖的依赖(直接绕口令)

image-20240323235548628

接下来就在 src/main.rs 中写入以下内容

rust
use ferris_says::say; // from the previous step
use std::io::{stdout, BufWriter};
fn main() {
    let stdout = stdout();
    let message = String::from("Hello fellow Rustaceans!");
    let width = message.chars().count();

    let mut writer = BufWriter::new(stdout.lock());
    say(message.as_bytes(), width, &mut writer).unwrap();
}

然后在终端中再次执行

bash
cargo run

就会看到以下结果

image-20240323235653125

这个案例我们随后会进行更加细致的讲解,并且随着后面的学习,对其中的语法理解会更加的深刻。

Rustaceans 是对学习 Rust 者的称呼。

其他待总结

5 分钟带你入门 Rust 编程语言官方推荐工具 Rustup (qq.com)

安装 - Cargo 手册 中文版 (rustwiki.org)

精通 Rust 的包管理工具 Cargo (qq.com)

Rust开发革新:热重载技术实战指南与应用技巧 (qq.com)

[Rust 极简教程:最快上手 Rust 编程! (qq.com)](https://mp.weixin.qq.com/s/722roFliQeQnFNUy8VdNVA)

[【Rust 基础篇】Rust FFI:连接Rust与其他编程语言的桥梁 - 掘金 (juejin.cn)](https://juejin.cn/post/7261270802645221436)

使用Rust构建IP嗅探器 (qq.com)

【Rust 基础篇】Rust 属性宏:定制你的代码 - 掘金 (juejin.cn)

【一起学 Rust】Rust 的 Hello Rust 详细解析_rust hello world-CSDN 博客

【一起学 Rust】Rust 包管理工具 Cargo 初步了解_cargo.lock-CSDN 博客

【一起学 Rust】Rust 学习前准备——注释和格式化输出_rust 中英文对齐打印-CSDN 博客

【一起学 Rust | 基础篇】Rust 基础——变量和数据类型_rust 变量指定数据类型-CSDN 博客

【一起学 Rust | 基础篇】rust 函数与流程控制详解_rust else-CSDN 博客

【一起学 Rust | 基础篇 | rust 新特性】Rust 1.65.0——泛型关联类型、let-else 语句_rust let else-CSDN 博客

前端开发者视角入门Rust

前端开发者上手

前端开发者的 Rust 尝鲜: Rust 的第一印象 - 掘金 (juejin.cn)

rust 上手很难?搞懂这些知识,前端开发能快速成为 rust 高手 (qq.com)

盘点 Rust 中的那些天才构思 - 掘金 (juejin.cn)

参考文章:写给想学 Rust 的前端同学 - 掘金 (juejin.cn)

前言

Rust 可能正在逐渐渗透前端的方方面面,所以作为一个前端究竟有没有必要学习 Rust 呢?我认为,还是看个人的精力吧,有那个精力多学一点没有坏处,没那个精力不学也没有影响。本篇不是讨论该不该学 Rust,而是将 Rust 大概是一个什么样的语言展现给可能在观望的小伙伴,并以一个前端的视角来看看 Rust 究竟和前端有什么不一样。

本篇也是我在认真阅读了Rust 程序设计语言 - Rust 程序设计语言 简体中文版几遍以后,才敢下笔做一些总结,因能力有限,错误之处还望大家及时指出。

我会从一个语言层面的几个方面来分析 Rust 究竟和 JavaScript 以及 TypeScript 这样的语言的不同之处,以及相似之处,并且希望能给想要学习 Rust 的同学一些语言的梗概,也给前端学习 JS 的同学一些新的理解。

Rust VS JavaScript

属性RustJavaScript
编译器rustcv8
包管理工具cargonpm、yarn、pnpm、cnpm
第三方依赖注册表crates.ionpmjs.com
垃圾回收

数据类型

Rust 的数据类型同样分为基础类型和复杂类型,主要包括以下几类:

  • 基础类型:包括整型、浮点型、布尔型、字符型
  • 复杂类型:元组、数组以及其他复合类型

这点和传统的强类型语言基本是一致的,但是 Rust 也拥有不同的地方。

Rust 声明变量的方法竟然和 JS 出奇的一致,并且很多方面也是类似 JS 或 TS 的写法:

rust
// 声明不可变变量
let a = 1;

// 声明可变变量
// 如果没有 mut 关键字,修改变量会导致报错
let mut b = 2;
b = 3;

// 重复声明变量,会发生遮蔽,即覆盖原有变量
// 此时之前声明的 a 变量无效了
let a = 2;

// 声明元组,近似理解为 TS 中的元组
let c = ('a', 2);
// 元组可以被解构,也是类似 JS 的解构
// 此时变量a就是'a',变量b则是2
let (a, b) = c;

// 声明数组
let arr = [1,2,3,4];
// 还可以有很多方式
// 表示arr1是一个包含两个元素,每个元素的值都是3
let arr1 = [3;2];

流程控制

流程控制则和大多数语言一样,包括 if-else、while 循环、for 循环,不同的是,还多了一个 loop 循环:

rust
// if 后面没有括号,并且后面的值类型只能是 bool 类型
// 并没有 JS 中类型转换的能力,这点其实和其他语言是类似的
if 1 > 2 {
    
} else {

}

// while 循环也是一样,后面没有括号
while 1 > 2 {}

// for 循环有点类似 JS 中的 for-in 循环
let arr = [1,2,3,4];
for i in arr {
    // 这里遍历的值都是值本身,并没有索引
    // 打印1,2,3,4
}

// loop 循环则是 while 不带条件的循环:
loop {
    // 代码块中的代码会不停的循环
    // 退出循环可以使用 break 或者 continue
}

结构体和枚举

Rust 的结构体类似于 C 语言的结构体,这也是 JS 所没有类型。而枚举类型则在 TS 中是有的,但是 Rust 的枚举功能远远多于 TS 中的枚举。

rust
// 普通结构体
struct Person {
    name: String,
    age: u8,
}

// 元组结构体
struct Color(i32, i32, i32);

// 单元结构体
struct Unit;

// 声明结构体
let p = Person {
    name: "qiugu",
    age: 22
};

// 结构体也可以解构
// name 为 "qiugu",age 为 22
let Person { name, age } = p;

枚举是 Rust 中非常重要的数据类型。Rust 中并没有空指针的概念,于是 Rust 通过枚举类型来模拟空的概念:

rust
// 这是 rust 标准库内置的枚举类型 Option
enum Option<T> {
    Some(T),
    None
}

Rust 中很多方法返回的都是 Option 类型,通过处理 Option 类型来拿到具体的值,如果是 None,则表示空的概念。关于如何匹配枚举类型的值,这点后面会说到。

集合

Rust 中常用的集合类型包括以下几种:

  • string
  • vector
  • hashmap

这些类型在 Rust 中都是复杂类型,其中在 JS 常用的基本类型 string,在这里其实非常复杂,并且其他语言中的 string 类型都比较复杂,只是 JS 做了很多工作,简化了 string 的使用。

rust
// 声明可变 String 类型
let mut s = String::from("i am a coder");
// 修改 String
s.push_str("abc");
// i am a code abc

// 注意:这并不是 String 类型,而是字符串切片类型slice
let s1 = "i am str";
// 这样才是 String 类型
let s2 = String::from(s1);

// String是复杂类型
let s1 = String::from("i am a coder");
let s2 = s1;
// 打印s1会报错,因为s1的所有权已经被转移
// 这也证明了String是一个复杂类型,因为基础类型会复制一个值,而复杂类型只是复制了引用
println!("{}", s1); // 报错

类型系统

可以发现上面所有的示例代码并没有类型注解,原因是因为 Rust 可以自动推导类型(这是不是和 TS 有点像)。

可以在 VSCode 中安装 Rust 的插件,就可以看到变量对应的类型:

image-20240424180035025

Rust 的类型系统除了可以自动推导变量类型,也存在泛型的类型复用能力,可以近似理解为 TS 中的泛型概念。

我们知道 TS 中存在 interface 类型复用类型,以及定义类型的结构包含哪些属性方法。Rust 中同样也存在类似的概念 trait:

rust
// 定义 trait
trait Greet {
    fn hello(&self) -> String;
}

是不是和 interface 非常相似!

如何实现这个 trait 呢?逻辑也是类似的,Rust 中也需要对象才能实现 trait,Rust 中的对象其实就是结构体类型:

rust
// 定义一个单元结构体(什么属性都不包括的结构体)
struct Person;

// 实现 Greet trait
impl Greet for Person {
    // 先不用看方法如何声明,后面会提到
    fn hello(&self) -> String {
        // 注意:语句后面没有分号,表示它是一个表达式,而不是语句
        String::from("hello, man!")
    }
}

// 使用
fn main() {
    let p = Person;
    // 执行 trait 上的方法
    p.hello(); // hello, main!
}

现在是不是对 Rust 更熟悉一点了!

接下来就是 Rust 独有的生命周期概念,它也是泛型的一部分。生命周期又涉及到了引用的概念。引用在 JS 中同样存在,只是和 Rust 引用并不一样:

rust
let x = 5;
// 可以引用任意类型的变量
let r = &5;

println!("x: {}, r: {}", x, r); // x: 5, r: 5

稍微改写一下上面的代码:

rust
let r;
{
    let x = 5;
    r = &x;
    // rust 作用域也存在块级作用域
    // 并且当变量退出该作用域时,引用该变量的其他值,这里就是r也会失效
    // 这样会导致变量r变成一个空引用
}
println!("r: {}", r);

我们可以在编译时就能发现上面代码的问题:

image-20240424180130660

翻译一下,就是变量x的生命周期不如变量r的生命周期长,因为当x退出块级作用域时,变量r还依然存在,而生命周期就是为了确保引用总是有效。上面的例子可以通过作用域直接看出来变量生命周期的长短,但是以下情况无法直接看出来变量的生命周期:

rust
// 返回x、y中的大值
// 注意x、y都是引用类型,并且返回的也是引用类型
// 编译器无法确定返回类型的引用的生命周期是和x一样长,还是和y一样长,或者和x、y都一样长
// 所以编译无法通过
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这个时候就需要使用生命周期注解来告诉编译器这些引用之间的生命周期关系是怎样的:

rust
// 生命周期注解就是在引用符合后面加上'a,表示该引用生命周期为'a
// 变量x、y,以及返回类型的生命周期都是一样的,说明它们的引用的生命周期也都是一样长
// 这样编译器就可以确定引用都是有效的,编译可以通过
fn longest(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

函数、方法及闭包

在 JS 中函数和方法可以看作是一个意思,但是 Rust 中的函数和方法却是不一样的。

Rust 中的函数就是我们在上一节看到的 longest 函数,指定了参数、以及参数类型,并且指定了返回值类型,还是以上面的函数举例:

rust
// 函数参数必须指定其类型,这和声明变量时自动推导类型表现不一样
// 原因可能因为对于一个函数来说,需要暴露给调用者使用,因此需要明确参数和输出参数的类型
fn longest(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

可以看到上面的函数并没有指定像 return 这样的关键字来返回值,而是将返回值包裹在大括号中了。这是因为 Rust 中块级作用域的最后一个表达式就作为其返回值。注意表达式是不带分号结尾的,带上了分号就变成了语句,而不是表达式,这点在前面已经提到过了。

rust
// x 的值就是 3
let x = {
    let a = 1 + 2;
    a
};

// 函数的返回值就是最后一个表达式的结果,也就是 a+b 的结果
fn foo(a: i32, b: i32) -> i32 {
    let c = a * b;
    let d = c + 1;
    a + b
}

而方法和函数不一样的地方在于,方法是依附于对象存在的,调用函数时,直接函数名称后面接括号就可以调用了,但是方法则需要使用对象来调用,比如,前面提到的 trait 实现,其就是一个方法:

rust
impl Greet for Person {
    // 注意:方法的第一个参数都是 self,表示对象自身,这里并没有用到 self
    fn hello(&self) -> String {
        String::from("hello, man!") 
    } 
}

// 调用方法
p.hello();

// 调用函数
foo();

最后 Rust 中也存在闭包的概念。闭包也是一种函数,只是闭包写法和普通函数不一样,并且可以捕获上下文中的变量:

rust
let a = 1;
// 使用“||”表示参数列表,同普通函数的小括号
// 如果有参数就写在双竖线中间
// 闭包的参数和返回值类型可以不写,编译器会自动推断,但是一旦确定类型,就不能再传其他类型
let b = ||{
    a + 1
};
b();

闭包一般是作为函数或方法的参数,因为它可以捕获上下文中的变量,这点和 JS 是有异曲同工之妙的。

模块化

作为一门强类型语言,模块化是其与生俱来的功能,这点不像 JS,过了很多年才有模块化。

Rust 的模块化,了解几个关键词就能大概掌握了。

rust
// a.rs
// 使用 pub 导出结构体
pub struct Person;

// 导出函数
pub fn foo() -> String {
    String::from('i am a coder')
}

// 没有使用pub导出的数据不能被外部使用
enum Color(u8, u8, u8);

// main.rs
// 声明a模块
// a就是a.rs的文件名称
mod a;
// 使用use指定使用a模块中的哪些内容
// 注意:只能使用a模块中使用pub关键字导出的
use a::Person;
// 也可以写完整的导入路径
use crate::a::Person;
// 导入多个
use a::{Person, foo}
// 或者*匹配所有导出的成员
use a::*;

let p = Person;

内存模型

Rust 的内存模型外观上和 JS 是相似的,比如 Rust 的基本类型存储在栈上,复杂类型则存储在堆上,但是本质上还是区别比较大的。

rust
// 基本类型
let a = 1;
let b = a;

// 复杂类型
let s = String::from("hello");
let s1 = s;

以上代码我们使用一张图来展示其执行过程:

image-20240424180217874

重点就是堆内存的分配,当 s 复制给 s1 的时候,并不会像 JS 那样存在两个“指针”同时指向存储 hello 字符串的内存,而是 s 的“指针”失效了,也就是同一时刻,只能有一个指向该内存的“指针”,这个“指针”并不是真正意义上的指针,在 Rust 中称它为所有者,所有者的规则则称为所有权,于是有这样关于所有权的结论:

  • Rust 中的每个值都有一个所有者(也就是上面提到的“指针”)。
  • 值在任何时刻有且只有一个所有者(赋值以后,s就失效了,只能有一个)。
  • 当所有者(变量)离开作用域,这个值将被丢弃(和 JS 类型,变量离开作用域则失效,但是有所不同)。

关于第三点,在上面生命周期的例子中解释过 Rust 作用域相关规则,当变量离开作用域时,变量的值将会被销毁,此时如果存在引用该值的变量,则会报错:生命周期长度问题。因为 Rust 不允许引用一个被销毁的值,这点和 JS 是不一样的(JS 中存在变量引用了某个值,会导致该值不会被释放,直到引用该值的变量全部退出作用域才会被销毁)。

上面的引用以及所有权还可以这么解释:

rust
let x = 5;
// 表示 y 借用了 x 的值
let y = &x;
// 注意:被借用的值不能再次被赋值
x += 1; // 这么做会报错

let s = String::from("hello");
// 表示 s 的所有权移动到了 s1 上,s 就失去了所有权
let s1 = s;

借用就是创建一个引用,比如例子的变量 y。移动则表示一个变量的所有权移动到另外一个变量上,那么失去所有权的变量就不能被使用了。按照这么一套规则,就能在不需要垃圾回收器的情况下,安全的使用内存了,这也是 Rust 的特色之一。

引用同样也有一套规则:

  • 任意给定时间,要么只能有一个可变引用(防止多个可变引用,导致同一时间数据被改变,产生了数据竞争),要么只能有多个不可变引用(不能同时存在可变引用和不可变引用,原因也是数据竞争)。
  • 引用必须总是有效的(这就是上面引用的值失效时,会报错的原因)。

所有权规则和借用规则都是可以打破的,这就涉及到更复杂的内容,它们不是我今天所要说的内容,所以就暂时忽略了。

其他

除了这些语言通用的内容,Rust 还包括像并发智能指针等功能,这些对前端来说可能涉及到知识盲区了,所以也就不在这里继续说了。

总结

以上就是 Rust 语言的入门级内容了,相比于 JS 来说,Rust 确实更加复杂和繁琐,当然复杂繁琐的同时也带来了更强大的运行机制,比如所有权规则。除此之外,Rust 的内存模型也给我们展示了一个不同于 JS 的垃圾回收的一种内存管理机制。所以这也是无论什么语言,最终都会殊途同归,变化的是语言的写法规范,不变的是内存永远是有限的。

Aquascope:可视化揭秘 Rust 程序的编译与运行时

参考网址

图片

引言

作为一名 Rust 开发者,你是否曾对 Rust 独特的所有权机制和借用检查感到好奇?是否希望能更直观地理解 Rust 编译器是如何“思考”你的代码的?今天,我要给大家介绍一款神奇的工具——Aquascope,它能生成 Rust 程序的交互式可视化图表,帮助我们深入洞察 Rust 的编译期和运行时行为。

创作背景

Aquascope 是由 Brown 大学 Cognitive Engineering Lab 开发的一款研究性质的软件工具。它旨在帮助 Rust 开发者、教育工作者和编程语言研究人员更好地理解 Rust 的内在机制。通过生成直观的交互式可视化图表,Aquascope 让 Rust 的编译期借用检查和运行时行为变得"可见",方便我们学习和分析。

主要特性

  • 生成展示 Rust 借用检查器如何“思考”程序的交互式可视化图表
  • 生成展示 Rust 程序实际执行情况的交互式可视化图表
  • 提供 mdBook 预处理器,可将 Aquascope 图表嵌入 mdBook 中
  • 支持通过 Web 界面本地运行 Aquascope 游乐场

快速上手

想快速体验 Aquascope 的威力吗?我们可以直接在 Aquascope Playground 中尝试。访问以下网址:

https://cognitive-engineering-lab.github.io/aquascope/

你会看到一个在线的代码编辑器,可以在里面编写 Rust 代码。比如我们写下这样一段简单的代码:

rust
fn main() {
    let x = String::from("Hello");  // 创建一个字符串变量 x
    foo(x);  // 将 x 传递给函数 foo
    println!("{}", x);  // 尝试打印 x
}

fn foo(s: String) {  // 函数 foo 接收一个 String 类型的参数 s
    println!("{}", s);
}

然后点击 “Interpret“ 按钮,Aquascope 就会开始工作,生成该程序运行时的可视化图表。我们可以通过下方的控制面板调整细节,比如查看每一步的状态。

同时你会注意到,“Boundaries” 和 “Permissions” 按钮在这段代码下是灰色不可点击的。这提示我们这段代码没有通过 Rust 的借用检查。将光标移动到第 4 行,就会看到错误提示:

rust
error[E0382]: borrow of moved value: `x`
 --> src/lib.rs:4:20
  |
2 |     let x = String::from("Hello");
  |         - move occurs because `x` has type `String`, which does not implement the `Copy` trait
3 |     foo(x);
  |         - value moved here
4 |     println!("{}", x);
  |                    ^ value borrowed here after move
For more information about this error, try `rustc --explain E0382`.

Rust 独特的所有权机制在起作用。当我们将 x 传递给 foo 函数时,x 的所有权就转移给了函数参数 s,之后 x 就不再有效。第 4 行尝试再次使用 x,就会触发错误。

我们对代码做一些修改:

rust
fn main() {
    let x = String::from("Hello");
    foo(&x); // 传递 x 的不可变引用给函数 foo
    println!("{}", x);
}

fn foo(s: &String) {  // 函数 foo 接收一个 &String 类型的参数
    println!("{}", s);
}

再次点击 “Interpret”,“Boundaries” 和 “Permissions” 按钮就变成可点击的了。我们可以尝试点击它们,生成展示借用检查信息的可视化图表,进一步研究一下 Rust 所有权机制是如何工作的。

是不是感觉很神奇?我们只需要简单几步,就能在 Aquascope Playground 中学习和探索 Rust 的编译期和运行时行为。快去试试看吧,相信你一定能从 Aquascope 生动直观的可视化中获得新的认识和灵感!

总结

Aquascope 是一款非常有助于学习和理解 Rust 的实用工具。通过可视化的方式,它揭示了 Rust 编译期借用检查和运行时行为的奥秘,让 Rust 初学者能更轻松地掌握 Rust 的独特机制。

作为研究性质的软件,Aquascope 目前仍在活跃开发中。欢迎大家关注该项目,为其贡献代码和反馈问题。让我们一起推动 Rust 学习和研究的发展!

Released under the MIT License.