【Rust入门知识点学与练】第34课:所有权与借用

知识点:所有权三条核心规则

rust 复制代码
fn main() {
    // 规则1:每个值有且只有一个所有者(owner)
    let s1 = String::from("hello");
    // s1 是 "hello" 的唯一所有者

    // 规则2:所有者离开作用域时,值被丢弃(drop)
    {
        let s2 = String::from("world");
        println!("{}", s2);
    } // s2 离开作用域,"world" 被释放

    // println!("{}", s2);  // ❌ s2 已不存在

    // 规则3:值在同一时刻只能有一个所有者
    let s3 = String::from("rust");
    let s4 = s3;  // 所有权从 s3 移动到 s4
    // println!("{}", s3);  // ❌ s3 已失效(被移动)
    println!("{}", s4);     // ✅ s4 是新的所有者
}

知识点:移动语义(Move)

rust 复制代码
fn main() {
    // === 堆上数据:赋值 = 移动 ===
    let a = String::from("hello");
    let b = a;  // 所有权移动,a 失效
    // println!("{}", a);  // ❌ 编译错误:value used here after move
    println!("{}", b);     // ✅

    // === 栈上数据:赋值 = 拷贝(Copy)===
    let x = 42;
    let y = x;  // 拷贝,x 仍然有效
    println!("x={}, y={}", x, y);  // ✅ 都能用

    // 实现了 Copy trait 的类型赋值时是拷贝而非移动:
    // - 所有整数类型(i32, u64, ...)
    // - 浮点类型(f32, f64)
    // - 布尔类型 bool
    // - 字符类型 char
    // - 元组(当所有元素都实现 Copy 时)
    // - 引用 &T

    let t1 = (1, 2, 3);
    let t2 = t1;  // 拷贝(元组元素都是 i32)
    println!("t1={:?}, t2={:?}", t1, t2);  // ✅ 都能用

    let t3 = (1, String::from("hello"), 3);
    let t4 = t3;  // 移动!(元组包含 String,不实现 Copy)
    // println!("{:?}", t3);  // ❌ t3 已失效

    // === 函数参数也是移动 ===
    fn take_ownership(s: String) {
        println!("获得了: {}", s);
    }  // s 离开作用域,被释放

    let my_string = String::from("hello");
    take_ownership(my_string);
    // println!("{}", my_string);  // ❌ my_string 已移动进函数

    // === 返回值也可以转移所有权 ===
    fn give_ownership() -> String {
        let s = String::from("给你的");
        s  // 所有权移动给调用者
    }

    let received = give_ownership();
    println!("收到: {}", received);

    // === 移动 vs 拷贝 的本质区别 ===
    // 移动:只转移指针(栈上的 ptr/len/capacity),不拷贝堆上数据
    // 拷贝:复制栈上的值(因为数据全在栈上)
}

知识点:克隆 --- 深拷贝

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // 深拷贝:堆上数据也复制一份

    println!("s1={}, s2={}", s1, s2);  // ✅ 都能用

    // clone 的开销:
    // - String::clone 会分配新堆内存并复制内容
    // - Vec::clone 会分配新堆内存并逐个克隆元素
    // - HashMap::clone 会分配新堆内存并复制所有键值对

    let v1 = vec![1, 2, 3];
    let v2 = v1.clone();
    println!("v1={:?}, v2={:?}", v1, v2);

    // 嵌套结构的 clone
    let data = vec![
        String::from("a"),
        String::from("b"),
    ];
    let data2 = data.clone();  // 深拷贝整个 Vec 及其中的每个 String
    println!("{:?}, {:?}", data, data2);

    // clone 与 move 的性能对比
    let big_string = "x".repeat(1_000_000);  // 1MB 字符串

    // 移动:O(1),只复制 3 个 usize(ptr, len, cap)
    let moved = big_string;

    // 克隆:O(n),分配 1MB 新内存并复制
    let cloned = moved.clone();

    println!("moved 长度: {}", moved.len());
    println!("cloned 长度: {}", cloned.len());
}

