跳至主要內容

rust随笔(五)

Mr.Lexon大约 12 分钟rust

Rust随笔(五)

本篇讨论两个东西:

  1. RcRefCell
  2. Reference (&) (在这里我们用引用代替Reference) 在前面的随笔中,提到了“引用”的概念,我们回顾一下这个基础函数:
fn judge_size<'a>(x:&'a str,y:&'a str) -> &'a str {
	if (x > y){
		x
	}else{
		y
	}
}

在随笔第一篇,里面提到一个很重要的概念,就是函数即是上下文,但是在实践当中,可能会有多个函数共享一个变量,或者是多个结构体共享一个变量,那么生命周期就变得复杂起来了,还有就是,有些函数可能还要修改一下变量本身,the book提到,一个作用域内,只能存在多个引用和一个可变引用,因此,若还是采用“引用”的化,整个设计就变得相当的复杂, 可以查看这个例子,一个简单的文本处理系统(单线程):

use std::fmt::Debug;

//文本数据 
#[derive(Debug, Clone)] // Clone 是为了稍后简化 Editor 的修改逻辑,实际复杂场景可能不允许随意 Clone
struct TextData {
    content: String,
    version: u32,
}

impl TextData {
    fn new(content: &str) -> Self {
        TextData {
            content: content.to_string(),
            version: 1,
        }
    }
}

//文本存储库
// 它拥有数据,并尝试将数据的引用借给其他组件
struct TextRepository<'a> {
    data: TextData,
    // 为了让 Analyzer 和 Editor 能够持有对 data 的引用,
    // Repository 可能需要持有对它们的引用,或者反过来,
    // 这里我们尝试让外部组件持有对 Repository 内部数据的引用,这将引入生命周期。
    _phantom: std::marker::PhantomData<&'a ()>, 
}

impl<'a> TextRepository<'a> {
    fn new(initial_content: &str) -> Self {
        TextRepository {
            data: TextData::new(initial_content),
            _phantom: std::marker::PhantomData,
        }
    }

    // 提供对数据的不可变引用
    fn get_data(&self) -> &TextData {
        &self.data
    }

    // 提供对数据的可变引用 (这将是问题的关键)
    fn get_data_mut(&mut self) -> &mut TextData {
        &mut self.data
    }

    fn update_content(&mut self, new_content: &str) {
        self.data.content = new_content.to_string();
        self.data.version += 1;
        println!("Repository: Content updated, new version {}", self.data.version);
    }
}

//分析器
// Analyzer 需要长时间持有对 TextData 的引用
struct Analyzer<'repo, 'data: 'repo> {
    id: String,
    // 它借用了 TextRepository 中的 TextData
    // 'data 必须至少活得和 'repo 一样长,或者说 'repo 的生命周期不能超过 'data
    text_data_ref: &'data TextData,
    repository_ref: &'repo TextRepository<'data>, // 只是为了展示更复杂的依赖
}

impl<'repo, 'data> Analyzer<'repo, 'data> {
    fn new(id: &str, repository: &'repo TextRepository<'data>) -> Self {
        Analyzer {
            id: id.to_string(),
            text_data_ref: repository.get_data(), // 获取引用
            repository_ref: repository,
        }
    }

    fn analyze(&self) {
        println!(
            "Analyzer '{}': Analyzing content (version {}): '{}...'",
            self.id,
            self.text_data_ref.version,
            self.text_data_ref.content.chars().take(15).collect::<String>()
        );
    }
}

// 编辑器
// Editor 可能需要修改数据,这意味着它需要一个可变引用
// 这将与 Analyzer 持有的不可变引用冲突
struct Editor<'repo, 'data: 'repo> {
    // 为了修改,它需要对 TextRepository 的可变引用,
    // 或者直接对 TextData 的可变引用 (如果 TextRepository 允许的话)
    repository_mut_ref: &'repo mut TextRepository<'data>,
}

impl<'repo, 'data> Editor<'repo, 'data> {
    fn new(repository: &'repo mut TextRepository<'data>) -> Self {
        Editor {
            repository_mut_ref: repository,
        }
    }

    fn edit_and_save(&mut self, append_text: &str) {
        let current_content = self.repository_mut_ref.get_data().content.clone(); // 需要 clone 来拼接
        let new_content = format!("{} {}", current_content, append_text);
        self.repository_mut_ref.update_content(&new_content);
        println!("Editor: Text appended and saved.");
    }
}


