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

19 KiB
Raw Blame History

Rust 所有权练习题

建议先手动写出每道题的答案,再运行代码验证。

一、基础题:判断正误

判断以下代码能否通过编译。如果可以,说明原因;如果不能,指出错误原因。

题目 1-1

fn main() {
    let s = String::from("hello");
    let t = s;
    println!("{}", s);
}

答: 不能

题目 1-2

fn main() {
    let x = 42;
    let y = x;
    println!("x = {}, y = {}", x, y);
}

答: 能

题目 1-3

fn main() {
    let s = String::from("rust");
    let t = s.clone();
    println!("s = {}, t = {}", s, t);
}

答: 能

题目 1-4

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    let s3 = &s1;
    println!("{} {} {}", s1, s2, s3);
}

答: 能

题目 1-5

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{}, {}", r1, r2);
}

答: 不能

题目 1-6

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转移所有权

fn main() {
    let s = String::from("rustacean");
    takes_ownership(________);  // 填空,使 s 的所有权进入函数
    // println!("{}", s);      // 此行若取消注释会报错
}

fn takes_ownership(s: String) {
    println!("{}", s);
}

答: s

题目 2-2借用的生命周期

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可变引用

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作用域技巧

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

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 一份。

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

fn main() {
    let s = dangle();
    println!("{}", s);
}

fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

[批注] 原错误分析不精确。 真正原因:dangle() 返回 &String,但引用指向的是函数内的局部变量 s。函数结束时 s 被释放返回的引用变成悬垂引用dangling reference。Rust 编译器在编译期阻止了这种行为。修复方法:直接返回 String 让所有权移出。

fn main() {
    let s = dangle();
    println!("{}", s);
}

fn dangle() -> String {
    let s = String::from("hello");
    s
}

题目 3-3

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 创建之前。

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

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。

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,计算字符串中的单词数量(以空格分隔)。

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),返回字符串的首单词和尾单词。

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字符串处理

编写一个函数,接收一个字符串,返回一个新的字符串,将其中的每个单词首字母大写,其余字母小写。

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参数所有权设计

请合理设计以下函数的参数类型(传值 / 传引用 / 传可变引用):

// 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数组切片操作

编写函数实现以下功能:

// 返回数组前 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

以下代码为什么不能通过编译?提出两种修复方法,并说明各自的优缺点。

fn main() {
    let mut data = vec![1, 2, 3];
    let first = &data[0];
    data.push(4);
    println!("first = {}", first);
}

答:

题目 5-2

分析以下代码,回答:

  1. 写出每一步所有权的状态变化
  2. 最终输出是什么?
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 中是否合法?为什么?

fn main() {
    let x = 5;
    let y = &x;
    let z = &x;
    println!("{} {} {}", x, y, z);
}

[批注] 原分析对引用的理解有严重错误:引用(&)不会转移所有权!

对于 i32 y = &x 创建的是指向 x 的引用,并没有发生 Copy 或所有权转移。x 始终拥有那个 5yz 只是借用它。代码合法,因为多个不可变引用可以共存,且原值本身也可以在引用存在时使用(前提是 x 实现了 Copy 或本身没有被移动)。这里 println! 读取 x 也是合法的,因为 i32 是 Copy会自动复制。

对于 String 同样,y = &xz = &x 创建的是不可变引用,没有转移所有权。x 仍然持有该 String。代码合法因为多个不可变引用可以同时存在。原答说"所有权被转移给y和zx不再有变量所有权"是完全错误的——引用和所有权转移move是两回事。

如果将 x 的类型从 i32 改为 String,同样的结构是否仍然合法?

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 所有权:编译期静态检查所有权、移动、借用,零运行时开销。内存安全和数据竞争在编译期得到保证。缺点:学习曲线陡峭,某些合法但需要复杂生命周期的模式难以表达。

参考答案

请独立完成再查看答案。

点击展开答案

一、基础题

1-1 编译错误。s 的所有权已移动到 ts 已失效。

1-2 通过编译。i32 实现了 Copy,赋值时自动复制。

1-3 通过编译。clone 创建了深拷贝,两个变量独立。

1-4 通过编译。不可变引用可以同时存在多个。

1-5 编译错误。同一作用域不能同时存在两个可变引用。

1-6 编译错误。已有不可变引用时不能创建可变引用。

二、填空题

2-1takes_ownership(s);

2-2let len = get_length(&s);

2-3

let mut s = String::from("hello");
append_world(&mut s);

2-4

let r1 = &s;
let r2 = &mut s;

三、修复错误

3-1s1 已被移动给 s2,不能再传给函数。修复:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // 克隆一份
    print_both(s1, s2);
}

3-2:返回了局部变量的引用,生命周期不够。修复:

fn dangle() -> String {    // 返回 String 而非 &String
    let s = String::from("hello");
    s                       // 所有权移出
}

3-3r1r2 的可变借用时间重叠。修复:将两次借用限制在各自的作用域内,或使用 Non-Lexical Lifetimes 特性Rust 2018+ 已默认启用,但此处 r1r2 创建后仍被使用)。修复方法:

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-4clear() 需要一个可变引用,但 first 是一个不可变引用,违反了借用规则。修复:先使用 first,再 clear

fn main() {
    let s = String::from("hello world");
    let first = &s[0..5];
    println!("first = {}", first);  // 先使用
    let mut s = s;                  // 重新绑定
    s.clear();
}

四、编程题

4-1

fn count_words(s: &str) -> usize {
    s.split_whitespace().count()
}

4-2

fn first_and_last(s: &str) -> (&str, &str) {
    let words: Vec<&str> = s.split_whitespace().collect();
    (words[0], words[words.len() - 1])
}

4-3

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

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

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-1first 是对 data 元素的不可变引用,push 需要一个可变引用,违反借用规则。

  • 方法一:调整顺序,先 push 再取引用 —— 简单但有时逻辑不允许
  • 方法二:使用索引 let first = data[0](需要 Copy 类型)—— 避免了借用但只适用于 Copy 类型

5-2

  • 步骤:创建 s → 两次不可变借用r1, r2→ 打印后 r1, r2 释放 → 一次可变借用r3→ 打印后 r3 释放 → 移动 ss2
  • 输出:rust and rust / rust is great / rust is great
  • 取消注释会报错:s 已移动

5-3:两者都合法。创建引用不会转移 x 的所有权,无论 x 是什么类型。只要引用和原值不冲突即可(此处都是不可变引用)。

5-4(简要):

  • C++ RAII:灵活、性能极高,但依赖程序员自觉,容易出现野指针、重复释放
  • Java GC:使用方便,不会出现悬挂指针,但存在运行时开销、不可预测的停顿
  • Rust 所有权:编译期零成本抽象,保证内存安全/线程安全,但学习曲线陡峭