1. 一道题开始
1.1 为什么无法运行
1 2 3 4
| let mut data = vec![1, 2, 3]; let first_ref = &data[0]; data.push(4); println!("{}", first_ref);
|
这个代码为什么编译不成功?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
|
Rust 的编译器(管家)执行一条铁律:你不能在存在 “ 只读借用 “(&data
)的期间,对原始数据进行 “ 可变操作 “(data.push
)。这就是 Rust 如何在编译时就彻底杜绝 “ 悬垂指针 “ 这类内存安全问题的。
1.2 let mut data = vec![1, 2, 3]
我们把它拆开来看,就像庖丁解牛一样:
let
: 这是 Rust 中声明一个变量的关键字。mut
: 这是 “mutable”(可变的)的缩写。在 Rust 中,所有变量默认都是不可变的(immutable)。如果你想让一个变量的值可以被修改,就必须明确地使用 mut
关键字。这是一种安全设计,鼓励你编写更清晰、副作用更少的代码。let x = 5; x = 6;
// 编译错误!因为 x 不可变。let mut y = 5; y = 6;
// 正确!因为 y 被声明为可变。
data
: 你为变量起的名字。vec![]
: 这个看起来有点奇怪。vec!
不是一个函数,而是一个宏(Macro)。- 什么是宏? 你可以把它理解为 “ 代码的代码 “。它是一种在编译器把你的代码翻译成机器码之前运行的特殊代码段,它的作用是根据你给的模式生成你想要的代码。
!
符号就是用来区分宏和普通函数的。vec![1, 2, 3]
这个宏的作用就是方便地创建一个向量(Vector),也就是一个可以在运行时增长或缩小的动态数组,并用 1, 2, 3
来初始化它。它最终生成的代码类似于 Vec::new()
然后再把元素一个个 push
进去,但用宏写起来方便多了。
所以,这整行代码的意思是:” 声明一个名为 data
的可变变量,它的类型是一个整数向量,并初始化内容为 1, 2, 3。”
2. 语法基础
2.1 变量声明与数据类型
- 声明:
let
(不可变) 和 let mut
(可变)。 - 基本类型:
i32
(32 位有符号整数), u64
(64 位无符号整数), f64
(64 位浮点数), bool
(true/false), char
(字符)。Rust 通常能自动推断类型。
1 2
| let x = 10; let name: &str = "BotGem";
|
- 复合类型 - 元组 (Tuple): 固定长度,可以包含不同类型的元素。
1 2
| let user_info: (String, i32) = (String::from("Alice"), 28); let user_name = user_info.0;
|
- 复合类型 - 数组 (Array): 固定长度,所有元素必须是相同类型。
1
| let numbers: [i32; 3] = [1, 2, 3];
|
2.2 函数 (Functions)
- 使用
fn
关键字定义。 - 参数和返回值的类型必须标注。
->
符号用于指定返回值类型。- Rust 是一个 “ 基于表达式 “ 的语言。函数的最后一个表达式的值就是返回值,可以省略
return
关键字和分号。
1 2 3 4
| fn add_one(x: i32) -> i32 { x + 1 }
|
2.3 控制流 (Control Flow)
1 2 3 4
| let a = [10, 20, 30]; for element in a.iter() { println!("the value is: {}", element); }
|
2.4 结构体 (Structs) - 自定义数据结构
- 类似于 C 的
struct
或其他语言中没有方法的 class
。用来把相关的数据打包在一起。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct User { username: String, email: String, active: bool, }
let mut user1 = User { email: String::from("some@example.com"), username: String::from("someuser123"), active: true, };
user1.email = String::from("another@example.com");
|
2.5 枚举 (Enums) 与 match
- Rust 的超能力
- 枚举允许你定义一个可以是若干种不同变体之一的类型。
- Rust 的枚举特别强大,每个变体都可以关联数据。
1 2 3 4 5
| enum WebEvent { PageLoad, KeyPress(char), Click { x: i64, y: i64 }, }
|
match
是一个控制流运算符,像一个超强的 switch
。它必须穷尽(exhaustive)所有可能的情况,编译器会帮你检查。这消除了大量潜在的 bug。
1 2 3 4 5 6 7 8 9
| fn inspect(event: WebEvent) { match event { WebEvent::PageLoad => println!("page loaded"), WebEvent::KeyPress(c) => println!("pressed '{}'.", c), WebEvent::Click { x, y } => { println!("clicked at x={}, y={}.", x, y); }, } }
|
match
和 Enum
的组合是 Rust 中表达复杂逻辑、保证程序健壮性的核心工具之一。
3. 字符串
字符串怎么定义
从根本上说,Rust 的字符串处理围绕着两种主要的类型:
String
:一个拥有所有权的、可变的、在堆上分配内存的字符串。&str
(读作 “string slice”,字符串切片):一个借用的、不可变的、指向字符串数据的视图。
我们可以用一个形象的比喻来理解:
String
就像你拥有的一本实体书。它是你的,你可以在上面写字、撕掉几页或者增加新页。它有自己明确的存放位置(在内存的 “ 堆 “ 上)。&str
就像这本书某一章节的一张复印件。它只是对原始内容的一个 “ 查阅凭证 “。你无法通过涂改复印件来修改原始的书。而且,这张复印件可以来自你的私有书籍(String
),也可以来自图书馆的永久馆藏(程序里写死的字符串)。
1. String
:所有者
- 它是什么:
String
是一个拥有其内容的数据结构。它的内容存储在堆(Heap)上,这意味着它的大小可以在程序运行时增长或缩小。例如,你可以往一个 String
后面追加更多文本。 - 核心特征:
- 拥有所有权:它有唯一的 “ 所有者 “。当所有者离开其作用域时,
String
会被自动销毁,其占用的内存也会被释放。 - 可变:如果用
mut
声明,你就可以修改它的内容。 - 堆分配:它很灵活,可以改变大小。
- 如何创建:
String::from("你的文本")
:这是最常见、最明确的方式。它清楚地表达了 “ 请从这段文本数据创建一个新的 String
对象 “。"你的文本".to_string()
:这个方法和 String::from
的作用完全一样。使用哪个纯粹是个人偏好。
2. &str
:借用者 (字符串切片)
- 它是什么:
&str
是一个引用,它指向一段由 _ 别人 _ 拥有的 UTF-8 文本序列。它被称为 “ 切片 “,因为它常常只是一个 String
中 “ 一部分 “ 的视图。它实际上是一个 “ 胖指针 “,因为它包含两条信息:一个指向数据起始位置的指针,以及这个切片的长度。 - 核心特征:
- 借用:它不拥有它所指向的数据,只是一个临时的视图。
- 不可变(默认):你不能通过
&str
来修改它指向的底层数据。 - 大小固定:
&str
本身的大小是固定的(一个指针 + 一个长度),它指向的数据序列长度也是固定的。
- 它指向哪里?
- 指向字符串字面量 (String Literal):当你写
let name: &str = "BotGem";
时,"BotGem"
这段文本是一个字符串字面量。它被直接编译进你的程序二进制文件的只读数据区。name
这个变量就是一个指向那个内存地址的引用。这种方式非常快,在程序运行时完全没有内存分配的开销。 - 指向一个
String
:你可以创建一个 &str
,让它指向一个 String
的全部或一部分。
1 2 3 4 5
| let s: String = String::from("hello world");
let hello: &str = &s[0..5]; let world: &str = &s[6..11]; let whole_thing: &str = &s;
|
场景 | 推荐类型 | 为什么? |
---|
在 struct 结构体中存储数据 | String | 结构体应该是自包含的,应该拥有它自己的数据。如果你用 &str ,你就必须处理复杂的生命周期问题,这意味着该结构体的存活时间不能超过它所借用的数据的存活时间。 |
作为函数参数 | &str | 这是黄金法则。使用 &str 会让你的函数更灵活。一个接受 &str 的函数,既可以接收一个拥有的 String (&my_string ),也可以接收一个字符串字面量 ("hello" ),或者另一个切片。这是一种无所有权的 “ 出借 “。 |
从函数返回值 | 通常是 String | 如果函数创建了一个新的字符串(例如,通过拼接两个字符串),它必须返回一个拥有的 String ,这样才能把所有权转移给调用者。 |
…特例:返回输入字符串的一部分 | &str (带生命周期) | 如果一个函数只是在它的输入中查找并返回一个子串,它可以返回一个 &str 切片。这更高效(没有新的内存分配),但需要使用生命周期来向编译器证明,返回的切片不会比原始数据活得更久。(这是一个更高级的话题)。 |
需要被修改的变量 | let mut s = String::… | 只有 String 是可以增长和修改的。 |
1 2 3 4 5 6 7 8 9 10 11 12 13
| fn print_message(message: &str) { println!("{}", message); }
fn main() { let owned_string = String::from("我是一个拥有的字符串。"); let literal_string = "我是一个字符串字面量。";
print_message(&owned_string); print_message(literal_string); }
|
4. 引用和可变变量
在 C++ 里,引用的主要目的之一就是修改值,但在 Rust 中,简单的 & 引用似乎不行。
首先,Rust 的引用分为两种,这与 C++ 的单一引用类型有本质区别:Rust 的引用体系:共享 Vs 独占
- 共享引用 (Shared Reference):
&T
- 权限:只读。你只能通过它来读取数据,不能修改。
- 特点:可以同时存在多个。就像很多人可以同时阅读同一本书。
- 这就是
&name
。它是一个共享引用,因此默认不可修改值。
- 可变引用 (Mutable Reference):
&mut T
- 权限:可读可写。你可以通过它来修改数据。
- 特点:具有独占性。在它的作用域内,不能存在任何其他对该数据的引用(无论是共享的还是可变的)。就像一个人要修改一本书,他必须先把书拿走,不允许任何其他人同时阅读或修改。
在 Rust 中,” 可变性 “ 有两个层面,必须将它们分开理解:🔥
- 变量绑定的可变性 (
let mut
):决定一个变量是否可以被重新赋值,指向另一个东西。 - 数据的可变性 (
&mut
):决定是否可以通过引用去修改所指向的数据。
1 2 3 4 5 6 7
| let mut data = 10; let ref_b = &mut data;
*ref_b = 20; println!("修改后的 data: {}", data);
|
1 2 3 4 5 6 7 8 9 10 11
| let mut data1 = 10; let mut data2 = 50;
let mut ref_d = &mut data1;
*ref_d = 20; println!("修改后的 data1: {}", data1);
ref_d = &mut data2; *ref_d = 60; println!("修改后的 data2: {}", data2);
|
组合 | 变量能否重新赋值? | 能否通过引用修改数据? | 示例 |
---|
let x = &y; | 否 ❌ | 否 ❌ | let x = &y; |
let mut x = &y; | 是 ✅ | 否 ❌ | x = &z; |
let x = &mut y; | 否 ❌ | 是 ✅ | *x = 100; |
let mut x = &mut y; | 是 ✅ | 是 ✅ | x = &mut z; *x = 100; |
上面为什么都有 &
直接回答:& 并不是必须的。 & 是一个独立的操作符,它的作用是 “ 创建一个引用 “。
我们有三种东西需要区分:
- 值 (Value):内存中实际的数据,比如数字
10
,字符串 String::from("abc")
。 - 变量绑定 (Variable Binding):给 “ 值 “ 起的一个名字,比如
let x = …
。 - 引用 (Reference):一个指向某个 “ 值 “ 的 “ 指针 “ 或 “ 地址 “,它本身也是一种值。
世界一:直接操作 “ 值 “ (没有 &
的世界)
1 2 3 4 5 6 7 8 9
| let x = 10;
let mut y = 10; println!("y 的初始值: {}", y); y = 20; println!("y 的新值: {}", y);
|
世界二:创建并使用 “ 引用 “ (有 &
的世界)
现在,我们引入 &
操作符。&
的作用是:” 别动那个值,给我它的地址,我要创建一个引用。”
mut 在 & 后面,是修饰所创建的引用的类型,决定了这个引用是 “ 只读权限 “ 还是 “ 可写权限 “。
1 2 3 4 5 6 7 8 9 10 11
| let mut data = 10;
let ref_a = &data;
let ref_b = &mut data; *ref_b = 30;
|
- 如果你想直接操作值,就不需要
&
。 - 如果你想创建一个引用(比如,为了避免数据所有权的转移,或者只是想 “ 借用 “ 一下数据),那么你就必须使用
&
,因为 &
就是 “ 创建引用 “ 的语法。
如何修改一个变量
如果你想以任何方式修改一个变量的值(无论是直接修改还是通过可变引用),这个变量的原始绑定都必须是 let mut
。
为什么?这源于 Rust 的安全承诺。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| let mut s: String = String::from("hello world");
println!("原始的 s 是: '{}'", s);
{ let hello_mut: &mut str = &mut s[0..5];
hello_mut.make_ascii_uppercase(); }
println!("修改后的 s 是: '{}'", s);
|
5. 注意事项
- 拥抱编译器:Rust 编译器非常严格,但它的错误信息极其友好和详尽。把它当作你的导师,而不是敌人。它指出的问题几乎都是潜在的严重 Bug。
- 默认不可变:始终优先使用不可变变量和引用 (
let x
和 &x
)。只在确实需要修改时才使用 mut
关键字。这会引导你写出更清晰、更易推理的代码。 - 优先使用迭代器:Rust 的迭代器是 “ 零成本抽象 “ 的典范。使用 for item in my_vec.iter() 而不是手写 for i in 0..my_vec.len(),代码更优雅,性能也同样出色。
- 善用 Option 和 Result:Rust 没有 null。
它使用 Option<T> (表示 Some(T) 或 None) 来处理可能缺失的值,使用 Result<T, E> (表示 Ok(T) 或 Err(E))
来处理可能失败的操作。这强迫你在编译时就处理所有异常情况。 - 不要过度使用 .clone(): 当所有权问题让你头疼时,一个简单的 “ 修复 “ 方法就是到处复制数据 (.clone()),但过度使用会带来性能损失。问问自己:” 我真的需要一份全新的数据副本,还是只需要 ‘ 借用 ‘ 一下?” 学习何时使用引用(&)是关键。
- 混淆 String 和 &str:
- String:一个在堆上分配、可增长的、拥有所有权的 UTF-8 字符串。(像 C++ 的 std::string)
- &str (字符串切片):一个对 String 或字符串字面量某个部分的不可变借用。(像 C 的 const char*)
- 记住,函数参数优先使用 &str,因为它更灵活,可以接受 String 和字符串字面量两种类型的输入,且避免了所有权转移。