fn main() {
    // 场景1:只有一个 Analyzer
    println!(" 场景1: 只有一个 Analyzer ");
    let mut repo1 = TextRepository::new("Initial document content.");
    {
        let analyzer1 = Analyzer::new("A1", &repo1);
        analyzer1.analyze();
        // analyzer1 在这里 drop,对 repo1 的借用结束
    }
    // repo1 在这里仍然可用
    repo1.update_content("Updated content for next phase.");
    println!("Repo1 data after update: {:?}", repo1.get_data());


    // 场景2:尝试同时拥有 Analyzer 和 Editor (这将编译失败或非常棘手)
    println!("\n场景2: Analyzer 和 Editor 同时存在 (预期的复杂性/编译问题)");
    let mut repo2 = TextRepository::new("Content for shared access.");

    // 如果我们先创建 Analyzer,它会持有对 repo2 的不可变借用
    let analyzer2 = Analyzer::new("A2", &repo2); // repo2 被不可变借用

    // 此时,我们不能再获取对 repo2 的可变借用以创建 Editor
    // 下面的代码会导致编译错误,因为 repo2 已经被不可变地借用了
    /*
    let mut editor1 = Editor::new(&mut repo2); // 编译错误: cannot borrow `repo2` as mutable because it is also borrowed as immutable
    editor1.edit_and_save("Appended by Editor.");
    */

    // 为了让它编译,analyzer2 必须先被销毁,释放不可变借用
    analyzer2.analyze();
    drop(analyzer2); // 显式销毁 analyzer2

    // 现在可以创建 Editor 了
    {
        let mut editor1 = Editor::new(&mut repo2);
        editor1.edit_and_save("Appended by Editor.");
    } // editor1 在这里销毁,释放可变借用

    // 如果想再次分析,需要重新创建 Analyzer
    let analyzer3 = Analyzer::new("A3", &repo2);
    analyzer3.analyze();
    println!("Repo2 data at end: {:?}", repo2.get_data());


    // 演示生命周期更复杂的情况
    // 假设我们想把 Analyzer 存起来,同时还想在某个时刻使用 Editor
    println!("\n场景3: 尝试管理更长生命周期的引用 (更复杂的生命周期管理)");

    let mut repo3 = TextRepository::new("Long living repository content.");
    let mut analyzers_collection: Vec<Analyzer<'_, '_>> = Vec::new();

    // 我们不能简单地这样做,因为 repo3 的生命周期需要比 analyzers_collection 中的引用更长,
    // 并且在 analyzers_collection 存活期间,repo3 不能被可变借用。

    // 这个例子试图展示:如果 Analyzer 的实例需要比创建它的那个临时作用域活得更久,
    // (例如,将它们存储在一个集合中),并且 Editor 也需要在某个时候操作同一个 TextRepository,
    // 那么生命周期的管理会变得非常困难。

    // 简化版:如果 Analyzer 和 Editor 不是同时活跃,而是交替的,那还比较容易管理。
    // 但如果它们的生命周期有重叠,并且一个需要不可变引用,另一个需要可变引用,
    // Rust 的借用检查器会阻止你。

    // 尝试创建一个 Analyzer 并放入集合
    // 为了让下面的代码编译(即使只是部分),我们需要确保 repo3 的生命周期足够长
    // 并且在 analyzers_collection 引用它期间,它不能被可变借用。
    let analyzer_temp = Analyzer::new("TempA", &repo3);
    // analyzers_collection.push(analyzer_temp); // 这里会有生命周期问题,因为 analyzer_temp 很快就失效了
    // 要解决这个问题,需要更复杂的生命周期标注或不同的结构设计

    // 真正的痛点是:如果 analyzers_collection 持有了对 repo3.data 的不可变引用,
    // 那么在 analyzers_collection 的整个生命周期内,你都无法获得对 repo3 的可变引用去调用 editor.edit_and_save()。
    // 这就是 Rc<T> 和内部可变性模式 (如 Rc<RefCell<T>>) 可以解决的问题。

    // 为了让这个 main 函数能跑通并展示一些东西,我们不把 analyzer 存入集合,
    // 而是演示交替使用(这仍然不能完全体现 Rc 的必要性,但能暗示复杂性)
    {
        let analyzer_s3 = Analyzer::new("S3-A1", &repo3);
        analyzer_s3.analyze();
    } // analyzer_s3 销毁

    {
        let mut editor_s3 = Editor::new(&mut repo3);
        editor_s3.edit_and_save("Edited in Scenario 3.");
    } // editor_s3 销毁

    {
        let analyzer_s3_after_edit = Analyzer::new("S3-A2", &repo3);
        analyzer_s3_after_edit.analyze();
    }

    println!("场景3的最终目的是想说明:如果你需要在不同组件间共享数据,并且这些组件的生命周期各不相同,");
    println!("有的需要读,有的需要写,那么仅靠 Rust 的借用和生命周期系统,代码会变得非常受限且难以管理。");
    println!("例如,你无法轻易地将持有引用的 Analyzer 实例存入一个集合,然后在其他地方对源数据进行修改。");

}

