计数指针
概述
在大多数情况下,Rust 的所有权系统可以准确地知道哪个变量拥有某个值。然而,在某些情况下,单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点在没有任何边指向它之前不应该被清理掉。
为了支持这种多所有权,Rust 提供了 Rc<T>
类型,其代表引用计数(Reference Counting)。引用计数记录了一个值的引用数量,以便知道该值是否仍在使用。如果某个值的引用数量为零,意味着没有任何有效引用,可以被清理。
使用场景
Rc<T>
用于在堆上分配一些内存,供程序的多个部分读取,但在编译时无法确定程序的哪一部分会最后结束使用它。如果能够确定最后一个使用的部分,那么可以令其成为数据的所有者。注意 Rc<T>
只能用于单线程场景,多线程中可以使用 Arc<T>
(atomic reference counting)。
使用 Rc<T>
共享数据
示例:共享列表
假设我们有一个包含三个列表的示例,其中两个列表共享第三个列表的所有权:
enum List {
Cons(i32, Rc<List>),
Nil,
}
use std::rc::Rc;
use crate::List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
在这个示例中,a
包含了值 5
和 10
。b
和 c
则共享 a
的所有权。当创建 b
和 c
时,我们使用 Rc::clone(&a)
来增加 a
的引用计数。
引用计数变化
我们可以打印出引用计数的变化:
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
输出结果为:
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
这个示例展示了 Rc<T>
的引用计数如何随着克隆和作用域的变化而变化。每次调用 Rc::clone
时,引用计数增加;当 c
离开作用域时,引用计数减少。
Rc<T>
的深度拷贝与浅拷贝
Rc::clone不会进行数据的深拷贝,而是增加引用计数。相比于深拷贝,这种操作非常高效。当需要查找代码中的性能问题时,只需考虑深拷贝类的克隆,而无需担心
Rc::clone` 的性能。
功能小结
- 用途:
Rc<T>
用于单线程场景下的多所有权,允许多个不可变引用。 - 引用计数:通过引用计数管理数据的生命周期,确保数据在有引用时不会被清理。
- 使用场景:适用于需要多个所有者读取同一数据的场景,如图数据结构。
Rc
智能指针和普通指针的区别
1. 所有权和引用计数
- 普通引用(
&T
):- 普通引用不改变数据的所有权,只是临时借用数据。
- 普通引用不涉及引用计数,Rust 编译器在编译时检查借用规则。
- 借用结束后,引用失效,数据的所有权返回给原始所有者。
-
Rc<T>
智能指针:Rc
代表引用计数(Reference Counting),允许多个所有者共享数据的所有权。Rc
通过引用计数管理数据的生命周期,只有当引用计数为零时,数据才会被清理。- 每次克隆
Rc
时,引用计数增加;当Rc
实例离开作用域时,引用计数减少。
2. 内存管理
- 普通引用:
- 普通引用不会影响数据的生命周期,只是临时访问数据。
- 数据的生命周期由原始所有者管理,借用结束后引用失效。
- 内存分配和释放完全由编译器在编译时确定。
-
Rc<T>
:Rc
管理堆上的数据,通过引用计数来控制数据的生命周期。- 数据只有在最后一个
Rc
实例被释放后,才会被清理。 - 适用于无法在编译时确定所有权关系,且需要在多个地方共享数据的场景。
3. 用途和使用场景
- 普通引用:
- 适用于短期的、只读的访问场景。
- 保证在借用期间数据不可变,适合临时借用和局部范围的使用。
- 使用简单,性能高,没有运行时开销。
-
Rc<T>
:- 适用于需要多个所有者共享数据的场景,如图、树等数据结构。
- 允许在程序的不同部分共享数据,并管理其生命周期。
- 有一定的运行时开销,但提供了灵活的多所有权管理。
堆和栈上的区别
1. 栈上的数据
- 栈内存用于存储局部变量和函数调用信息。
- 栈上的数据具有固定大小和生命周期,按后进先出(LIFO)顺序分配和释放。
- 栈上的操作速度快,因为分配和释放内存都非常高效。
2. 堆上的数据
- 堆内存用于存储动态分配的数据,如通过
Box<T>
、Rc<T>
和Vec<T>
等分配的数据。 - 堆上的数据可以在运行时动态分配和释放,大小不固定。
- 堆内存管理的开销较大,因为需要跟踪分配和释放,并且可能导致内存碎片。
3. Rc<T>
在堆和栈上的使用
- 普通引用:
- 普通引用本身存储在栈上,引用的数据可以在堆上或栈上。
- 引用的生命周期由原始数据的所有者决定,引用结束后栈上的引用失效。
-
Rc<T>
:Rc
本身存储在栈上,但它指向的数据存储在堆上。Rc
管理堆上的数据,通过引用计数控制数据的生命周期。- 每个
Rc
实例在栈上都有一个引用计数,当所有Rc
实例离开作用域时,堆上的数据才会被释放。
总结
- 普通引用(
&T
):用于临时借用数据,不改变数据所有权,没有引用计数,适合短期只读访问。普通引用本身在栈上,引用的数据可以在栈上或堆上。 -
Rc<T>
:用于多个所有者共享数据,通过引用计数管理数据生命周期,适合复杂数据结构和无法在编译时确定所有权的场景。Rc
本身在栈上,指向的数据在堆上,引用计数控制堆上数据的释放。
RefCell<T>
和它的特性
1. 概述
在 Rust 中,编译器通过借用检查器在编译时确保所有的借用规则得到遵循:一个值要么有一个可变借用,要么有多个不可变借用,不能同时拥有两者。这种严格的规则在大多数情况下确保了内存安全,但在某些情况下会显得过于严格。例如,我们可能需要在运行时而不是编译时执行借用规则检查。这时,RefCell<T>
就派上用场了。
RefCell<T>
允许我们在运行时执行借用检查,而不是在编译时。它遵循 Rust 的借用规则,但是在运行时检查这些规则,而不是在编译时。
2. 特性
- 可变性:
RefCell<T>
允许在其内部数据上执行不可变借用和可变借用。 - 内部可变性:
RefCell<T>
允许你即使在一个不可变结构体中也能修改内部的数据。 - 单线程:
RefCell<T>
仅适用于单线程场景。如果需要在多线程环境中使用相似的功能,可以使用Mutex<T>
或RwLock<T>
。
3. 基本用法
以下是一个使用 RefCell<T>
的简单示例:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// 不可变借用
{
let borrowed_data = data.borrow();
println!("Borrowed data: {}", *borrowed_data);
}
// 可变借用
{
let mut borrowed_data = data.borrow_mut();
*borrowed_data += 10;
}
// 再次不可变借用
{
let borrowed_data = data.borrow();
println!("Updated data: {}", *borrowed_data);
}
}
在这个示例中,我们首先创建了一个 RefCell
包裹的整数值 5
。然后我们演示了如何借用这个值,既有不可变借用(borrow
方法)也有可变借用(borrow_mut
方法)。我们可以看到在运行时 RefCell
能够确保借用规则。
4. 运行时借用检查
与编译时借用检查不同,RefCell<T>
在运行时执行借用检查。如果违反了借用规则(例如,同时有多个可变借用),程序会在运行时触发 panic:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
let borrowed_data1 = data.borrow();
let borrowed_data2 = data.borrow();
// 这行会导致运行时 panic,因为同时有两个可变借用
let mut borrowed_data_mut = data.borrow_mut();
}
运行这段代码会产生以下错误:
thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:8:36
5. 内部可变性模式
内部可变性模式是指即使在不可变引用的上下文中也能修改数据的能力。通过 RefCell<T>
和其他类似类型(如 Cell<T>
),我们可以在一个不可变的结构体中修改其内部字段。
use std::cell::RefCell;
struct MyStruct {
value: RefCell<i32>,
}
impl MyStruct {
fn new(val: i32) -> Self {
MyStruct {
value: RefCell::new(val),
}
}
fn modify_value(&self, delta: i32) {
*self.value.borrow_mut() += delta;
}
fn get_value(&self) -> i32 {
*self.value.borrow()
}
}
fn main() {
let my_struct = MyStruct::new(10);
println!("Initial value: {}", my_struct.get_value());
my_struct.modify_value(5);
println!("Modified value: {}", my_struct.get_value());
}
在这个示例中,MyStruct
结构体包含一个 RefCell<i32>
字段。尽管 MyStruct
的实例是不可变的,但我们仍然可以使用 modify_value
方法修改其内部的值。
6. 结合 Rc<T>
使用
Rc<T>
和 RefCell<T>
常常结合使用,以实现既有多所有权又有内部可变性的场景。Rc<T>
允许多个所有者,而 RefCell<T>
允许在这些所有者之间进行可变借用。
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
}
fn main() {
let node1 = Rc::new(RefCell::new(Node {
value: 1,
next: None,
}));
let node2 = Rc::new(RefCell::new(Node {
value: 2,
next: Some(Rc::clone(&node1)),
}));
println!("Node1: {:?}", node1);
println!("Node2: {:?}", node2);
node1.borrow_mut().value = 10;
println!("Updated Node1: {:?}", node1);
println!("Node2: {:?}", node2);
}
在这个示例中,Node
结构体包含一个整数值和一个指向下一个节点的可选引用。通过使用 Rc<RefCell<Node>>
,我们可以创建一个共享所有权和内部可变性的链表节点。
7. 小结
Arc rcMutex refcell
- 用途:
RefCell<T>
允许在运行时检查借用规则,适用于需要内部可变性的场景。 - 特性:运行时借用检查、内部可变性、单线程。
- 结合 Rc** 使用**:在需要多所有权和内部可变性的场景下,
Rc<RefCell<T>>
是一个常见的组合。
通过学习RefCell<T>
和它的特性,我们可以在 Rust 中更灵活地处理复杂的所有权和借用场景,同时保持内存安全。