知识点:引用与借用(Borrowing)

rust 复制代码
fn main() {
    // === 不可变引用 &T(共享引用)===
    let s = String::from("hello");

    // &s 创建不可变引用(借用)
    // 不转移所有权,s 仍然是所有者
    let len = calculate_length(&s);
    println!("'{}' 的长度是 {}", s, len);  // ✅ s 仍可用

    // 多个不可变引用可以同时存在
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    println!("{}, {}, {}", r1, r2, r3);  // ✅

    // === 可变引用 &mut T(独占引用)===
    let mut s = String::from("hello");

    change(&mut s);
    println!("修改后: {}", s);  // hello, world

    // === 借用规则(编译期强制执行)===
    // 规则A:同一时刻,要么只有一个可变引用,要么有多个不可变引用
    // 规则B:引用必须始终有效(不能指向已释放的数据)

    // 违反规则A的例子:
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} {}", r1, r2);
    // r1 和 r2 在这之后不再使用(NLL:非词法生命周期)

    let r3 = &mut s;  // ✅ 因为 r1、r2 的最后使用已经结束
    println!("{}", r3);

    // === 引用也是值(遵循移动规则)===
    let s = String::from("hello");
    let r = &s;       // r 是 &String 类型
    let r2 = r;       // 引用实现了 Copy,所以是拷贝
    println!("{} {}", r, r2);  // ✅ 都能用

    // 但可变引用不实现 Copy
    let mut s = String::from("hello");
    let r = &mut s;
    let r2 = r;  // 移动!r 失效
    // println!("{}", r);   // ❌ r 已移动
    println!("{}", r2);     // ✅
}

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s 是引用,离开作用域时不会释放数据

fn change(s: &mut String) {
    s.push_str(", world");
}

知识点:悬垂引用(Dangling Reference)

rust 复制代码
fn main() {
    // Rust 编译器会阻止悬垂引用

    // ❌ 这段代码无法编译:
    // fn dangle() -> &String {
    //     let s = String::from("hello");
    //     &s  // s 离开作用域后被释放,引用变成悬垂
    // }

    // ✅ 正确做法:返回所有权
    fn no_dangle() -> String {
        let s = String::from("hello");
        s  // 移动所有权给调用者
    }

    let s = no_dangle();
    println!("{}", s);

    // 另一种悬垂引用场景:
    let reference = {
        let x = 42;
        // &x  // ❌ x 即将被释放
        // 正确做法:
        x  // 返回值的拷贝(i32 实现了 Copy)
    };
    println!("{}", reference);

    // 条件分支中的生命周期问题
    let s;
    {
        let inner = String::from("inner");
        // s = &inner;  // ❌ inner 在块结束时被释放
        s = inner;      // ✅ 移动所有权
    }
    println!("{}", s);  // ✅ s 拥有数据
}

知识点:切片(Slice)--- 不拥有数据的引用

