如何理解Rust引用和借用的规则-代码实战分析

我是蚂蚁背大象(Apache EventMesh PMC&Committer),文章对你有帮助给项目rocketmq-rust star,关注我GitHub:mxsm,文章有不正确的地方请您斧正,创建ISSUE提交PR~谢谢! Emal:mxsm@apache.com

1. Rust引用的两条规则

  1. 在任何给定的时间,您可以有一个可变引用或任意数量的不可变引用。
  2. 引用必须始终有效。

那么如何来理解这两条规则。

2. 引用规则理解

第一条规则:在任何给定的时间,您可以有一个可变引用或任意数量的不可变引用。

这意味着在同一时刻,要么有一个可变引用,要么有多个不可变引用,但不能同时存在可变引用和不可变引用。通过下面的代码来讲解

rust 复制代码
fn main() {
    let mut data = 42;

    // 不可变引用
    let reference1 = &data;
    println!("Reference 1: {}", reference1);

    // 不可变引用
    let reference2 = &data;
    println!("Reference 2: {}", reference2);

    // 错误示例:同时存在可变引用和不可变引用
    // let mutable_reference = &mut data; // 这一行将导致编译错误

    // 正确示例:在不可变引用作用域结束后获取可变引用
    let mutable_reference = &mut data;
    *mutable_reference += 1;
    println!("Mutable Reference: {}", mutable_reference);
}

然后再来看一下我在项目中遇到的相同的问题:

看一下编译错误:

调整后的

这里就违反了第一条规则。导致的编译错误

具体代码可以查看 rocketmq-rust 项目

第二条规则:引用必须始终有效。

这意味着引用不能越过其有效范围。在 Rust 中,这是通过生命周期来保证的。

rust 复制代码
fn main() {
    let reference;
    {
        let data = 42;
        reference = &data; // 错误示例:data 的生命周期结束后,reference 将指向无效的数据
    }
    // 此处 reference 指向无效的数据,这将导致编译错误
    // println!("Reference: {}", reference);

    // 正确示例:使用引用的生命周期确保引用在有效范围内
    let data = 42;
    let reference = &data;
    println!("Reference: {}", reference);
}

这第二条也就是说不能有悬垂引用。

3. 借用规则理解

首先看一下代码(代码来源:rocketmq-rust 项目):

rust 复制代码
fn clean_topic_by_un_register_requests(
    &mut self,
    removed_broker: HashSet<String>,
    reduced_broker: HashSet<String>,
) {
    let mut delete_topic = HashSet::new();
    while let Some((topic, queue_data_map)) = self.topic_queue_table.iter_mut().next() {
        for broker_name in &removed_broker {
            if let Some(removed_qd) = queue_data_map.remove(broker_name) {
                println!(
                    "removeTopicByBrokerName, remove one broker's topic {} {:?}",
                    topic, removed_qd
                );
            }
        }

        if queue_data_map.is_empty() {
            println!(
                "removeTopicByBrokerName, remove the topic all queue {}",
                topic
            );
            delete_topic.insert(topic);
        }

        for broker_name in &reduced_broker {
            if let Some(queue_data) = queue_data_map.get_mut(broker_name) {
                if self
                    .broker_addr_table
                    .get(broker_name)
                    .map_or(false, |b| b.enable_acting_master())
                {
                    // Master has been unregistered, wipe the write perm
                    if self.is_no_master_exists(broker_name.as_str()) {
                        // Update the queue data's permission, assuming PermName is an enum
                        // For simplicity, I'm using 0 as PERM_WRITE value, please replace it
                        // with the actual value
                        queue_data.perm = queue_data.perm & (!PermName::PERM_WRITE as u32);
                    }
                }
            }
        }
    }
    for topic in delete_topic {
        self.topic_queue_table.remove(topic);
    }
}

编译错误:

