控制流与所有权: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..5for i in 0..=5。前者是左闭右开 [0,5),后者是闭区间 [0,5]。

所有权:Rust 的「独门绝技」

好了,重头戏来了。所有权是 Rust 最核心的概念,也是让很多人头疼的地方。但说白了,它解决的就是「内存谁管」的问题。

在 C 里,你 malloc 了就得 free,忘了就内存泄漏。在 Java 里,有 GC 帮你收垃圾,但性能有损耗。Rust 走了第三条路:通过编译时的所有权规则,既保证内存安全,又没有运行时开销。

三条铁律

  1. 每个值在 Rust 中都有一个所有者(owner)。
  2. 同一时刻,一个值只能有一个所有者
  3. 当所有者离开作用域,值会被自动释放

嗯,这三条看起来简单,但实际写代码时很容易踩坑。我举个例子:

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 移动类型

你可能会问:那整数、浮点数这些呢?它们也会被移动吗?

不会。像 i32f64boolchar 这些类型,实现了 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 核心概念关系图 控制流 • if/else(表达式) • loop(break 带值) • while / for 所有权 • 每个值只有一个所有者 • 移动(Move) • Copy 类型 vs 堆类型 引用与借用 • &T(不可变引用) • &mut T(可变引用) • 借用规则 切片 • &str(字符串切片) • &[T](数组切片) • 不拥有所有权 依赖 延伸 特殊引用 所有权是基础,引用和切片都是它的延伸应用

好了,这一节的内容确实有点多。控制流是基本功,所有权是 Rust 的灵魂,引用和切片是日常开发最常用的工具。我建议你多写几段代码,亲手感受一下「移动」和「借用」的区别。遇到编译错误别慌,Rust 的编译器提示非常友好,它会告诉你哪里违反了规则。

记住一句话:所有权不是限制,而是解放。它让你在写代码时就能发现内存问题,而不是等到运行时崩溃。


专注资料整理