fn main() {
    // === 字符串切片 &str ===
    let s = String::from("hello world");

    let hello = &s[0..5];     // "hello"
    let world = &s[6..11];    // "world"
    let all = &s[..];         // "hello world"(整个字符串)
    let hello2 = &s[..5];     // "hello"(从开头)
    let world2 = &s[6..];     // "world"(到结尾)

    println!("{}, {}, {}", hello, world, all);
    println!("{}, {}", hello2, world2);

    // 字符串切片的类型是 &str
    fn first_word(s: &str) -> &str {
        let bytes = s.as_bytes();
        for (i, &byte) in bytes.iter().enumerate() {
            if byte == b' ' {
                return &s[..i];
            }
        }
        s
    }

    let word = first_word("hello world");
    println!("第一个词: {}", word);  // hello

    // 字面量本身就是 &str
    let literal: &str = "这是字面量";  // 存储在二进制文件中,生命周期为 'static

    // === 数组切片 &[T] ===
    let arr = [10, 20, 30, 40, 50];

    let slice1 = &arr[1..3];   // &[20, 30]
    let slice2 = &arr[..2];    // &[10, 20]
    let slice3 = &arr[3..];    // &[40, 50]
    let all = &arr[..];        // &[10, 20, 30, 40, 50]

    println!("{:?}, {:?}, {:?}", slice1, slice2, slice3);

    // 切片作为函数参数(更通用)
    fn sum(slice: &[i32]) -> i32 {
        slice.iter().sum()
    }

    let vec = vec![1, 2, 3, 4, 5];
    let arr = [10, 20, 30];

    println!("Vec 切片之和: {}", sum(&vec));    // 15
    println!("数组切片之和: {}", sum(&arr));     // 60
    println!("部分切片之和: {}", sum(&vec[1..4])); // 9

    // === 切片的本质 ===
    // 切片是一个胖指针(fat pointer):
    // - 指向数据起始位置的指针
    // - 切片的长度
    // 大小 = 2 个 usize(16 字节 on 64位)

    // === 可变切片 ===
    let mut arr = [1, 2, 3, 4, 5];
    let slice = &mut arr[1..4];
    slice[0] = 20;
    slice[1] = 30;
    slice[2] = 40;
    println!("修改后: {:?}", arr);  // [1, 20, 30, 40, 5]
}

知识点:所有权与结构体

rust 复制代码
// 结构体可以拥有数据
struct Owner {
    name: String,      // 拥有 String 数据
    items: Vec<String>, // 拥有 Vec 数据
}

// 结构体也可以持有引用(需要生命周期标注)
struct Borrower<'a> {
    name: &'a str,           // 借用字符串切片
    items: &'a [String],     // 借用切片
}

fn struct_ownership() {
    // 结构体拥有数据
    let owner = Owner {
        name: String::from("Alice"),
        items: vec![String::from("书"), String::from("笔")],
    };
    println!("{} 拥有 {:?}", owner.name, owner.items);

    // 移动结构体中的字段
    let name = owner.name;  // name 被移动出来
    // println!("{}", owner.name);  // ❌ 部分移动后不能访问整个结构体
    println!("名字: {}", name);

    // 但其他字段仍可使用(如果没被移动)
    // 注意:实际上部分移动后整个结构体都不能用了
    // 需要用 Option 或 RefCell 来解决

    // 结构体借用数据
    let data = vec![String::from("x"), String::from("y")];
    let borrower = Borrower {
        name: "Bob",
        items: &data,
    };
    println!("{} 借用 {:?}", borrower.name, borrower.items);
}

// === 部分移动问题与解决方案 ===
struct Config {
    host: String,
    port: u16,
    debug: bool,
}

fn partial_move_solution() {
    let config = Config {
        host: String::from("localhost"),
        port: 8080,
        debug: true,
    };

    // 方案1:用引用避免移动
    let host_ref = &config.host;
    println!("host={}, port={}, debug={}", host_ref, config.port, config.debug);
    // ✅ config 完整可用

    // 方案2:实现 Clone
    let host_clone = config.host.clone();
    println!("克隆的host: {}", host_clone);
    println!("原config仍可用: {}", config.host);

    // 方案3:用 Copy 类型(port 和 debug 是 Copy 的)
    let port = config.port;     // 拷贝
    let debug = config.debug;   // 拷贝
    println!("port={}, debug={}", port, debug);
    println!("config.port={}, config.debug={}", config.port, config.debug);
    // ✅ Copy 类型不会导致部分移动
}

fn main() {
    struct_ownership();
    println!("---");
    partial_move_solution();
}

知识点:函数签名与所有权传递

rust 复制代码
// 三种函数参数模式:

// 模式1:获取所有权
fn takes_ownership(s: String) {
    println!("我拥有了: {}", s);
}  // s 在这里被释放

// 模式2:不可变借用(只读)
fn borrows_immutable(s: &String) {
    println!("我只读: {}", s);
}  // 不释放,借用结束

