🦀 目标:
掌握函数定义、参数、返回值、表达式与语句的区别,理解所有权的三条规则与 move 语义。
一、核心概念
- 函数定义
rust
fn add(a: i32, b: i32) -> i32 {
a + b
}
要点:
· fn 定义函数,参数必须标注类型
· -> 标注返回类型
· 函数体最后一个表达式就是返回值,不加分号
· 加了分号就变成语句,返回 (),类型不匹配会报错
调用:
rust
fn main() {
let result = add(3, 5);
println!("{}", result); // 8
}
- 表达式 vs 语句
这是 Rust 的核心概念之一。
语句:执行一个动作,不返回值。以分号结尾。
表达式:计算出一个值。不加分号。
rust
fn main() {
let x = 5; // 语句
let y = {
let z = 3;
z + 1 // 表达式,块的值是 4
}; // 整个 let 是语句
println!("{}", y); // 4
}
注意 z + 1 没有分号,它是块的返回值。如果写成 z + 1;,块返回 (),y 就是 ()。
- 所有权的三条规则
这是 Rust 最核心、最独特的概念。
规则一:每个值都有一个所有者(owner)。
规则二:同一时刻,只有一个所有者。
规则三:所有者离开作用域时,值被自动释放(drop)。
rust
fn main() {
let s = String::from("hello"); // s 是所有者
println!("{}", s);
} // s 离开作用域,内存自动释放
- 移动(move)
当把一个堆上的值赋给另一个变量,或传给函数时,所有权会移动。
rust
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有权从 s1 移动到 s2
// println!("{}", s1); // ❌ s1 已失效
println!("{}", s2); // ✅
}
为什么?因为 String 在堆上分配内存。如果 s1 和 s2 都指向同一块内存,离开作用域时会被释放两次(double free)。Rust 直接让 s1 失效,避免这个问题。
- 克隆(clone)
想保留两份独立数据,用 .clone():
rust
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝,堆上数据复制一份
println!("{} {}", s1, s2); // 两个都可用
}
- 栈上类型:Copy
简单类型(整数、浮点、布尔、char、以及只含这些的元组)实现了 Copy trait,赋值时是复制而不是移动:
rust
fn main() {
let x = 5;
let y = x; // 复制,x 仍然可用
println!("{} {}", x, y); // 5 5
}
- 所有权与函数
传参和返回都会涉及移动:
rust
fn takes_ownership(s: String) {
println!("{}", s);
} // s 在这里被 drop
fn gives_ownership() -> String {
String::from("hello") // 返回,所有权移出
}
fn main() {
let s = String::from("world");
takes_ownership(s);
// println!("{}", s); // ❌ s 已被移动
let s2 = gives_ownership(); // 接收所有权
println!("{}", s2);
}
二、多元化练习题(含答案与解读)
🟢 练习 1(基础):写一个加法函数
要求:定义 fn add(a: i32, b: i32) -> i32,在 main 中调用并打印 3 + 5 的结果。
参考答案
rust
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
println!("{}", add(3, 5)); // 8
}
解读:a + b 后面不能加分号,否则返回 (),类型不匹配会报错 expected i32, found ()。
🟢 练习 2(表达式):预测输出
rust
fn main() {
let x = {
let a = 2;
let b = 3;
a * b
};
println!("{}", x);
}
参考答案
输出 6。
解读:花括号块的值是最后一个表达式 a * b,等于 6。如果写成 a * b;(带分号),块返回 (),x 就是 (),编译报错。
🟡 练习 3(找出错误):语句 vs 表达式
以下代码无法编译,请指出原因并修复。
rust
fn square(x: i32) -> i32 {
x * x;
}
fn main() {
println!("{}", square(4));
}
参考答案
rust
fn square(x: i32) -> i32 {
x * x // 去掉分号
}
解读:x * x; 是语句,值为 ()。函数声明返回 i32,但实际返回 (),类型不匹配。编译器的报错会提示 "help: remove this semicolon"。这是新手最常犯的错误之一。
🟡 练习 4(move 语义):预测能否编译
rust
fn main() {
let s1 = String::from("hi");
let s2 = s1;
println!("{}", s1);
}
参考答案
无法编译。
解读:let s2 = s1; 发生移动,s1 失效。报错 borrow of moved value: s1。修复方式有两种:改成 s1.clone(),或者后面不再用 s1。
🟡 练习 5(Copy vs Move):判断对错
以下两段代码,哪段能编译?
A:
rust
fn main() {
let x = 5;
let y = x;
println!("{} {}", x, y);
}
B:
rust
fn main() {
let s = String::from("hi");
let t = s;
println!("{} {}", s, t);
}
参考答案
A 能编译,B 不能。
解读:i32 实现了 Copy,赋值是复制,x 仍可用。String 没有实现 Copy,赋值是移动,s 失效。判断标准:数据是否在堆上。栈上的简单类型通常 Copy,堆上的类型通常 Move。
🔴 练习 6(函数传参):修复代码
rust
fn print_len(s: String) {
println!("{}", s.len());
}
fn main() {
let s = String::from("hello");
print_len(s);
println!("{}", s); // 这里想再用 s
}
参考答案
方案一:让函数借用而不是拿走所有权(第 5 课内容):
rust
fn print_len(s: &String) {
println!("{}", s.len());
}
fn main() {
let s = String::from("hello");
print_len(&s);
println!("{}", s); // ✅
}
方案二:传 clone:
rust
fn main() {
let s = String::from("hello");
print_len(s.clone());
println!("{}", s); // ✅
}
解读:方案一更好,不复制数据。这里提前接触 & 引用,下节课会详细讲。方案二有性能开销,只适合数据小或确实需要独立副本的场景。
🔴 练习 7(综合):计算并返回所有权
要求:
· 写函数 fn make_greeting(name: &str) -> String,返回 "Hello, {name}!"
· 在 main 中调用,接收返回的 String,打印它
参考答案
rust
fn make_greeting(name: &str) -> String {
let mut s = String::from("Hello, ");
s.push_str(name);
s.push('!');
s
}
fn main() {
let greeting = make_greeting("Rust");
println!("{}", greeting); // Hello, Rust!
}
解读:函数内创建 String,最后作为表达式返回,所有权移出给 main 中的 greeting。name: &str 是借用字符串切片,不获取所有权,也不复制数据。这是 Rust 中很常见的函数签名模式。
🔵 思考题 1:为什么 Rust 需要所有权?
参考答案
为了在不引入垃圾回收(GC)的前提下保证内存安全。
其他语言的做法:C/C++ 手动管理,容易内存泄漏、double free、悬垂指针;Java/Go 有 GC,运行时开销大、有停顿。
Rust 的所有权是编译期的静态检查:每个值有唯一所有者,离开作用域自动释放,移动后旧变量失效。这样既没有 GC 的运行时开销,又避免了手动管理的错误。
解读:这是 Rust "零成本抽象"和"内存安全"两大卖点的基石。所有权是 Rust 学习曲线的核心难点,但一旦理解,后面会越来越顺。
🔵 思考题 2:clone 和 Copy 有什么区别?
参考答案
Copy 是隐式的、按位复制,通常用于栈上的简单类型。赋值、传参时自动发生,不消耗额外心智。
clone 是显式的、可能深拷贝,用于堆上数据。需要你手动调用 .clone(),明确表示"这里要复制一份"。
rust
let x = 5;
let y = x; // Copy,隐式
let s = String::from("hi");
let t = s.clone(); // clone,显式
解读:Rust 用这种区分让你对"昂贵操作"保持警觉。复制一个整数很便宜,可以隐式;复制一个堆上的大字符串可能很贵,必须显式写出来,提醒你这里可能有性能开销。
🔵 思考题 3:函数返回 String 会不会有性能问题?
参考答案
不会。
Rust 有返回值优化(RVO)和移动语义。返回 String 时,堆上的数据不会被复制,只是把指向堆的指针、长度、容量这"三要素"移出函数。真正的堆内存在原地不动。
rust
fn make() -> String {
String::from("hello") // 堆数据不复制,只移出所有权
}
解读:在很多语言里"返回大对象"是性能陷阱,Rust 里不用太担心。编译器会尽量消除拷贝。这也是为什么 Rust 里 String 可以放心地按值返回,而不会像 C++ 老代码那样到处传指针。
三、知识点总结(第4课)
函数:fn name(param: Type) -> RetType { body },参数必须标类型。
返回值:函数体最后一个表达式即返回值,不加分号。加分号变语句,返回 ()。
表达式 vs 语句:表达式有值,语句没有。块 { } 的值是最后一个表达式。
所有权三规则:每个值有唯一所有者;同一时刻只有一个所有者;所有者离开作用域时值被释放。
移动:堆上类型赋值或传参时所有权转移,旧变量失效。
克隆:.clone() 显式深拷贝,保留两份独立数据。
Copy:栈上简单类型(整数、浮点、bool、char)赋值是复制,不移动。
函数与所有权:传参可能移动,返回时所有权移出。&str 借用不获取所有权。
四、下一课预告
第5课:引用与借用
· 引用 &T 与可变引用 &mut T
· 借用规则:同一时刻要么一个可变引用,要么多个不可变引用
· 悬垂引用与生命周期入门
· 切片 &T 与 &str
📌 本课心法:
所有权不是限制,是契约。它让编译器在编译期就知道每块内存的归宿,从而不需要 GC,也不会 double free。理解移动,就理解了 Rust 的一半。