learning-rust/part3/所有权练习题.md

663 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Rust 所有权练习题
> 建议先手动写出每道题的答案,再运行代码验证。
## 一、基础题:判断正误
判断以下代码能否通过编译。如果可以,说明原因;如果不能,指出错误原因。
### 题目 1-1
```rust
fn main() {
let s = String::from("hello");
let t = s;
println!("{}", s);
}
```
> 答: 不能
### 题目 1-2
```rust
fn main() {
let x = 42;
let y = x;
println!("x = {}, y = {}", x, y);
}
```
> 答: 能
### 题目 1-3
```rust
fn main() {
let s = String::from("rust");
let t = s.clone();
println!("s = {}, t = {}", s, t);
}
```
> 答: 能
### 题目 1-4
```rust
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
let s3 = &s1;
println!("{} {} {}", s1, s2, s3);
}
```
> 答: 能
### 题目 1-5
```rust
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
```
> 答: 不能
### 题目 1-6
```rust
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
println!("{}, {}, {}", r1, r2, r3);
}
```
> **[批注] 原答"能"错误 —— 该代码无法通过编译。**
> r1、r2 是不可变引用r3 是可变引用。Rust 不允许在已有不可变借用的作用域内再创建可变借用。`println!` 同时使用了三者,借用范围重叠,编译器报错:
> `error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable`
---
## 二、填空题:补充代码
补全下列代码使其能通过编译。
### 题目 2-1转移所有权
```rust
fn main() {
let s = String::from("rustacean");
takes_ownership(________); // 填空,使 s 的所有权进入函数
// println!("{}", s); // 此行若取消注释会报错
}
fn takes_ownership(s: String) {
println!("{}", s);
}
```
> 答: s
### 题目 2-2借用的生命周期
```rust
fn main() {
let s = String::from("hello");
let len = get_length(________); // 填空:传递引用取得长度
println!("'{}' 的长度是 {}", s, len);
}
fn get_length(s: &String) -> usize {
s.len()
}
```
> 答: &s
### 题目 2-3可变引用
```rust
fn main() {
let ________ s = String::from("hello"); // 填空:声明可变变量
append_world(________); // 填空:传递可变引用
println!("{}", s); // 期望输出hello, world!
}
fn append_world(s: &mut String) {
s.push_str(", world!");
}
```
> 答: mut, &mut s
### 题目 2-4作用域技巧
```rust
fn main() {
let mut s = String::from("hello");
{ // 进入新作用域
let r1 = ________; // 填空:创建不可变引用
println!("{}", r1);
} // r1 离开作用域
let r2 = ________; // 填空:现在可以创建可变引用了
r2.push_str(" world");
println!("{}", r2);
}
```
> 答: &s, &mut s
---
## 三、找出并修复错误
以下每段代码都有编译错误,请指出错误并写出修正后的代码。
### 题目 3-1
```rust
fn main() {
let s1 = String::from("hello");
let s2 = s1;
print_both(s1, s2);
}
fn print_both(a: String, b: String) {
println!("{} and {}", a, b);
}
```
> **[批注] 原错误分析有误:** 错误原因是 `let s2 = s1;` 将 s1 的所有权移动给了 s2此后 s1 已失效,不能继续用于 `print_both(s1, s2)`。原答写的修复代码语法也有问题(`fn print_both(a: String, &b: String)` 不是合法签> 名。正确修复clone 一份。
```rust
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
print_both(s1, s2);
}
fn print_both(a: String, b: String) {
println!("{} and {}", a, b);
}
```
### 题目 3-2
```rust
fn main() {
let s = dangle();
println!("{}", s);
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
```
> **[批注] 原错误分析不精确。** 真正原因:`dangle()` 返回 `&String`,但引用指向的是函数内的局部变量 `s`。函数结束时 `s` 被释放返回的引用变成悬垂引用dangling reference。Rust 编译器在编译期阻止了这种行为。修复方法:直接返回 `String` 让所有权移出。
```rust
fn main() {
let s = dangle();
println!("{}", s);
}
fn dangle() -> String {
let s = String::from("hello");
s
}
```
### 题目 3-3
```rust
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
let r2 = &mut s;
r2.push_str("!");
println!("{}", r1);
println!("{}", r2);
}
```
> **[批注] 原分析"同时只能存在一个可变引用"基本正确,但未给出修复代码。** 由于 r1 在 `println!("{}", r1)` 之后不再使用Rust 的 NLLNon-Lexical Lifetimes可在该点结束 r1 的借用。修复:将 println! 移到 r2 创建之前。
```rust
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
println!("{}", r1); // r1 在此之后不再使用NLL 结束其借用
let r2 = &mut s; // 此时 r1 已失效,可以创建新的可变引用
r2.push_str("!");
println!("{}", r2);
}
```
### 题目 3-4
```rust
fn main() {
let s = String::from("hello world");
let first = &s[0..5];
s.clear(); // 清空字符串
println!("first = {}", first);
}
```
> **[批注] 原分析不精确,原修复代码 `let first = s[0..5];` 仍是切片引用,与 `s.clear()` 的 `&mut self` 冲突,同样无法编译。** 真正原因:`first` 是对 `s` 内容的不可变引用,而 `clear()` 需要 `&mut self`(可变引用),两者不能同时存在。正确修复:先使用 first再 clear。
```rust
fn main() {
let s = String::from("hello world");
let first = &s[0..5];
println!("first = {}", first); // 先使用 first
let mut s = s; // 重新绑定为可变的
s.clear(); // 此时 first 的借用已结束
}
```
---
## 四、编程题
### 题目 4-1计算单词数量
编写一个函数 `count_words(s: &str) -> usize`,计算字符串中的单词数量(以空格分隔)。
```rust
fn count_words(s: &str) -> usize {
// 你的代码
return s.split_whitespace().count();
}
fn main() {
let s = String::from("hello world rust");
let count = count_words(&s);
println!("'{}' 有 {} 个单词", s, count); // 期望输出3
// 验证 s 的所有权未被取走
println!("原始字符串仍然可用: {}", s);
}
```
### 题目 4-2获取首尾单词
编写函数 `first_and_last(s: &str) -> (&str, &str)`,返回字符串的首单词和尾单词。
```rust
fn first_and_last(s: &str) -> (&str, &str) {
// 你的代码
let words: Vec<&str> = s.split_whitespace().collect();
return (words[0], words[words.len() - 1]);
}
fn main() {
let s = String::from("Rust is a systems programming language");
let (first, last) = first_and_last(&s);
println!("首单词: '{}', 尾单词: '{}'", first, last);
// 期望输出:首单词: 'Rust', 尾单词: 'language'
println!("原字符串: {}", s);
}
```
### 题目 4-3字符串处理
编写一个函数,接收一个字符串,返回一个新的字符串,将其中的每个单词首字母大写,其余字母小写。
```rust
fn title_case(s: &str) -> String {
// 你的代码
// 提示:可以使用 split_whitespace、to_uppercase、to_lowercase、collect 等方法
> **[批注] 原代码未完成。** collect words 但没有做大小写转换。补全如下:
```rust
fn title_case(s: &str) -> String {
s.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn main() {
let s = String::from("hello WORLD rust");
let result = title_case(&s);
println!("输入: {}", s);
println!("输出: {}", result);
// 期望输出Hello World Rust
// 验证 s 的所有权未被取走
println!("原字符串: {}", s);
}
```
### 题目 4-4参数所有权设计
请合理设计以下函数的参数类型(传值 / 传引用 / 传可变引用):
```rust
// fn capitalize(s: ???) // 1. 只读取,不修改,调用后原变量还要用
// fn consume_and_print(s: ???) // 2. 拿走所有权,调用后原变量不再使用
// fn append_exclamation(s: ???)// 3. 修改原字符串
fn capitalize(s: &str) -> String {
let mut result = s.to_string(); // &str -> String
if let Some(c) = result.get_mut(0..1) {
c.make_ascii_uppercase();
}
result
}
fn consume_and_print(s: String) { // [批注] 原写了 strstr 是 DST 不能直接作参数
println!("消费了: {}", s);
}
fn append_exclamation(s: &mut String) { // [批注] 原写了 &mut str应为 &mut String
s.push_str("!");
}
fn main() {
let s = String::from("hello");
let capitalized = capitalize(&s); // 填空:调用 capitalize
println!("{}", s); // s 仍可用
consume_and_print(s); // 填空:调用 consume_and_print传入 s
// println!("{}", s); // 若取消注释会报错
let mut t = String::from("hello");
append_exclamation(&mut t); // 填空:调用 append_exclamation
println!("{}", t); // 期望输出hello!
}
```
### 题目 4-5数组切片操作
编写函数实现以下功能:
```rust
// 返回数组前 n 个元素的和
fn sum_of_first_n(arr: &[i32], n: usize) -> i32 {
// 你的代码
// 提示:使用切片 &arr[..n]
return arr[..n].iter().sum();
}
// 判断一个切片是否包含目标值
fn contains(arr: &[i32], target: i32) -> bool {
// 你的代码
return arr.contains(&target);
}
fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("前 3 个元素的和: {}", sum_of_first_n(&numbers, 3)); // 期望60
println!("包含 30 吗? {}", contains(&numbers, 30)); // 期望true
println!("包含 99 吗? {}", contains(&numbers, 99)); // 期望false
}
```
---
## 五、综合思考题
### 题目 5-1
以下代码为什么不能通过编译?提出两种修复方法,并说明各自的优缺点。
```rust
fn main() {
let mut data = vec![1, 2, 3];
let first = &data[0];
data.push(4);
println!("first = {}", first);
}
```
> 答:
### 题目 5-2
分析以下代码,回答:
1. 写出每一步所有权的状态变化
2. 最终输出是什么?
```rust
fn main() {
let mut s = String::from("rust");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1, r2 此后不再使用
let r3 = &mut s;
r3.push_str(" is great");
println!("{}", r3);
// r3 此后不再使用
let s2 = s; // 移动 s
// println!("{}", s); // 若取消注释会怎样?
println!("{}", s2);
}
```
> **[批注] 原答最终输出写错了。** r3 通过 `push_str(" is great")` 修改了 s 的内容s 此时已经是 `"rust is great"`。之后 s 移动给 s2s2 也是 `"rust is great"`。
>
> 正确的逐步分析:
> 1. `let mut s = String::from("rust")` — s 拥有 "rust"
> 2. `let r1 = &s` — 不可变借用s 仍持有所有权
> 3. `let r2 = &s` — 第二个不可变借用(允许多个)
> 4. `println!("{} and {}", r1, r2)` — 输出 `rust and rust`,此后 r1、r2 不再使用
> 5. `let r3 = &mut s` — 可变借用r1、r2 已失效,允许)
> 6. `r3.push_str(" is great")` — 通过可变引用修改 s
> 7. `println!("{}", r3)` — 输出 `rust is great`
> 8. `let s2 = s` — s 的所有权移动到 s2
> 9. `println!("{}", s2)` — 输出 `rust is great`
>
> 最终输出:
> ```
> rust and rust
> rust is great
> rust is great
> ```
### 题目 5-3
以下代码在 Rust 中是否合法?为什么?
```rust
fn main() {
let x = 5;
let y = &x;
let z = &x;
println!("{} {} {}", x, y, z);
}
```
> **[批注] 原分析对引用的理解有严重错误:引用(`&`)不会转移所有权!**
>
> **对于 `i32`** `y = &x` 创建的是指向 x 的引用,并没有发生 Copy 或所有权转移。`x` 始终拥有那个 `5``y` 和 `z` 只是借用它。代码合法,因为多个不可变引用可以共存,且原值本身也可以在引用存在时使用(前提是 `x` 实现了 `Copy` 或本身没有被移动)。这里 `println!` 读取 `x` 也是合法的,因为 i32 是 Copy会自动复制。
>
> **对于 `String`** 同样,`y = &x` 和 `z = &x` 创建的是不可变引用,没有转移所有权。`x` 仍然持有该 String。代码合法因为多个不可变引用可以同时存在。原答说"所有权被转移给y和zx不再有变量所有权"是完全错误的——引用和所有权转移move是两回事。
如果将 `x` 的类型从 `i32` 改为 `String`,同样的结构是否仍然合法?
```rust
fn main() {
let x = String::from("hello");
let y = &x;
let z = &x;
println!("{} {} {}", x, y, z);
}
```
> **[批注] 同上一问,引用不会转移所有权。** String 版本同样合法。x 持有 String 的所有权y 和 z 只是不可变借用。但注意这里 `println!` 中直接使用 `x` 与 String 的情况,由于 `x` 不可变,`println!` 只需要 `&T`,所以编译器会自动将 `x` 视为 `&x`,不会发生移动。如果此处是 `let y = x;`(不带 & 的移动),那 x 的所有权才真正被转移,编译才会失败。
### 题目 5-4
Rust 的所有权系统与 C++ 的 RAII、Java 的垃圾回收各自有什么优缺点?请简要对比分析。
> **[批注] 原答过于简略且有概念偏差:"C++ RAII和Java的垃圾回收在运行时检查"不够准确。**
> - **C++ RAII**:所有权由程序员显式管理(构造/析构、拷贝/移动语义C++11 引入 `std::unique_ptr` 等智能指针提供了类似 Rust 的部分保护但编译器不强制检查野指针、重复释放、use-after-move 等错误仍可能静默通过编译。优点:灵活性极高,零运行时开销。
> - **Java GC**:完全无所有权概念,所有对象在堆上分配,由 GC 自动追踪引用并回收。程序员完全不用操心释放。缺点运行时开销GC 暂停),无法在编译期保证资源及时释放,内存占用不可预测。
> - **Rust 所有权**:编译期静态检查所有权、移动、借用,零运行时开销。内存安全和数据竞争在编译期得到保证。缺点:学习曲线陡峭,某些合法但需要复杂生命周期的模式难以表达。
---
## 参考答案
> 请独立完成再查看答案。
<details>
<summary>点击展开答案</summary>
### 一、基础题
**1-1**:❌ 编译错误。`s` 的所有权已移动到 `t``s` 已失效。
**1-2**:✅ 通过编译。`i32` 实现了 `Copy`,赋值时自动复制。
**1-3**:✅ 通过编译。`clone` 创建了深拷贝,两个变量独立。
**1-4**:✅ 通过编译。不可变引用可以同时存在多个。
**1-5**:❌ 编译错误。同一作用域不能同时存在两个可变引用。
**1-6**:❌ 编译错误。已有不可变引用时不能创建可变引用。
### 二、填空题
**2-1**`takes_ownership(s);`
**2-2**`let len = get_length(&s);`
**2-3**
```rust
let mut s = String::from("hello");
append_world(&mut s);
```
**2-4**
```rust
let r1 = &s;
let r2 = &mut s;
```
### 三、修复错误
**3-1**`s1` 已被移动给 `s2`,不能再传给函数。修复:
```rust
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 克隆一份
print_both(s1, s2);
}
```
**3-2**:返回了局部变量的引用,生命周期不够。修复:
```rust
fn dangle() -> String { // 返回 String 而非 &String
let s = String::from("hello");
s // 所有权移出
}
```
**3-3**`r1` 和 `r2` 的可变借用时间重叠。修复:将两次借用限制在各自的作用域内,或使用 `Non-Lexical Lifetimes` 特性Rust 2018+ 已默认启用,但此处 `r1``r2` 创建后仍被使用)。修复方法:
```rust
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
println!("{}", r1);
// r1 在 println! 后不再使用
let r2 = &mut s;
r2.push_str("!");
println!("{}", r2);
}
```
**3-4**`clear()` 需要一个可变引用,但 `first` 是一个不可变引用,违反了借用规则。修复:先使用 `first`,再 `clear`
```rust
fn main() {
let s = String::from("hello world");
let first = &s[0..5];
println!("first = {}", first); // 先使用
let mut s = s; // 重新绑定
s.clear();
}
```
### 四、编程题
**4-1**
```rust
fn count_words(s: &str) -> usize {
s.split_whitespace().count()
}
```
**4-2**
```rust
fn first_and_last(s: &str) -> (&str, &str) {
let words: Vec<&str> = s.split_whitespace().collect();
(words[0], words[words.len() - 1])
}
```
**4-3**
```rust
fn title_case(s: &str) -> String {
s.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
}
})
.collect::<Vec<String>>()
.join(" ")
}
```
**4-4**
```rust
fn capitalize(s: &str) -> String { /* ... */ }
fn consume_and_print(s: String) { /* ... */ }
fn append_exclamation(s: &mut String) { /* ... */ }
// 调用方式:
let capitalized = capitalize(&s); // 传引用
consume_and_print(s); // 传值,所有权移入
append_exclamation(&mut t); // 传可变引用
```
**4-5**
```rust
fn sum_of_first_n(arr: &[i32], n: usize) -> i32 {
arr[..n].iter().sum()
}
fn contains(arr: &[i32], target: i32) -> bool {
arr.contains(&target)
}
```
### 五、综合思考题
**5-1**`first` 是对 `data` 元素的不可变引用,`push` 需要一个可变引用,违反借用规则。
- 方法一:调整顺序,先 push 再取引用 —— 简单但有时逻辑不允许
- 方法二:使用索引 `let first = data[0]`(需要 Copy 类型)—— 避免了借用但只适用于 Copy 类型
**5-2**
- 步骤:创建 `s` → 两次不可变借用r1, r2→ 打印后 r1, r2 释放 → 一次可变借用r3→ 打印后 r3 释放 → 移动 `s``s2`
- 输出:`rust and rust` / `rust is great` / `rust is great`
- 取消注释会报错:`s` 已移动
**5-3**:两者都合法。创建引用不会转移 `x` 的所有权,无论 `x` 是什么类型。只要引用和原值不冲突即可(此处都是不可变引用)。
**5-4**(简要):
- **C++ RAII**:灵活、性能极高,但依赖程序员自觉,容易出现野指针、重复释放
- **Java GC**:使用方便,不会出现悬挂指针,但存在运行时开销、不可预测的停顿
- **Rust 所有权**:编译期零成本抽象,保证内存安全/线程安全,但学习曲线陡峭
</details>