// 模式3:可变借用(读写)
fn borrows_mutable(s: &mut String) {
    s.push_str(" (已修改)");
    println!("我修改了: {}", s);
}  // 不释放,借用结束

// 更好的做法:用 &str 代替 &String
fn better_borrow(s: &str) {
    println!("通用借用: {}", s);
}

fn main() {
    // 模式1
    let s1 = String::from("hello");
    takes_ownership(s1);
    // println!("{}", s1);  // ❌ s1 已移动

    // 模式2
    let s2 = String::from("world");
    borrows_immutable(&s2);
    println!("s2 仍可用: {}", s2);  // ✅

    // 模式3
    let mut s3 = String::from("rust");
    borrows_mutable(&mut s3);
    println!("s3 仍可用: {}", s3);  // ✅

    // 更好的做法
    better_borrow(&s2);       // &String 可以自动转为 &str
    better_borrow("字面量");   // &str 直接传入
    better_borrow(&s3);

    // === 返回值转移所有权 ===
    fn give_back(s: String) -> String {
        println!("临时拥有: {}", s);
        s  // 归还所有权
    }

    let s4 = String::from("temp");
    let s4 = give_back(s4);  // 移动进去,再移动出来
    println!("拿回来了: {}", s4);

    // === 同时借用和返回引用 ===
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() { x } else { y }
    }

    let result = longest("hello", "hi");
    println!("更长的: {}", result);
}

知识点:生命周期标注基础

rust 复制代码
// 生命周期标注不改变引用的实际存活时间
// 它只是告诉编译器多个引用之间的关系

// 函数返回引用时,必须标注生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// 'a 的含义:返回值的生命周期不会超过 x 和 y 中较短的那个

fn lifetime_examples() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str());
        println!("结果: {}", result);  // ✅ result 在 s2 的作用域内使用
    }
    // println!("结果: {}", result);  // ❌ result 可能引用已释放的 s2

    // 静态生命周期
    let s: &'static str = "我活在整个程序期间";
    println!("{}", s);

    // 结构体中的生命周期
    #[derive(Debug)]
    struct Important<'a> {
        text: &'a str,
    }

    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = &novel[..];
    let excerpt = Important { text: first_sentence };
    println!("{:?}", excerpt);
}

// 省略规则(Lifetime Elision)
// 编译器自动推断生命周期的三条规则:
// 1. 每个引用参数获得自己的生命周期参数
// 2. 如果只有一个输入生命周期,它被赋给所有输出引用
// 3. 如果有 &self 或 &mut self,self 的生命周期被赋给所有输出引用

fn elision_examples() {
    // 编译器自动推断:
    // fn first_word(s: &str) -> &str
    // 等价于:
    // fn first_word<'a>(s: &'a str) -> &'a str

    // fn longest(x: &str, y: &str) -> &str  // ❌ 无法推断(两个输入生命周期)
    // 必须手动标注:
    // fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

    // 方法签名自动推断:
    // impl MyStruct { fn get(&self) -> &str }
    // 等价于:
    // fn get<'a>(&'a self) -> &'a str
}

fn main() {
    lifetime_examples();
    elision_examples();
}

知识点:常见所有权陷阱与最佳实践

