Rust 宣称的安全、高性能、无畏并发这些特点,初次接触的时候都是感受不到的。
WSL 中,curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Windows 原生平台(32 位/64 位)。
安 GNU 工具链版本,不需要其他软件包。
安 MSVC 工具链,要先安装微软的 Visual Studio 依赖。
在终端输入:cargo new --bin helloworld
显示: Created binary (application) `helloworld` package
目录组织结构:
helloworld
├── Cargo.toml
└── src
└── main.rs
第一层是一个 src 目录和一个 Cargo.toml 工程配置文件。src 放源代码。
Cargo.toml文件内容:
[package]
name = "helloworld"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
Cargo.toml 中包含 package 等基本信息,项目名称、项目版本和采用的 Rust 版次。
Rust 3 年发行一个版次,有 2015、2018 和 2021 版次,最新是 2021 版次。
执行 rustc -V 查看 Rust 版本。
rustc 1.69.0 (84c898d65 2023-04-16)
src 下 main.rs 里的代码:
fn main() {
println!("Hello, world!");
}
在终端输出 "Hello, world!" 字符串。
用 cargo build 编译:
$ cargo build
Compiling helloworld v0.1.0 (/home/mike/works/classes/helloworld)
Finished dev [unoptimized + debuginfo] target(s) in 1.57s
用 cargo run 直接运行:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/helloworld`
Hello, world!
赋值语句
用 let 关键字定义变量及初始化。
let a: u32 = 1;
类型在变量名的后面。
Rust 保证你定义的变量在第一次使用之前一定被初始化过。
整数
isize 和 usize 的位数与具体 CPU 架构位数相同。CPU 64 位,是 64 位,CPU 32 位,是 32 位。
整数类型,可在写字面量的时候作为后缀,直接指定值的类型,如 let a = 10u32;
整数字面量的辅助写法--更清晰直观
十进制字面量 98_222,使用下划线按三位数字一组隔开
十六进制字面量 0xff,使用0x开头
8进制字面量 0o77,使用0o(小写字母o)开头
二进制字面量 0b1111_0000,使用0b开头,按4位数字一组隔开
字符的字节表示 b'A',对一个ASCII字符,在其前面加b前缀,直接得到此字符的ASCII码值
浮点数
f32 和 f64, 32 位浮点数类型和 64 位浮点数类型。
跟在字面量的后面,指定浮点数值的类型,如 let a = 10.0f32;
布尔类型为 bool,只两个值,true 和 false。
let a = true;
let b: bool = false;
字符类型是 char,值用单引号括起来。
fn main() {
let c = 'z';
let z: char = 'ℤ';
let heart_eyed_cat = '';
let t = '中';
}
char 类型存的是 Unicode 散列值。可以表达各种符号,如中文、emoji 符号等。
Rust 中,char 类型占 4 个字节。-->与 C 语言或其他某些语言中的 char 有很大不同。
字符串类型是 String。
字符串和字符的内部存储结构完全不同。
Rust 字符串对 Unicode 字符集有良好的支持。
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
注意,Rust 中的 String 不能通过下标去访问。
let hello = String::from("你好");
let a = hello[0]; // 你可能想把“你”字取出来,但实际上这样是错误的
String 存储的 Unicode 序列的 UTF8 编码(UTF8 编码是变长编码),取出一个字符的 UTF8 编码的第一个字节,没有意义。 Rust 对 String 禁止索引操作。
字符串字面量中的转义
与 C 一样,Rust 中转义符号是反斜杠 \ ,用来转义各种字符。
fn main() {
// 将""号进行转义
let byte_escape = "I'm saying \"Hello\"";
println!("{}", byte_escape);
// 分两行打印
let byte_escape = "I'm saying \n 你好";
println!("{}", byte_escape);
// Windows下的换行符
let byte_escape = "I'm saying \r\n 你好";
println!("{}", byte_escape);
// 打印出 \ 本身
let byte_escape = "I'm saying \\ Ok";
println!("{}", byte_escape);
// 强行在字符串后面加个0,与C语言的字符串一致。
let byte_escape = "I'm saying hello.\0";
println!("{}", byte_escape);
}
//
I'm saying "Hello"
I'm saying
你好
I'm saying
你好
I'm saying \ Ok
I'm saying hello. //这个是C中的字符串结束符,不会体现在打印信息上。
Rust 支持通过 \x 输入等值的 ASCII 字符,及通过 \u{} 输入等值的 Unicode 字符。
fn main() {
// 使用 \x 输入等值的ASCII字符(最高7位)
let byte_escape = "I'm saying hello \x7f";
println!("{}", byte_escape);
// 使用 \u{} 输入等值的Unicode字符(最高24位)
let byte_escape = "I'm saying hello \u{0065}";
println!("{}", byte_escape);
}
//
I'm saying hello
I'm saying hello e
注:字符串转义的详细知识点,参考 Tokens - The Rust Reference (rust-lang.org)。
禁止转义的字符串字面量
若不希望字符串被转义(想输出原始字面量),用 r"" 或 r#""# 把字符串字面量套起来。
fn main() {
// 字符串字面量前面加r,表示不转义
let raw_str = r"Escapes don't work here: \x3F \u{211D}";
println!("{}", raw_str);
// 这个字面量必须使用r##这种形式,因为我们希望在字符串字面量里面保留""
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);
// 如果遇到字面量里面有#号的情况,可以在r后面,加任意多的前后配对的#号,
// 只要能帮助Rust编译器识别就行
let longer_delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", longer_delimiter);
}
//
Escapes don't work here: \x3F \u{211D}
And then I said: "There is no escape!"
A string with "# in it. And even "##!
提示:Rust 中的字符串字面量都支持换行写,默认把换行符包含进去。
若字符串字面量中只需要 ASCII 字符集。
更紧凑的表示:字节串。用 b 开头,双引号括起来,如 b"this is a byte string"。
这时的类型已不是字符串,而是字节的数组[u8; N],N 为字节数。
fn main() {
// 字节串的类型是字节的数组,而不是字符串了
let bytestring: &[u8; 21] = b"this is a byte string";
println!("A byte string: {:?}", bytestring);
// 可以看看下面这串打印出什么
let escaped = b"\x52\x75\x73\x74 as bytes";
println!("Some escaped bytes: {:?}", escaped);
// 字节串与原始字面量结合使用
let raw_bytestring = br"\u{211D} is not escaped here";
println!("{:?}", raw_bytestring);
}
//
A byte string: [116, 104, 105, 115, 32, 105, 115, 32, 97, 32, 98, 121, 116, 101, 32, 115, 116, 114, 105, 110, 103]
Some escaped bytes: [82, 117, 115, 116, 32, 97, 115, 32, 98, 121, 116, 101, 115]
[92, 117, 123, 50, 49, 49, 68, 125, 32, 105, 115, 32, 110, 111, 116, 32, 101, 115, 99, 97, 112, 101, 100, 32, 104, 101, 114, 101]
字节串很有用,系统级编程或网络协议开发,经常用到。
array 类型,存储同一类型的多个值。
表示成[T; N],中括号括起来,分号隔开,分号前面是类型,分号后面是数组长度。
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [1, 2, 3, 4, 5];
}
Rust 中的数组是固定长度的,编译阶段就知道占用的字节数,运行阶段不能改变长度(尺寸)。
Rust 中区分固定尺寸数组和动态数组。
要适应不同的场合,要有足够的韧性,能在不同的场景中都达到最好的性能。
固定尺寸的数据类型可以直接放栈上,创建和回收都比在堆上动态分配的动态数组性能要好。
能否在编译期计算出某个数据类型在运行过程中占用内存空间的大小,很重要,Rust 的类型系统就按这个对类型进行分类。
数组常用于开辟一个固定大小的 Buffer(缓冲区),用来接收 IO 输入输出等。
也常用已知元素个数的字面量集合来初始化,如一年有 12 个月。
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
数组的访问,用下标索引。
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
let b = a[0];
println!("{}", b)
}
// 输出
1
下标索引越界:
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
let b = a[5];
println!("{}", b)
}
会提示:
Compiling playground v0.0.1 (/playground)
error: this operation will panic at runtime
--> src/main.rs:3:13
|
3 | let b = a[5];
| ^^^^ index out of bounds: the length is 5 but the index is 5
这个操作会在运行的时候崩溃。因为数组的长度是确定的,Rust 在编译时就分析并提取了这个数组类型占用空间长度的信息,直接阻止了越界访问。
动态数组类型是 Vec(Vector),向量。
存储同一类型的多个值,容量可在程序运行的过程中动态地扩大或缩小。
fn main() {
let v: Vec = Vec::new();
let v = vec![1, 2, 3];
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
动态数组用下标进行索引访问。
fn main() {
let s1 = String::from("superman 1");
let s2 = String::from("superman 2");
let s3 = String::from("superman 3");
let s4 = String::from("superman 4");
let v = vec![s1, s2, s3, s4];
println!("{:?}", v[0]);
}
// 输出
"superman 1"
下标越界:
fn main() {
let s1 = String::from("superman 1");
let s2 = String::from("superman 2");
let s3 = String::from("superman 3");
let s4 = String::from("superman 4");
let v = vec![s1, s2, s3, s4];
// 这里下标访问越界了
println!("{:?}", v[4]);
}
编译无提示,运行后:
Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 0.62s
Running `target/debug/playground`
thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', src/main.rs:9:22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
代码正确通过了编译,但运行的时候出错了,导致了主线程的崩溃。
array 的越界访问能在编译阶段检查出来,而 Vec 的越界访问不能在编译阶段检查出来
存储 Key-Value 映射关系,哈希表类型为 HashMap。
对一个 HashMap 结构,所有Key 是同一种类型,所有Value 是同一种类型。Key 和 Value 的类型不需要相同。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
哈希表能从一个键索引到一个值,应用场景非常广泛。
复合类型可以包含多种基础类型,是一种将类型进行有效组织的方式,提供了一级一级搭建更高层类型的能力。Rust 中的复合类型包括元组、结构体、枚举等。
固定(元素)长度的列表,每个元素类型可以不一样。小括号括起来,元素间用逗号隔开。
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
元组元素的访问:
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
// 元组使用.运算符访问其元素,下标从0开始,注意语法
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
元组,用于函数的返回值,可把多个值捆绑在一起,一次性返回。
当没有任何元素的时候,元组退化成 (),叫 unit 类型,是 Rust 中一个非常重要的基础类型和值,unit 类型唯一的值实例就是 (),与其类型本身的表示相同。函数没有返回值,默认返回 unit 值。
由若干字段组成,每个字段的类型可以不一样。用 struct 关键字来定义结构体。
定义 User 类型。
struct User {
active: bool,
username: String,
email: String,
age: u64,
}
结构体类型的实例化。
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
age: 1,
};
}
用 enum 关键字定义枚举类型。
enum IpAddrKind {
V4,
V6,
}
枚举类型里面的选项叫做此枚举的变体(variants)。变体是其所属枚举类型的一部分。
用变体进行枚举类型的实例化:
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
与结构体不同:
枚举类型是 Rust 中最强大的复合类型,枚举像一个载体,可以携带任何类型。
Rust 用 if else 来构造分支。
fn main() {
let number = 6;
// 判断数字number能被4,3,2中的哪一个数字整除
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
与 C 系语言不同,Rust 中 if 后面的条件表达式不推荐用()包裹起来,多余的语法噪音。
注意,if else 支持表达式返回。
fn main() {
let x = 1;
// 在这里,if else 返回了值101
let y = if x == 0 {
// 代码块结尾最后一句不加分号,表示把值返回回去
100
} else {
// 代码块结尾最后一句不加分号,表示把值返回回去
101
};
println!("y is {}", y);
}
上面代码,类似于 C 语言中的三目运算符。
三种循环: loop、while、for。
loop :无条件(无限)循环。
fn main() {
let mut counter = 0;
// 这里,接收从循环体中返回的值,对result进行初始化
let result = loop {
counter += 1;
if counter == 10 {
// 使用break跳出循环,同时带一个返回值回去
break counter * 2;
}
};
println!("The result is {result}");
}
//The result is 20
这种返回一个值到外面对一个变量初始化的方式,是 Rust 中的习惯用法,代码更紧凑。
while :条件判断循环。当条件为真,执行循环体里的代码。
和 if 语句一样,while 后面的条件表达式不推荐用()包裹起来:
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
for :Rust 中,基本只用于迭代器(数组,动态数组等)的遍历。
Rust 中没有 C 语言那种 for 循环风格的语法支持(不好的设计)。
对数组进行遍历。
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
上面代码对动态数组 Vec 的遍历阻止了越界的可能性,比用下标索引的方式访问更安全。
对于循环的场景,Rust 可便捷生成遍历区间: ..(两个点)。
fn main() {
// 左闭右开区间
for number in 1..4 {
println!("{number}");
}
println!("--");
// 左闭右闭区间
for number in 1..=4 {
println!("{number}");
}
println!("--");
// 反向
for number in (1..4).rev() {
println!("{number}");
}
}
// 输出
1
2
3
--
1
2
3
4
--
3
2
1
试试打印字符。
fn main() {
for ch in 'a'..='z' {
println!("{ch}");
}
}
// 输出:
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z
Rust 的函数、闭包和模块,用于封装和复用代码。
基本的代码复用方法。用 fn 关键字定义函数:
fn print_a_b(a: i32, b: char) {
println!("The value of a b is: {a}{b}");
}
fn main() {
print_a_b(5, 'h');
}
函数定义时的参数叫作形式参数(形参),函数调用时传入的参数值叫做实际参数(实参)。
函数的调用要与函数的签名(函数名、参数个数、参数类型、参数顺序、返回类型)一致,实参和形参要匹配。
各种编程语言,都是以函数作为基本单元来组织栈上的内存分配和回收的,这个基本的内存单元就是栈帧(frame)。
另一种风格的函数。用两个竖线符号 || 定义,不是用 fn () 来定义。
// 标准的函数定义
fn add_one_v1 (x: u32) -> u32 { x + 1 }
// 闭包的定义,请注意形式对比
let add_one_v2 = |x: u32| -> u32 { x + 1 };
// 闭包的定义2,省略了类型标注
let add_one_v3 = |x| { x + 1 };
// 闭包的定义3,花括号也省略了
let add_one_v4 = |x| x + 1 ;
注:可参考完整代码链接
闭包与函数的一个显著不同:闭包可以捕获函数中的局部变量为我所用,而函数不行。
示例中的闭包 add_v2 捕获了 main 函数中的局部变量 a 来使用,但函数 add_v1 就没有这个能力。
fn main() {
let a = 10u32; // 局部变量
fn add_v1 (x: u32) -> u32 { x + a } // 定义一个内部函数
let add_v2 = |x: u32| -> u32 { x + a }; // 定义一个闭包
let result1 = add_v1(20); // 调用函数
let result2 = add_v2(20); // 调用闭包
println!("{}", result2);
}
编译出错,并提示。
error[E0434]: can't capture dynamic environment in a fn item
--> src/main.rs:4:40
|
4 | fn add_v1 (x: u32) -> u32 { x + a } // 定义一个内部函数
| ^
|
= help: use the `|| { ... }` closure form instead
闭包之所以能够省略类型参数等信息,主要是其定义在某个函数体内部,从闭包的内容和上下文环境中能够分析出来那些类型信息。
代码量多了,分成不同的文件模块书写。
分文件和目录组织代码,知识点在于目录的组织结构:
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden // 子目录
│ └── vegetables.rs
├── garden.rs // 与子目录同名的.rs文件,表示这个模块的入口
└── main.rs
第五行到第七行组成 garden 模块,在 garden.rs 中,使用 mod vegetables; 导入 vegetables 子模块。
在 main.rs 中,用同样的方式导入 garden 模块。
mod garden;
整个代码结构就这样一层一层地组织起来了。
另一种文件的组织形式来自 2015 版,也很常见。
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden // 子目录
│ └── mod.rs // 子目录中有一个固定文件名 mod.rs,表示这个模块的入口
│ └── vegetables.rs
└── main.rs
同上,由第五行到第七行代码组成 garden 模块,在 main.rs 中导入它使用。
你可以在本地创建文件,来体会两种不同目录组织形式的区别。(模块同名.rs / 固定mod.rs)
Rust 语言自带单元测试和集成测试方案。
示例,在 src/lib.rs 或 src/main.rs 下有一段代码。
fn foo() -> u32 { 10u32 }
#[cfg(test)] // 这里配置测试模块
mod tests {
use crate::foo;
#[test] // 具体的单元测试用例
fn it_works() {
let result = foo(); // 调用被测试的函数或功能
assert_eq!(result, 10u32); // 断言
}
}
在项目目录下运行 cargo test,会输出类似如下结果。
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Rust 自带测试方案,非常方便地写测试用例,统一了社区的测试设计规范。
Cargo.toml 是 Rust 语言包和依赖管理器 Cargo 的配置文件,由官方定义约定。
写 Rust 代码基本都会按照这种约定来使用它,对所在工程进行配置。
这里面其实包含的知识点非常多,后面实战的部分,会详细解释用到的特性。
包依赖的复杂度。 npm 依赖黑洞?就是 Node.js 的包依赖太多太琐碎了。包依赖,本身就很复杂,是软件工程固有的复杂性。
幸运的是,Cargo 工具搞定了包依赖相关方方面面的麻烦事( C++ 社区羡慕不已)。为了应对这种复杂性,Cargo 工具的提供了非常多的特性,配置起来也相对比较复杂。
Rust 的基本的语法结构和数据类型。
一个比较重要的指标:是否能在编译期计算出某个数据类型在运行过程中占用的内存空间的大小。能计算出,称为固定尺寸的数据类型;不能计算出,称为不固定尺寸的数据类型,或动态数据类型。
Rust 要尽量在编译期间多做一些事情,帮我们做安全性的检查。而在编译期只有能计算出内存尺寸的数据类型,才能被更详尽地去分析和检查,就是这个原理。
推荐几个Rust的VsCode插件
Rust Syntax:语法高亮
crates:分析项目依赖
Even Better Toml:项目配置管理
Rust Test Lens:Rust快速测试
IDE推荐一波 RustRover ,jetbrains 新出的 Rust 专用 IDE
Rust 闭包:编写更强大、更灵活的代码 - Levix GPT总结的闭包
答:虽然 Rust 不像 JavaScript 那样具有动态的通用数字类型,但可以通过使用 num crate 和 Rust 的强类型系统来实现类似的通用数字操作。
关于char的unicode散列值,gpt4的说明是: Unicode 是一种字符集(Character Set),用于对世界上大多数语言的字符进行编码。不同于 ASCII 码仅包括了基本的英文字符和控制字符,Unicode 意在包括世界上所有的字符,包括字母、符号、表情符号(emoji)等。
在 Unicode 标准中,每个字符都有一个唯一的标识符,通常称为代码点(Code Point)。这些代码点是用整数表示的,一般用十六进制的形式来展示。例如,英文字母 "A" 的 Unicode 代码点是 U+0041,而中文字符 "中" 的 Unicode 代码点是 U+4E2D。
散列值一般用于描述数据结构中用于快速查找的数值标识,但在这里(Rust 的 char 类型存的是 Unicode "散列值"),这种说法不太准确。实际上,Rust 的 char 类型存储的是 Unicode 代码点。