shell 复制代码
error\[E0499]: cannot borrow `self.topic_queue_table` as mutable more than once at a time
\--> rocketmq-namesrv\src\route\route\_info\_manager.rs:1010:51
|
1010 |         while let Some((topic, queue\_data\_map)) = self.topic\_queue\_table.iter\_mut().next() {
\|                                                   ^^^^^^^^^^^^^^^^^^^^^^ `self.topic_queue_table` was mutably borrowed here in the previous iteration of the loop
...
1046 |         for topic in delete\_topic {
\|                      ------------ first borrow used here, in later iteration of loop

错误原因分析: Rust的 borrow checker 保证了在同一时间内要么是可变引用,要么是不可变引用。在你的代码中,self.topic_queue_table.iter_mut().next() 返回一个对 self.topic_queue_table 的可变引用,而在迭代器的生命周期内你尝试调用 self.is_no_master_exists(broker_name.as_str()) 使用了 self 的不可变引用。这是不允许的,因为它违反了 borrow checker 的规则。

解决这个问题的一种方式是通过使用 clone 来避免在迭代期间对同一数据的多次可变引用。这样,你就可以在迭代器和 is_no_master_exists 函数中都使用 self 的不可变引用。

Rust的借用规则是由其借用检查器(Borrow Checker)来执行的,目的是在编译时防止数据竞争、空指针引用以及一些其他内存安全问题。以下是Rust的借用规则的主要要点:

  1. 不可变引用(Immutable Borrowing)

    • 在任何给定时间内,可以有多个不可变引用指向同一块内存。
    • 不可变引用的生命周期不能长于持有数据的可变引用的生命周期。
  2. 可变引用(Mutable Borrowing)

    • 在给定时间内,只能有一个可变引用指向同一块内存。
    • 可变引用的生命周期不能长于持有数据的不可变引用的生命周期。
    • 可变引用的生命周期不能长于持有数据的其他可变引用的生命周期。
  3. 借用检查器

    • 借用检查器在编译时静态地检查代码,确保没有数据竞争和悬垂引用。
    • 数据竞争是指多个引用同时访问同一块内存,并且至少有一个引用是可变的。
    • 悬垂引用是指引用超过了其指向的数据的生命周期。
  4. 生命周期参数

    • 生命周期参数(Lifetime)用于指定引用的生命周期。
    • 生命周期参数允许编译器验证引用的有效性,确保引用在其指向的数据仍然有效时使用。
  5. 借用规则的主旨

    • 在任何给定时间,要么只有一个可变引用,要么只有多个不可变引用。
    • 引用的生命周期必须正确管理,以防止悬垂引用。

在 Rust 中,当使用 iter_mut() 对一个集合进行可变迭代时,next() 方法返回一个可变引用的 Option,表示下一个元素。在你的代码中,你使用了 while let Some((topic, queue_data_map)) = self.topic_queue_table.iter_mut().next() 循环,但是你并没有在循环体内对 self.topic_queue_table 进行修改。

这样的循环可能导致 iter_mut() 一直返回同一个可变引用,因为没有对集合进行修改。在这种情况下,循环会一直返回 Some,进入死循环。

要避免这个问题,确保在循环体内对 self.topic_queue_table 进行一些修改,以使得每次迭代都能得到不同的元素。例如,你可以使用 drain 方法或者使用索引来修改集合。下面是一个例子,使用 drain 方法:

javascript 复制代码
while let Some((topic, queue_data_map)) = self.topic_queue_table.iter_mut().next() {
    // 在这里进行集合的修改,例如使用 drain 方法
    let _ = self.topic_queue_table.drain(topic);
    // 或者使用索引
    // let _ = self.topic_queue_table.remove(topic);
}

这里的 drain 方法会将集合中的元素移除,确保下一次迭代时能够得到不同的元素。当然,具体的修改方式取决于你的业务逻辑和数据结构的设计。

相关推荐
hummhumm3 分钟前
第 12 章 - Go语言 方法
java·开发语言·javascript·后端·python·sql·golang
hummhumm3 分钟前
第 8 章 - Go语言 数组与切片
java·开发语言·javascript·python·sql·golang·database
尼克_张5 分钟前
tomcat配合geoserver安装及使用
java·tomcat
wywcool18 分钟前
JVM学习之路(5)垃圾回收
java·jvm·后端·学习
-seventy-37 分钟前
Java Web 工程全貌
java
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ42 分钟前
idea 删除本地分支后,弹窗 delete tracked brank
java·ide·intellij-idea
言慢行善43 分钟前
idea出现的问题
java·ide·intellij-idea
杨荧1 小时前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
beifengtz1 小时前
【Rust调用Windows API】获取正在运行的全部进程信息
rust·windows api
monkey_meng1 小时前
【Rust中的项目管理】
开发语言·rust·源代码管理