rust 复制代码
fn main() {
    // === 陷阱1:循环中移动 ===
    let names = vec![
        String::from("Alice"),
        String::from("Bob"),
    ];

    // ❌ for name in names { ... }  // names 被移动进循环
    // ✅ 用引用遍历
    for name in &names {
        println!("{}", name);
    }
    println!("names 仍可用: {:?}", names);  // ✅

    // === 陷阱2:闭包捕获所有权 ===
    let greeting = String::from("Hello");

    // move 关键字强制闭包获取所有权
    let closure = move || {
        println!("{}, World!", greeting);
    };
    // println!("{}", greeting);  // ❌ greeting 已移动进闭包
    closure();

    // 不移动:用引用捕获
    let name = String::from("Rust");
    let closure = || {
        println!("Hello, {}!", name);  // 借用 name
    };
    closure();
    println!("name 仍可用: {}", name);  // ✅

    // === 陷阱3:在循环中累积 String ===
    let words = vec!["hello", "world", "rust"];

    // ❌ 低效:每次循环都创建新 String
    // let mut result = String::new();
    // for w in &words {
    //     result = result + w + " ";  // 每次都重新分配
    // }

    // ✅ 高效:预分配容量
    let mut result = String::with_capacity(
        words.iter().map(|w| w.len() + 1).sum::<usize>()
    );
    for w in &words {
        result.push_str(w);
        result.push(' ');
    }
    println!("结果: '{}'", result.trim());

    // === 陷阱4:返回局部变量的引用 ===
    // fn bad() -> &String {
    //     let s = String::from("hello");
    //     &s  // ❌ 悬垂引用
    // }

    // ✅ 返回所有权
    fn good() -> String {
        let s = String::from("hello");
        s
    }

    // === 最佳实践 ===

    // 1. 优先用借用(&T / &mut T)而非获取所有权
    fn process(data: &[i32]) -> i32 {  // ✅ 借用
        data.iter().sum()
    }
    // fn process(data: Vec<i32>) -> i32 { ... }  // ❌ 不必要地获取所有权

    // 2. 用 &str 代替 &String
    fn greet(name: &str) {  // ✅ 更通用
        println!("Hello, {}!", name);
    }
    // fn greet(name: &String) { ... }  // ❌ 不够通用

    // 3. 需要修改参数时,用 &mut T
    fn append_greeting(msg: &mut String) {
        msg.push_str(" Welcome!");
    }

    // 4. 确实需要所有权时,明确说明
    fn store_data(data: String) -> Config {
        Config { data }
    }

    // 5. 用 clone() 时添加注释说明原因
    let important = String::from("重要数据");
    let copy = important.clone();  // 需要两份独立的数据
    println!("{} / {}", important, copy);
}

struct Config {
    data: String,
}

核心规则

概念 规则

所有权 每个值有且只有一个所有者

移动 赋值/传参时,非 Copy 类型的所有权转移

拷贝 Copy 类型(整数、浮点、bool、char、引用)赋值时复制

克隆 .clone() 深拷贝,开销可能很大

不可变借用 &T,可以多个同时存在

可变借用 &mut T,同一时刻只能有一个

互斥规则 同一时刻:要么多个 &T,要么一个 &mut T

悬垂引用 编译器禁止引用指向已释放的数据

切片 &sstart...end,不拥有数据的视图

生命周期 编译器确保引用不会比数据活得更久

函数参数 &str 优于 &String,&T 优于 &Vec

动手试试

补全下面的代码:

rust 复制代码
// === 题目1:修复编译错误 ===
// 以下代码有多处所有权/借用错误,补全修复版本

// 补全:修复函数,使其能正确返回两个字符串中较长的一个
// 要求不获取所有权(使用借用)
fn longer_string(s1: &str, s2: &str) -> &str {
    // 补全
    todo!()
}

// 补全:修复函数,接收一个可变字符串引用,
// 在末尾追加 " (已验证)",然后返回字符串长度
fn verify_and_measure(s: &mut String) -> usize {
    // 补全
    todo!()
}

// === 题目2:实现 WordIndex ===
// 补全:WordIndex 结构体
// - 不拥有数据,只借用一个字符串切片
// - build(text: &str) -> WordIndex:构建索引
//   将文本按空白字符分割,记录每个单词的起始位置
//   返回 HashMap<&str, usize>,键为单词,值为起始位置
// - find(&self, word: &str) -> Option<usize>:查找单词位置
// - words(&self) -> Vec<&str>:返回所有单词列表(按出现顺序)

struct WordIndex<'a> {
    text: &'a str,
    index: std::collections::HashMap<&'a str, usize>,
    order: Vec<&'a str>,
}

impl<'a> WordIndex<'a> {
    fn build(text: &'a str) -> Self {
        // 补全
        todo!()
    }

