控制流与所有权:Rust 的核心骨架
好,我们接着往下走。上一节我们把 Rust 的基础类型和变量摸了个大概,今天要聊的这几个概念,可以说是 Rust 的「脊梁骨」——控制流、所有权、引用和切片。嗯,我当年刚接触 Rust 时,所有权这块确实卡了好一阵子,但一旦想通了,后面就豁然开朗了。
控制流:不止是 if/else
控制流嘛,说白了就是让代码按你的想法走。Rust 的控制流跟 C 系语言很像,但有几个小细节值得注意。
if/else 是表达式
在 Rust 里,if 是个表达式,不是语句。这意味着它能返回值。我刚开始写的时候老忘记这点,习惯性地用 let x = if condition { a } else { b }; 这种写法,后来发现真香。
let score = 85;
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else {
"C"
};
println!("你的等级是:{}", grade); // 输出 B
注意:if 的每个分支返回的类型必须一致。你不能一个分支返回 i32,另一个返回 &str。编译器会直接报错,我当初就被这个坑过。
loop:无限循环,但可以 break 带值
loop 是 Rust 里最纯粹的循环——它永远不停止,除非你主动 break。但有个很实用的特性:break 可以返回值。
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // 返回 20
}
};
println!("结果:{}", result);
我个人习惯在需要「反复尝试直到成功」的场景用 loop,比如网络重连、等待资源就绪。配合 break 返回值,代码会非常干净。
while:条件循环,老朋友了
while 跟其他语言差不多,但 Rust 的 while let 模式匹配是个亮点。这个我们后面讲模式匹配时会细说,先记住有这么个东西。
let mut n = 3;
while n > 0 {
println!("{}!", n);
n -= 1;
}
println!("发射!");
for:遍历集合的首选
在 Rust 里,for 循环几乎总是比 while 更安全、更高效。它直接遍历迭代器,不用担心索引越界。
let arr = [10, 20, 30, 40];
for element in arr.iter() {
println!("值:{}", element);
}
// 带索引的遍历
for (index, value) in arr.iter().enumerate() {
println!("arr[{}] = {}", index, value);
}
小技巧:如果你需要遍历一个范围,用 for i in 0..5 或 for i in 0..=5。前者是左闭右开 [0,5),后者是闭区间 [0,5]。
所有权:Rust 的「独门绝技」
好了,重头戏来了。所有权是 Rust 最核心的概念,也是让很多人头疼的地方。但说白了,它解决的就是「内存谁管」的问题。
在 C 里,你 malloc 了就得 free,忘了就内存泄漏。在 Java 里,有 GC 帮你收垃圾,但性能有损耗。Rust 走了第三条路:通过编译时的所有权规则,既保证内存安全,又没有运行时开销。
三条铁律
- 每个值在 Rust 中都有一个所有者(owner)。
- 同一时刻,一个值只能有一个所有者。
- 当所有者离开作用域,值会被自动释放。
嗯,这三条看起来简单,但实际写代码时很容易踩坑。我举个例子:
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权被移动到 s2
// println!("{}", s1); // 编译错误!s1 已经失效了
println!("{}", s2); // 正常
为什么会这样?因为 String 是堆上分配的数据,如果让 s1 和 s2 都指向同一块内存,那当它们离开作用域时,就会发生「双重释放」——这是 C 里常见的 bug。Rust 干脆禁止了这种操作,直接把 s1 的所有权「移动」给了 s2。
关键理解:移动(move)不是浅拷贝,也不是深拷贝。它是所有权的转移。原来的变量就「失效」了,不能再使用。
Copy 类型 vs 移动类型
你可能会问:那整数、浮点数这些呢?它们也会被移动吗?
不会。像 i32、f64、bool、char 这些类型,实现了 Copy trait。它们存在栈上,拷贝成本极低,所以赋值时是「复制」而不是「移动」。
let x = 5;
let y = x; // 复制,x 仍然有效
println!("x = {}, y = {}", x, y); // 没问题
简单记:栈上数据 Copy,堆上数据 Move。当然,你也可以手动为自己的类型实现 Copy,但前提是它所有字段都是 Copy 的。
引用与借用:不转移所有权,也能用数据
所有权规则这么严格,那函数调用时怎么办?每次传参都把所有权交出去,用完还得还回来?那也太麻烦了。
这时候就需要「引用」了。引用允许你「借用」一个值,而不获取它的所有权。说白了,就是「我看看,不拿走」。
不可变引用 &
fn calculate_length(s: &String) -> usize {
s.len()
} // 这里 s 离开作用域,但因为它是引用,不会释放 String
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("'{}' 的长度是 {}", s1, len); // s1 仍然可用
看到没?&s1 创建了一个指向 s1 的引用,但所有权还在 s1 手里。函数结束后,引用失效,但 s1 安然无恙。
可变引用 &mut
如果你想通过引用修改数据,就需要可变引用:
fn change(s: &mut String) {
s.push_str(", world");
}
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // 输出 "hello, world"
可变引用有一个重要限制:同一时刻,只能有一个可变引用,或者多个不可变引用,但不能同时存在可变和不可变引用。这是 Rust 防止数据竞争的手段。我曾经在并发代码里违反了这个规则,编译器直接报错,帮我避免了一个潜在的 bug。
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
// let r3 = &mut s; // 编译错误!不能同时有不可变和可变引用
println!("{} {}", r1, r2);
切片:不拥有数据,但能引用一段连续区域
切片(Slice)是另一种「不拥有所有权」的类型。它让你可以引用集合中的一段连续元素,而不是整个集合。
字符串切片 &str
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
注意,切片是 &str 类型,它是对 String 中某段数据的引用。切片本身不拥有数据,所以不会导致所有权问题。
我个人习惯用字符串字面量 "hello" 时,它其实就是一个 &str 切片。所以 let s = "hello"; 中的 s 类型是 &str,不是 String。
数组切片
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4]; // 类型是 &[i32],内容是 [2, 3, 4]
println!("{:?}", slice);
切片在函数参数中特别有用。比如你想写一个函数,接收任意长度的数组的一部分:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
let s = String::from("hello world");
let word = first_word(&s);
println!("第一个单词:{}", word);
这个函数接收一个字符串切片,返回第一个单词的切片。它不拥有任何数据,只是借用了输入的一部分。这就是 Rust 的优雅之处——安全、高效、零开销。
本章知识体系
下面这张图帮你理清今天讲的核心概念之间的关系:
好了,这一节的内容确实有点多。控制流是基本功,所有权是 Rust 的灵魂,引用和切片是日常开发最常用的工具。我建议你多写几段代码,亲手感受一下「移动」和「借用」的区别。遇到编译错误别慌,Rust 的编译器提示非常友好,它会告诉你哪里违反了规则。
记住一句话:所有权不是限制,而是解放。它让你在写代码时就能发现内存问题,而不是等到运行时崩溃。