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

556 lines
12 KiB
Markdown
Raw 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);
}
```
---
## 二、填空题:补充代码
补全下列代码使其能通过编译。
### 题目 2-1转移所有权
```rust
fn main() {
let s = String::from("rustacean");
takes_ownership(________); // 填空,使 s 的所有权进入函数
// println!("{}", s); // 此行若取消注释会报错
}
fn takes_ownership(s: String) {
println!("{}", 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()
}
```
### 题目 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!");
}
```
### 题目 2-4作用域技巧
```rust
fn main() {
let mut s = String::from("hello");
{ // 进入新作用域
let r1 = ________; // 填空:创建不可变引用
println!("{}", r1);
} // r1 离开作用域
let r2 = ________; // 填空:现在可以创建可变引用了
r2.push_str(" world");
println!("{}", r2);
}
```
---
## 三、找出并修复错误
以下每段代码都有编译错误,请指出错误并写出修正后的代码。
### 题目 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);
}
```
### 题目 3-2
```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);
}
```
### 题目 3-4
```rust
fn main() {
let s = String::from("hello world");
let first = &s[0..5];
s.clear(); // 清空字符串
println!("first = {}", first);
}
```
---
## 四、编程题
### 题目 4-1计算单词数量
编写一个函数 `count_words(s: &str) -> usize`,计算字符串中的单词数量(以空格分隔)。
```rust
fn count_words(s: &str) -> usize {
// 你的代码
}
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) {
// 你的代码
}
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 等方法
}
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: ________) -> String {
let mut result = s.clone();
if let Some(c) = result.get_mut(0..1) {
c.make_ascii_uppercase();
}
result
}
fn consume_and_print(s: ________) {
println!("消费了: {}", s);
}
fn append_exclamation(s: ________) {
s.push_str("!");
}
fn main() {
let s = String::from("hello");
let capitalized = capitalize(________); // 填空:调用 capitalize
println!("{}", s); // s 仍可用
consume_and_print(________); // 填空:调用 consume_and_print传入 s
// println!("{}", s); // 若取消注释会报错
let mut t = String::from("hello");
append_exclamation(________); // 填空:调用 append_exclamation
println!("{}", t); // 期望输出hello!
}
```
### 题目 4-5数组切片操作
编写函数实现以下功能:
```rust
// 返回数组前 n 个元素的和
fn sum_of_first_n(arr: &[i32], n: usize) -> i32 {
// 你的代码
// 提示:使用切片 &arr[..n]
}
// 判断一个切片是否包含目标值
fn contains(arr: &[i32], target: i32) -> bool {
// 你的代码
}
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);
}
```
### 题目 5-3
以下代码在 Rust 中是否合法?为什么?
```rust
fn main() {
let x = 5;
let y = &x;
let z = &x;
println!("{} {} {}", x, y, z);
}
```
如果将 `x` 的类型从 `i32` 改为 `String`,同样的结构是否仍然合法?
```rust
fn main() {
let x = String::from("hello");
let y = &x;
let z = &x;
println!("{} {} {}", x, y, z);
}
```
### 题目 5-4
Rust 的所有权系统与 C++ 的 RAII、Java 的垃圾回收各自有什么优缺点?请简要对比分析。
---
## 参考答案
> 请独立完成再查看答案。
<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>