    fn find(&self, word: &str) -> Option<usize> {
        // 补全
        todo!()
    }

    fn words(&self) -> Vec<&str> {
        // 补全
        todo!()
    }
}

// === 题目3:实现 MessageQueue ===
// 补全:MessageQueue 结构体
// - 内部用 Vec<String> 存储消息
// - push(&mut self, msg: &str):添加消息(接受 &str,内部转为 String 存储)
// - pop(&mut self) -> Option<String>:取出最早的消息
// - peek(&self) -> Option<&str>:查看最早的消息(不取出,返回借用)
// - len(&self) -> usize:消息数量
// - contains(&self, keyword: &str) -> bool:是否包含某关键词的消息

struct MessageQueue {
    messages: Vec<String>,
}

impl MessageQueue {
    fn new() -> Self {
        // 补全
        todo!()
    }

    fn push(&mut self, msg: &str) {
        // 补全
        todo!()
    }

    fn pop(&mut self) -> Option<String> {
        // 补全:返回最早的消息(index 0)
        // 提示:如果 Vec 为空返回 None
        todo!()
    }

    fn peek(&self) -> Option<&str> {
        // 补全:返回最早消息的引用
        todo!()
    }

    fn len(&self) -> usize {
        // 补全
        todo!()
    }

    fn contains(&self, keyword: &str) -> bool {
        // 补全:检查是否有任何消息包含 keyword
        todo!()
    }
}

fn main() {
    // === 测试 longer_string ===
    let a = String::from("short");
    let b = String::from("much longer string");
    let result = longer_string(&a, &b);
    println!("较长: {}", result);       // 期望: much longer string
    println!("a仍可用: {}", a);         // ✅ a 未被移动
    println!("b仍可用: {}", b);         // ✅ b 未被移动

    // === 测试 verify_and_measure ===
    let mut msg = String::from("数据已加载");
    let len = verify_and_measure(&mut msg);
    println!("验证后: '{}' (长度: {})", msg, len);
    // 期望: 验证后: '数据已加载 (已验证)' (长度: 11)

    // === 测试 WordIndex ===
    let text = String::from("hello world hello rust world");
    let index = WordIndex::build(&text);

    println!("'hello' 位置: {:?}", index.find("hello"));   // 期望: Some(0)
    println!("'world' 位置: {:?}", index.find("world"));   // 期望: Some(6)
    println!("'missing' 位置: {:?}", index.find("missing")); // 期望: None
    println!("所有单词: {:?}", index.words());
    // 期望: ["hello", "world", "rust"](去重,按首次出现顺序)

    // === 测试 MessageQueue ===
    let mut queue = MessageQueue::new();

    queue.push("第一条消息");
    queue.push("第二条消息");
    queue.push("紧急:系统警告");
    queue.push("第四条消息");

    println!("队列长度: {}", queue.len());           // 期望: 4
    println!("最早消息: {:?}", queue.peek());         // 期望: Some("第一条消息")
    println!("包含'紧急': {}", queue.contains("紧急")); // 期望: true
    println!("包含'错误': {}", queue.contains("错误")); // 期望: false

    let first = queue.pop();
    println!("弹出: {:?}", first);                    // 期望: Some("第一条消息")
    println!("弹出后长度: {}", queue.len());           // 期望: 3
    println!("现在最早: {:?}", queue.peek());         // 期望: Some("第二条消息")
}

期望输出:

较长: much longer string

a仍可用: short

b仍可用: much longer string

验证后: '数据已加载 (已验证)' (长度: 11)

'hello' 位置: Some(0)

'world' 位置: Some(6)

'missing' 位置: None

所有单词: "hello", "world", "rust"

队列长度: 4

最早消息: Some("第一条消息")

包含'紧急': true

包含'错误': false

弹出: Some("第一条消息")

弹出后长度: 3

现在最早: Some("第二条消息")

写好了贴过来,没问题就进入第31课:生命周期 Lifetime 进阶 👋

