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

12 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);
}

二、填空题:补充代码

补全下列代码使其能通过编译。

题目 2-1转移所有权

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

fn takes_ownership(s: String) {
    println!("{}", 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()
}

题目 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!");
}

题目 2-4作用域技巧

fn main() {
    let mut s = String::from("hello");

    { // 进入新作用域
        let r1 = ________;        // 填空:创建不可变引用
        println!("{}", r1);
    } // r1 离开作用域

    let r2 = ________;            // 填空:现在可以创建可变引用了
    r2.push_str(" world");
    println!("{}", r2);
}

三、找出并修复错误

以下每段代码都有编译错误,请指出错误并写出修正后的代码。

题目 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);
}

题目 3-2

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);
}

题目 3-4

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

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

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

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

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

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

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

编写函数实现以下功能:

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

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

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);
}

题目 5-3

以下代码在 Rust 中是否合法?为什么?

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

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

fn main() {
    let x = String::from("hello");
    let y = &x;
    let z = &x;
    println!("{} {} {}", x, y, z);
}

题目 5-4

Rust 的所有权系统与 C++ 的 RAII、Java 的垃圾回收各自有什么优缺点?请简要对比分析。


参考答案

请独立完成再查看答案。

点击展开答案

一、基础题

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 所有权:编译期零成本抽象,保证内存安全/线程安全,但学习曲线陡峭