Rust 所有权机制
Rust 🦀 的所有权(Ownership)机制是该语言核心特性之一,它为内存安全提供了保障,同时避免了垃圾回收器的使用。所有权系统基于三个原则:所有权规则、借用和生命周期,持续更新ing
所有权规则
所有权系统包括以下三个规则:
- 每个值在 Rust 中都有一个称为所有者(owner)的变量。
- 同一时间内,一个值只能有一个所有者。
- 当所有者离开作用域时,值将被销毁。
这些规则确保了 Rust 程序的内存安全性和正确性。
使用方式
以下是 Rust 所有权机制的一些基本使用方式:
-
变量的赋值与移动:当将一个变量赋值给另一个变量时,Rust 默认执行的是移动(move)操作。这意味着原变量不再可用,所有权被移交给新变量。
let s1 = String::from("hello"); let s2 = s1; // 此时 s1 不再可用,因为所有权已经转移给 s2。 ```
-
函数传参与返回值:将值传递给函数时,所有权也会发生转移。类似地,从函数返回值时,所有权也会移交给调用者。
fn take_ownership(s: String) { println!("{}", s); } let s1 = String::from("hello"); take_ownership(s1); // 此时 s1 不再可用,因为所有权已经转移给函数 take_ownership。 ```
借用与引用
为了能够在不转移所有权的情况下使用值,Rust 提供了借用(borrowing)机制。借用允许你创建一个指向值的引用(reference),而不需要获取值的所有权。引用分为两种类型:不可变引用与可变引用。
-
不可变引用:允许你在不修改值的情况下访问它。可以创建多个不可变引用,但它们不能与可变引用共存。
fn calculate_length(s: &String) -> usize { s.len() } let s1 = String::from("hello"); let len = calculate_length(&s1); // 此时 s1 仍然可用,因为我们只是借用了它的引用。 ```
-
可变引用:允许你在访问值的同时修改它。在同一作用域中,一个值只能有一个可变引用,以防止数据竞争。
fn append_world(s: &mut String) { s.push_str(", world"); } let mut s1 = String::from("hello"); append_world(&mut s1); // 此时 s1 的值已经被修改,但所有权仍在我们手中。 ```
生命周期
生命周期(lifetime
)是 Rust 用于确保引用在其整个使用期间始终有效的机制。生命周期在函数签名中通过尖括号内的标识符表示,比如:<'a>
。这些标识符用于指定引用的最长生命周期。
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
在这个例子中,函数 longest
接受两个字符串切片的引用,并返回一个具有相同生命周期 'a
的引用。这确保了返回的引用在其使用期间始终有效。
总之,Rust 的所有权机制通过所有权规则、借用和生命周期来确保内存安全,避免了需要显式内存管理或垃圾回收器。这有助于避免常见的内存管理错误,如悬垂指针、内存泄漏和数据竞争。
切片
切片(slice
)是 Rust 中的一种数据类型,它允许你引用某个数据结构的一部分,而不是整个数据结构。切片通常用来处理字符串或数组。例如,你可以使用字符串切片来引用字符串的一部分:
let s = String::from("hello, world");
let hello = &s[0..5];
let world = &s[7..12];
这里,我们使用了范围语法(start..end
)创建了两个字符串切片,分别引用 s
中的 “hello” 和 “world”。注意,切片的范围是左闭右开,也就是说 0..5
表示索引 0 到 4 的字符。
切片也可以用于数组:
let a = [1, 2, 3, 4, 5];
let sub_a = &a[1..4];
在这个例子中,我们创建了一个整数数组 a
,然后创建了一个切片 sub_a
,它引用了数组 a
中的第 2 个到第 4 个元素。
模式匹配
模式匹配(pattern matching
)是 Rust 中的一种强大功能,它允许你对复杂的数据结构进行解构和检查。模式匹配最常用的结构之一是 match
表达式。match
表达式类似于其他语言中的 switch
语句,但更加强大和灵活。
例如,考虑一个简单的枚举类型 Color
:
enum Color {
Red,
Green,
Blue,
}
我们可以使用 match
表达式来检查 Color
变量的值:
fn describe_color(c: Color) -> &'static str {
match c {
Color::Red => "red",
Color::Green => "green",
Color::Blue => "blue",
}
}
let c = Color::Green;
println!("The color is {}", describe_color(c));
在这个例子中,describe_color
函数接受一个 Color
枚举值,并使用 match
表达式根据输入的颜色返回对应的字符串描述。
模式匹配还可以用于解构复杂的数据结构,例如结构体和元组。例如,下面是一个计算二维点距离原点的例子:
struct Point {
x: f64,
y: f64,
}
fn distance_from_origin(p: Point) -> f64 {
match p {
Point { x, y } => (x * x + y * y).sqrt(),
}
}
let p = Point { x: 3.0, y: 4.0 };
println!("The distance is {}", distance_from_origin(p));
在这个例子中,我们使用 match
表达式解构了 Point
结构体,并从中提取了 x
和 y
字段。然后,我们使用勾股定理计算点到原点的距离。
Rust 的所有权机制、切片、模式匹配等特性使得其功能强大且安全。这些特性共同为内存管理、错误处理和代码复用提供了高效、安全的解决方案。
常见问题
我怎样才能实现一个包含环的图或其他数据结构?
至少有四种选择(在 Too Many Linked Lists 中详细讨论过)。
1. 你可以使用 Rc
和 Weak
实现它,以允许节点的共享所有权。尽管这种方法需要付出内存管理的代价。
在 Rust 中,Rc
(引用计数)和 Weak
可以用来创建包含环的图结构。Rc
允许多个所有者共享同一个不可变的值,而 Weak
是对 Rc
的弱引用,这意味着它不会增加引用计数。当需要创建环形引用时,Weak
引用可以防止出现循环引用。
下面是一个简单的例子,用 Rc
和 Weak
实现一个包含环的有向图:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<RefCell<Node>>>,
parent: RefCell<Option<Weak<RefCell<Node>>>>,
}
impl Node {
fn new(value: i32) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Node {
value,
children: vec![],
parent: RefCell::new(None),
}))
}
fn add_child(&mut self, child: Rc<RefCell<Node>>) {
child.borrow_mut().parent.replace(Some(Rc::downgrade(&self)));
self.children.push(child);
}
}
fn main() {
let a = Node::new(1);
let b = Node::new(2);
let c = Node::new(3);
a.borrow_mut().add_child(b.clone());
a.borrow_mut().add_child(c.clone());
b.borrow_mut().add_child(c.clone());
// The graph structure:
// a -> b -> c
// | ^
// +---------+
}
在这个例子中,我们定义了一个 Node
结构体,它表示图中的一个节点。节点包含一个整数值、一个子节点列表和一个指向父节点的 Weak
引用。我们使用 RefCell
对 parent
和 children
字段进行内部可变性,因为在程序运行过程中,我们可能需要改变这些字段的值。
Node
结构体实现了两个方法:new
和 add_child
。new
方法用于创建一个新节点,而 add_child
方法用于将一个子节点添加到当前节点。
在 main
函数中,我们创建了三个节点:a
、b
和 c
,并将它们连接成一个图结构。首先,我们将 b
和 c
添加为 a
的子节点,然后将 c
添加为 b
的子节点。这样,我们就创建了一个包含环的有向图:
a -> b -> c
| ^
+---------+
使用 Rc
和 Weak
的关键在于 Node
结构体的 parent
字段。parent
字段是一个 Weak<RefCell<Node>>
类型,这意味着它是一个弱引用,不会增加引用计数。这样,在创建环时,我们就不会产生循环引用,从而避免了内存泄漏。
注意:在这个例子中,我们使用了 RefCell
来实现内部可变性。RefCell
是 Rust 中的一种运行时借用检查机制,它允许在运行时检查可变引用的唯一性。
当需要在多个不可变引用之间共享可变状态时,RefCell
是一个很好的选择。
child.borrow_mut().parent.replace(Some(Rc::downgrade(&self)));
这行代码执行了以下操作:
-
child.borrow_mut()
: 使用RefCell
的borrow_mut
方法获得child
的一个可变引用。这允许我们修改child
的内部状态。 -
parent.replace(Some(Rc::downgrade(&self)))
: 使用RefCell
的replace
方法替换child
的parent
字段的值。replace
方法将原先的值(可能是None
)替换为传入的新值,并返回被替换的旧值。新值是Some(Rc::downgrade(&self))
。Rc::downgrade
函数将&self
(当前节点的Rc<RefCell<Node>>
引用)转换为一个Weak<RefCell<Node>>
弱引用。这样一来,child
的parent
字段就指向了当前节点,但它不会增加当前节点的引用计数。这可以防止循环引用和内存泄漏。
总结一下,这行代码的目的是将当前节点(self
)设置为 child
节点的父节点,同时避免产生循环引用。
2. 你可以使用 “不安全(unsafe
)” 的代码实现它,使用原始指针。这将更加高效,但却绕过了 Rust 的安全保证。
在 Rust 中,unsafe
关键字允许你绕过编译器的一些安全检查。
尽管在大多数情况下,我们应该避免使用 unsafe
,但在某些特殊情况下,如高性能场景或与其他不安全代码库交互时,使用 unsafe
可能是必要的。
下面是一个使用 unsafe
实现的包含环的图结构示例:
use std::ptr::NonNull;
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<NonNull<Node>>,
parent: Option<NonNull<Node>>,
}
impl Node {
fn new(value: i32) -> Box<Self> {
Box::new(Node {
value,
children: vec![],
parent: None,
})
}
unsafe fn add_child(&mut self, mut child: Box<Node>) {
child.parent = Some(NonNull::new_unchecked(self as *mut _));
self.children.push(NonNull::new_unchecked(Box::leak(child)));
}
}
fn main() {
let mut a = Node::new(1);
let mut b = Node::new(2);
let mut c = Node::new(3);
unsafe {
a.add_child(b);
a.add_child(c.clone());
b.add_child(c);
}
// The graph structure:
// a -> b -> c
// | ^
// +---------+
}
在这个例子中,我们定义了一个 Node
结构体,它表示图中的一个节点。节点包含一个整数值、一个子节点列表和一个指向父节点的可选指针。我们使用 NonNull
类型来表示非空裸指针。NonNull
是一个 std::ptr::NonNull
类型,它表示一个非空的裸指针,但它仍然是不安全的。
Node
结构体实现了两个方法:new
和 add_child
。new
方法用于创建一个新节点,而 add_child
方法用于将一个子节点添加到当前节点。注意,我们将 add_child
方法标记为 unsafe
,因为它涉及到裸指针的操作。
pub struct NonNull<T>
where
T: ?Sized,
{ /* private fields */ }
std::ptr::NonNull
是 Rust 标准库中的一个类型,表示一个非空的裸指针。NonNull<T>
是一个简单的非空指针包装器,其中 T
是指针所指向的类型。
它的主要用途是在表示所有权关系时表明某个指针一定不为 null
。这对于某些底层数据结构(如自定义的智能指针、内存分配器等)的实现有很大帮助,因为它可以提供额外的编译时安全保障。
下面是关于 NonNull
的一些重要特点:
-
非空性:
NonNull
类型的主要特点是它确保了指针不为null
。这可以避免空指针解引用的潜在问题,提高代码的安全性。 -
不可变性:
NonNull
类型的指针是可变的,即它的内部指针可以指向可变引用(&mut T
)。需要注意的是,这种可变性与&mut T
具有的可变性略有不同,因为NonNull
类型没有借用检查器的安全保障。 -
构造方法:
NonNull
类型提供了一些用于创建实例的方法,如NonNull::new
和NonNull::new_unchecked
。其中,
NonNull::new
是一个安全的方法,它接受一个裸指针并返回一个Option<NonNull<T>>
。如果传入的裸指针为null
,则返回None
。而NonNull::new_unchecked
是一个不安全的方法,它接受一个裸指针并直接返回一个NonNull<T>
实例,不进行空指针检查。 -
方法:
NonNull
类型提供了一些方法,如as_ptr
、as_ref
和as_mut
,以在不同情况下与裸指针和引用进行转换。 -
优化:由于
NonNull
类型保证了指针不为null
,编译器可以在某些情况下为其生成更优化的代码。
需要注意的是,虽然 NonNull
类型提供了一定程度的安全保障,但它仍然涉及裸指针操作,因此使用时要谨慎。
unsafe fn add_child(&mut self, mut child: Box<Node>) {
child.parent = Some(NonNull::new_unchecked(self as *mut _));
self.children.push(NonNull::new_unchecked(Box::leak(child)));
}
在这个函数中,我们处理裸指针和内存泄漏,因此需要使用 unsafe
关键字。
下面是这段代码的详细解释:
-
child.parent = Some(NonNull::new_unchecked(self as *mut _));
这一行将当前节点(
self
)的裸指针设置为child
节点的父节点。首先,我们将self
的引用转换为一个可变裸指针(*mut _
)。然后,我们使用
NonNull::new_unchecked
函数将裸指针封装为NonNull
类型。这个函数是不安全的,因为它不检查传入的裸指针是否为null
。最后,我们将NonNull
类型的父节点存储在child.parent
字段中。 -
self.children.push(NonNull::new_unchecked(Box::leak(child)));
这一行将
child
节点添加到当前节点的子节点列表中。首先,我们使用
Box::leak
函数将child
的所有权泄漏,并返回一个&mut T
类型的引用。这个函数是不安全的,因为泄漏的内存将永远不会被回收。然后,我们将得到的引用转换为一个可变裸指针,并使用
NonNull::new_unchecked
函数将裸指针封装为NonNull
类型。最后,我们将
NonNull
类型的子节点添加到当前节点的children
列表中。
请注意,此示例中的代码不安全,因为它涉及到裸指针和内存泄漏。
总之,虽然这个例子展示了如何使用 unsafe
实现一个包含环的图结构,但在实际开发中,我们应该尽量避免使用 unsafe
,除非有特殊需求。
3. 使用向量和这些向量的索引。有几个可用这种方法的例子和解释。
// 待补充
4. 用 UnsafeCell
使用借用的引用。对于这种方法有解释和代码。
// 待补充
我怎样才能定义一个包含对其自身字段之一的引用的结构?
这是有可能的,但是这样做没有用。该结构会被自己永久借用,因此不能被移动。
下面是一些说明这个问题的代码。
use std::cell::Cell;
#[derive(Debug)]
struct Unmovable<'a> {
x: u32,
y: Cell<Option<&'a u32>>,
}
fn main() {
let test = Unmovable { x: 42, y: Cell::new(None) };
test.y.set(Some(&test.x));
println!("{:?}", test);
}
注意:这种设计限制了Unmovable
类型的实例在其生命周期内的可移动性。因为y
字段包含对x
字段的引用,因此在引用存在的情况下,该实例不能被移动。
创建一个结构体,其中一个字段直接引用其自身字段的情况并不常见。这可能会导致编译时的生命周期问题,因为结构体实例在引用的字段之前可能会被销毁。
然而,这样的结构体是可以定义的,但它会受到严格的限制,如上面提到的,结构体会被永久借用且无法移动。以下是一个例子:
#![allow(dead_code)]
struct SelfReferential<'a> {
value: i32,
reference: &'a i32,
}
impl<'a> SelfReferential<'a> {
fn new() -> SelfReferential<'a> {
let value = 42;
SelfReferential {
value,
reference: &value,
}
}
}
fn main() {
let _self_ref = SelfReferential::new();
}
error[E0515]: cannot return value referencing local variable `value`
--> src/main.rs:11:9
|
11 | / SelfReferential {
12 | | value,
13 | | reference: &value,
| | ------ `value` is borrowed here
14 | | }
| |_________^ returns a value referencing data owned by the current function
For more information about this error, try `rustc --explain E0515`.
error: could not compile `leetcode_in_rust` (bin "leetcode_in_rust") due to previous error
这段代码不能成功编译,因为value
字段的生命周期比SelfReferential
实例短。当new()
函数返回时,value
将被销毁,而引用&value
将成为悬垂引用。这就是为什么编译器会报错。
如果你确实需要在结构体中引用其自身的字段,可以考虑使用std::rc::Rc
和std::cell::RefCell
(单线程场景)或std::sync::Arc
和std::sync::Mutex/RwLock
(多线程场景)组合,这将允许你创建引用计数指针以确保结构体实例在其引用的字段之前不会被销毁。
然而,这种方法并不是最佳实践,因为它可能导致难以理解的代码和潜在的性能问题。在大多数情况下,更好的方法是重新审视你的数据结构设计,看看是否有其他方式来实现所需的功能,而无需将结构体字段的引用存储在结构体中。
下面是一个示例:
use std::rc::Rc;
use std::cell::RefCell;
struct MyStruct {
value: i32,
reference: Option<Rc<RefCell<MyStruct>>>,
}
impl MyStruct {
fn new(value: i32) -> MyStruct {
MyStruct {
value,
reference: None,
}
}
fn set_reference(&mut self, other: &Rc<RefCell<MyStruct>>) {
self.reference = Some(Rc::clone(other));
}
}
fn main() {
let my_struct1 = Rc::new(RefCell::new(MyStruct::new(1)));
let my_struct2 = Rc::new(RefCell::new(MyStruct::new(2)));
my_struct1.borrow_mut().set_reference(&my_struct2);
println!("my_struct1 value: {}", my_struct1.borrow().value);
if let Some(ref other) = my_struct1.borrow().reference {
println!("my_struct1 reference value: {}", other.borrow().value);
}
}
在这个例子中,我们创建了一个名为MyStruct
的结构体,其中包含一个名为reference
的字段,用于存储指向另一个MyStruct
实例的引用。我们使用Rc<RefCell<MyStruct>>
来实现引用计数指针。Rc
用于共享所有权,而RefCell
允许在运行时对借用进行检查,从而实现可变性。
注意:这个解决方案在单线程场景下工作良好。如果你在多线程环境中使用这个结构,你应该使用std::sync::Arc
和std::sync::Mutex
或std::sync::RwLock
代替Rc
和RefCell
。
Reference
- GPT-4
- http://smallcultfollowing.com/babysteps/blog/2015/04/06/modeling-graphs-in-rust-using-vector-indices/
- Introduction - Learning Rust With Entirely Too Many Linked Lists (rust-unofficial.github.io)
- 所有权 - Rust语言圣经(Rust Course)
- 【译】Rust 常见的问题 | Pure White
- NonNull in std::ptr - Rust (rust-lang.org)
- https://github.com/nrc/r4cppp/blob/master/graphs/README.md#node-and-unsafecell