贴代码

完整参考答案(借用、生命周期、切片经典练习题)

rust 复制代码
  
use std::collections::HashMap;

// === 题目1:修复编译错误 ===
fn longer_string(s1: &str, s2: &str) -> &str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn verify_and_measure(s: &mut String) -> usize {
    s.push_str(" (已验证)");
    s.len()
}

// === 题目2:实现 WordIndex ===
struct WordIndex<'a> {
    text: &'a str,
    index: HashMap<&'a str, usize>,
    order: Vec<&'a str>,
}

impl<'a> WordIndex<'a> {
    fn build(text: &'a str) -> Self {
        let mut index = HashMap::new();
        let mut order = Vec::new();

        // split_whitespace 返回 &str,生命周期绑定输入text('a)
        for word in text.split_whitespace() {
            // entry API:只记录单词**第一次**出现的起始位置
            index.entry(word).or_insert_with(|| {
                // word.as_ptr() - text.as_ptr() 得到字节偏移
                let pos = (word.as_ptr() as usize) - (text.as_ptr() as usize);
                order.push(word);
                pos
            });
        }

        WordIndex { text, index, order }
    }

    fn find(&self, word: &str) -> Option<usize> {
        self.index.get(word).copied()
    }

    fn words(&self) -> Vec<&'a str> {
        self.order.clone()
    }
}

// === 题目3:实现 MessageQueue ===
struct MessageQueue {
    messages: Vec<String>,
}

impl MessageQueue {
    fn new() -> Self {
        MessageQueue {
            messages: Vec::new(),
        }
    }

    fn push(&mut self, msg: &str) {
        self.messages.push(msg.to_string());
    }

    fn pop(&mut self) -> Option<String> {
        if self.messages.is_empty() {
            None
        } else {
            Some(self.messages.remove(0))
        }
    }

    fn peek(&self) -> Option<&str> {
        self.messages.first().map(|s| s.as_str())
    }

    fn len(&self) -> usize {
        self.messages.len()
    }

    fn contains(&self, keyword: &str) -> bool {
        self.messages.iter().any(|msg| msg.contains(keyword))
    }
}

fn main() {
    // === 测试 longer_string ===
    let a = String::from("short");
    let b = String::from("much longer string");
    let result = longer_string(&a, &b);
    println!("较长: {}", result);       // 期望: much longer string
    println!("a仍可用: {}", a);         // ✅ a 未被移动
    println!("b仍可用: {}", b);         // ✅ b 未被移动

    // === 测试 verify_and_measure ===
    let mut msg = String::from("数据已加载");
    let len = verify_and_measure(&mut msg);
    println!("验证后: '{}' (长度: {})", msg, len);
    // 期望: 验证后: '数据已加载 (已验证)' (长度: 11)

    // === 测试 WordIndex ===
    let text = String::from("hello world hello rust world");
    let index = WordIndex::build(&text);

    println!("'hello' 位置: {:?}", index.find("hello"));   // 期望: Some(0)
    println!("'world' 位置: {:?}", index.find("world"));   // 期望: Some(6)
    println!("'missing' 位置: {:?}", index.find("missing")); // 期望: None
    println!("所有单词: {:?}", index.words());
    // 期望: ["hello", "world", "rust"](去重,按首次出现顺序)

    // === 测试 MessageQueue ===
    let mut queue = MessageQueue::new();

    queue.push("第一条消息");
    queue.push("第二条消息");
    queue.push("紧急:系统警告");
    queue.push("第四条消息");

    println!("队列长度: {}", queue.len());           // 期望: 4
    println!("最早消息: {:?}", queue.peek());         // 期望: Some("第一条消息")
    println!("包含'紧急': {}", queue.contains("紧急")); // 期望: true
    println!("包含'错误': {}", queue.contains("错误")); // 期望: false

    let first = queue.pop();
    println!("弹出: {:?}", first);                    // 期望: Some("第一条消息")
    println!("弹出后长度: {}", queue.len());           // 期望: 3
    println!("现在最早: {:?}", queue.peek());         // 期望: Some("第二条消息")
}
 