这个例子中体现的复杂性和痛点:

  1. 生命周期参数的蔓延:TextRepositoryAnalyzerEditor 都带上了生命周期参数 ('a, 'repo, 'data)。随着系统变大,这些生命周期参数会传递到更多的结构体和函数签名中,使得代码难以阅读和维护。
  2. 场景2清晰地展示了“一个可变引用或多个不可变引用”的规则。一旦 Analyzer 持有了对 repo2 的不可变引用,就无法再创建 Editor 来获取可变引用,除非 Analyzer 的借用结束。这使得组件的同时活动变得困难。
  3. 场景3试图说明的:如果想将持有引用的 Analyzer 实例存储在一个集合中(意味着这些 Analyzer 的生命周期可能会延长),那么在这些 Analyzer 存活期间,对原始数据的可变访问(通过 Editor)会变得不可能。 为了解决这个复杂性问题,rust提供了一个工具:Rc<T>,这是一个单线程智能指针,目的就是为了解决引用复杂的问题,因为Rc只是共享一个只读引用而不能修改,如要修改,还得采用RefCell<T>,它允许你在拥有不可变引用的情况下,修改数据(在单线程环境下)。它将 Rust 编译期的借用规则检查推迟到运行时。 那么我们用这两个工具重构上面这个例子:
use std::fmt::Debug;
use std::rc::Rc;
use std::cell::RefCell;

//  文本数据 (保持不变) 
#[derive(Debug, Clone)]
struct TextData {
    content: String,
    version: u32,
}

impl TextData {
    fn new(content: &str) -> Self {
        TextData {
            content: content.to_string(),
            version: 1,
        }
    }
}

//  文本存储库 (重构) 
// Repository 现在持有对 TextData 的 Rc<RefCell<...>>
// 不再需要生命周期参数 'a'
struct TextRepository {
    shared_data: Rc<RefCell<TextData>>,
}

impl TextRepository {
    fn new(initial_content: &str) -> Self {
        TextRepository {
            shared_data: Rc::new(RefCell::new(TextData::new(initial_content))),
        }
    }

    // 提供对共享数据的访问 (通过 Rc 克隆)
    fn get_shared_data_access(&self) -> Rc<RefCell<TextData>> {
        Rc::clone(&self.shared_data)
    }

    // 更新内容现在通过 RefCell 的 borrow_mut() 完成
    // 这个方法可以保留,也可以让 Editor 直接操作共享数据
    fn update_content_via_repo(&self, new_content: &str) {
        let mut data = self.shared_data.borrow_mut(); // 运行时借用检查
        data.content = new_content.to_string();
        data.version += 1;
        println!(
            "Repository (via method): Content updated, new version {}",
            data.version
        );
    }
}

//  分析器 (重构) 
// 不再需要复杂的生命周期参数,它持有 Rc<RefCell<TextData>>
struct Analyzer {
    id: String,
    shared_data: Rc<RefCell<TextData>>, // 持有共享数据的 Rc
}

impl Analyzer {
    fn new(id: &str, shared_data_access: Rc<RefCell<TextData>>) -> Self {
        Analyzer {
            id: id.to_string(),
            shared_data: shared_data_access,
        }
    }

    fn analyze(&self) {
        let data = self.shared_data.borrow(); // 运行时不可变借用
        println!(
            "Analyzer '{}': Analyzing content (version {}): '{}...'",
            self.id,
            data.version,
            data.content.chars().take(15).collect::<String>()
        );
    }
}

//  编辑器 (重构) 
// 同样持有 Rc<RefCell<TextData>>
struct Editor {
    id: String,
    shared_data: Rc<RefCell<TextData>>, // 持有共享数据的 Rc
}

impl Editor {
    fn new(id: &str, shared_data_access: Rc<RefCell<TextData>>) -> Self {
        Editor {
            id: id.to_string(),
            shared_data: shared_data_access,
        }
    }

    fn edit_and_save(&self, append_text: &str) {
        // 获取可变借用,这会在运行时检查规则
        // 如果其他地方正持有可变借用,这里会 panic
        // 如果其他地方正持有不可变借用,这里也会 panic
        // 但如果所有其他借用都已释放,这里可以成功
        let mut data = self.shared_data.borrow_mut();

        let current_content = data.content.clone(); // 仍然需要 clone 来拼接,或用其他方式修改
        let new_content = format!("{} {}", current_content, append_text);

        data.content = new_content;
        data.version += 1;
        println!(
            "Editor '{}': Text appended. New version {}.",
            self.id, data.version
        );
    }
}

fn main() {
    println!(" 使用 Rc<RefCell<T>> 重构后的场景 ");

    // 创建 Repository,它内部创建了 Rc<RefCell<TextData>>
    let repo = TextRepository::new("Initial shared document content.");

    //  场景1 & 2 融合:多个 Analyzer 和 Editor 可以同时存在并共享数据 
    println!("\n 场景: 多个 Analyzer 和 Editor 共享数据 ");

    // Analyzer 和 Editor 都从 Repository 获取对共享数据的“访问权”(即 Rc 的克隆)
    let analyzer1 = Analyzer::new("A1", repo.get_shared_data_access());
    let analyzer2 = Analyzer::new("A2", repo.get_shared_data_access());
    let editor1 = Editor::new("E1", repo.get_shared_data_access());
    let editor2 = Editor::new("E2", repo.get_shared_data_access());

    analyzer1.analyze(); // A1 读取

    editor1.edit_and_save("Appended by E1."); // E1 修改

    analyzer2.analyze(); // A2 读取修改后的内容

    editor2.edit_and_save("Further appended by E2."); // E2 再次修改

    analyzer1.analyze(); // A1 再次读取,看到 E2 的修改

    // 我们可以通过 Repository 的一个实例来观察最终版本(如果需要)
    // 或者直接通过任何一个持有 Rc 的组件来观察
    println!(
        "Final content version from repo's perspective: {}",
        repo.get_shared_data_access().borrow().version
    );
    println!(
        "Final content from analyzer1's perspective: '{}'",
        analyzer1.shared_data.borrow().content
    );


    //  场景3:将 Analyzer (或 Editor) 存起来,同时其他组件仍可操作 
    println!("\n 场景: 组件存入集合,数据仍可共享和修改 ");
    let repo_s3 = TextRepository::new("Content for collection test.");

    let mut analyzers_collection: Vec<Analyzer> = Vec::new();
    let editor_s3_main = Editor::new("MainS3Editor", repo_s3.get_shared_data_access());

    let analyzer_s3_c1 = Analyzer::new("S3-C1", repo_s3.get_shared_data_access());
    let analyzer_s3_c2 = Analyzer::new("S3-C2", repo_s3.get_shared_data_access());

    analyzers_collection.push(analyzer_s3_c1);
    analyzers_collection.push(analyzer_s3_c2);

    // 即使 Analyzer 实例被存储在集合中,Editor 仍然可以修改共享数据
    editor_s3_main.edit_and_save("Content modified while analyzers are in collection.");

    // 集合中的 Analyzer 实例也能看到修改
    for analyzer_in_coll in &analyzers_collection {
        analyzer_in_coll.analyze();
    }

    println!("\n 演示 RefCell 的运行时借用规则 ");
    let data_access = repo_s3.get_shared_data_access();
    // let _borrow1 = data_access.borrow_mut(); // 获取一个可变借用
    // let _borrow2 = data_access.borrow();    // 尝试获取不可变借用,如果 _borrow1 仍存活,这里会 panic
    // let _borrow3 = data_access.borrow_mut(); // 再次尝试获取可变借用,如果 _borrow1 仍存活,这里会 panic
    // println!("如果上面 panic 的行被取消注释,这里不会执行。");
    // 确保借用在需要时被正确释放,通常是 Ref/RefMut 离开作用域时自动发生。

    println!("\n重构后的设计解决了生命周期参数蔓延和严格编译期借用冲突的问题。");
    println!("现在多个组件可以更灵活地共享和修改数据(在单线程内),");
    println!("代价是将部分借用规则的检查从编译期推迟到了运行时。");
}

重构完的例子显然降低了很多设计复杂度,但是代价就是运行时开销。而且RefCell<T>也是有条件的使用,这些建议仅供参考:

  • 确信你的逻辑在运行时不会违反“一个可变引用或多个不可变引用”的规则。 RefCell 只是把这个检查推迟了。
  • 仔细管理 RefRefMut 的生命周期,确保它们及时被释放。
  • 当存在借用冲突的风险时(尤其是在复杂的调用链中),优先使用 try_borrow()try_borrow_mut() 来避免 panic。
  • 理解你为什么需要内部可变性,并只在确实无法通过静态借用规则解决时才使用它。 最后,Rc<T>RefCell<T>都是单线程下的工具,多线程工具将在下一篇介绍。

总结,本文探讨了Rc<T>RefCell<T>在什么情况下使用,并且使用这些方法有什么具体的副作用,总的来说,能用”引用“就用”引用“,若设计的实在复杂,再引入Rc<T>RefCell<T>

上次编辑于:
贡献者: Lexon