运行输出

plaintext

较长: much longer string

a仍可用: short

b仍可用: much longer string

验证后: '数据已加载 (已验证)' (长度: 11)

'hello' 位置: Some(0)

'world' 位置: Some(6)

'missing' 位置: None

所有单词: "hello", "world", "rust"

队列长度: 4

最早消息: Some("第一条消息")

包含'紧急': true

包含'错误': false

弹出: Some("第一条消息")

弹出后长度: 3

现在最早: Some("第二条消息")

逐题知识点讲解

题目一(1)longer_string

  • 参数是两个不可变借用 &str,函数直接返回其中一个借用;
  • 返回值生命周期由输入参数自动推断,两个借用生命周期一样就可以编译通过;
  • 没有发生所有权转移,a、b 在调用之后仍然可用。

题目一(2)verify_and_measure

  • &mut String :可变借用,可以原地修改字符串;
  • push_str :追加文本,不会产生新字符串;
  • 返回 usize (Copy类型,直接返回值),不返回引用。

易错坑:新手容易写成 return s; ,尝试返回可变引用,本题不需要。

题目二: WordIndex(生命周期标记 <'a> 核心考题)

关键点

  1. WordIndex<'a> :结构体不拥有字符串数据,只是借用外部text,生命周期参数 'a ;
  2. HashMap 的键是 &'a str ,切片借用原字符串,没有复制,非常节省内存;
  3. entry.or_insert_with :保证只存单词第一次出现;
  4. 字节偏移计算: word.as_ptr() - text.as_ptr() ,得到单词相对于文本开头的字节索引;
  5. order 数组保存首次出现顺序,words() 返回去重后的单词列表。

⚠️ 生命周期大坑:WordIndex 不能活得比原始的 text 变量更长,否则悬垂引用,编译器会检查。

题目三: MessageQueue(所有权 vs 借用对比)

  1. 内部存储 Vec :拥有消息所有权;push接收 &str ,调用 to_string() 把数据拷贝到堆;
  2. pop: remove(0) ,把字符串所有权移出Vec,返回Option;

remove(0) 时间复杂度O(n),这是简易队列;高性能生产队列建议 VecDeque ;

  1. peek:返回 Option<&str> ,只是借用,所有权仍然保存在队列内部;
  2. contains: iter().any() ,遍历每条消息,调用str::contains子串匹配。

主线学习进度小结

到此:生命周期 + 借用切片 的专项练习完成。

整条Rust学习路线:基础语法 → 所有权/借用 → 生命周期 → 泛型&Trait → 标准容器 → 自定义错误 → Rc‑RefCell‑Weak → Unsafe MyVec → Tokio异步编程。

相关推荐
柒儿吖1 小时前
SSCom 重构全记录:用 Rust 把串口调试助手送上鸿蒙 PC、macOS、Windows和Linux
测试工具·rust·harmonyos
AAA代码批发商1 小时前
DAY43 ARM 体系结构学习笔记|嵌入式入门,从硬件组件到流水线、内存与栈帧
arm开发·笔记·学习
彧azz2 小时前
初学Unity:编辑器
笔记·学习·unity·游戏引擎
529宝宝起名网2 小时前
用 Python 开发历史名字查询与起名灵感工具:从古籍人物数据库到名字文化故事生成
开发语言·前端·python
明德扬2 小时前
AD9653单通道波形怎么看?幅度、频率与基础测量入门
学习·fpga开发·fpga
幼儿园蛋小黄2 小时前
学习ARM
学习
ocean21032 小时前
2025-2026年C++专题大厂面试高频问题
开发语言·c++·面试
白远山2 小时前
健身场馆无人自动化系统实战指南:从架构设计到部署全流程解析
java·开发语言·架构·需求分析
今年下半年2 小时前
mysql数据库备份详细操作流程
学习·mysql