从这一节开始,正式开始中级Rust特性的入门学习,主要包括:智能指针,多线程,异步,Unsafe Rust等。
Rust中最常用的指针就是引用,它和其他语言的指针相比多了借用规则这一特性。智能指针之所以“智能”,是因为它除了指针的基本功能之外,它还存在其他的元数据以及其他的特殊功能。引用只是借用数据,而智能指针很多时候是拥有数据的所有权的。在Rust中,智能指针和引用一样,都实现了Deref
和Drop
这两个trait。
其实之前学习的String
和Vec<T>
就是智能指针。它们除了指向堆内存上存放具体数据的指针之外,还有“容量”和“长度”这两个概念,这就是它们和普通指针相比多出的“元数据”。
Rust中的智能指针有很多,入门主要了解Deref
和Drop
两个trait以外,了解以下几种智能指针即可:
Box<T>
:指向存储在堆内存上的数据;Rc<T>
:允许一个变量拥有多个所有者;RefCell<T>
(Ref<T>
RefMut<T>
):允许在运行时(而不是编译时)才检查借用规则。
1 Box<T>
Box<T>
是最简单的智能指针,它的主要作用就是指向一个存储在堆内存上的数据。使用Box::new()
方法创建一个Box<T>
智能指针,并通过参数创建一个存储在堆内存上的数据。
fn main() {
let x: Box<i32> = Box::new(5);
println!("x: {x}");
}
Box<T>
在功能上比较接近C/C++的指针,它有确定的大小,并且可以看作一个变量,变量内存放的是指向堆内存上指定数据的地址。所以我们可以使用Box<T>
方便地创建一个链表数据结构:
use crate::List::{Cons, Nil};
enum List {
Cons(i32, Box<List>), // 第二个数据类型是指向List变体的智能指针
Nil,
}
// Box::new() 生成一个指向数据的指针,数据存放在堆内存上
fn main() {
let l = Cons(1, Box::new(Cons(2, Box::new(Nil))));
}
这里的Nil
不是Rust内置的关键字,它只是一个结束标记,作为List
枚举的一个变体存在。main
函数中创建的链表有三个节点,前面两个节点的数据是i32
,并包含一个(智能)指针指向下一个节点,最后一个节点是Nil
。
Box<T>
有以下三点常用场景(本节只涉及第一个场景):
- 在编译时,某类型的大小无法确定,但使用该类型时,上下文又需要知道它的确切大小;
- 当拥有大量数据,想移交所有权,但是需要确保在操作时数据不会被复制;
- 使用某个值时,只关心它是否实现了特定的trait,而不关心它的具体类型(有点类似泛型的trait bound)。
2 Deref trait 和 Drop trait
2.1 Deref trait
定义Deref trait可以使用户自定义解引用运算符*
的行为,并且可以使得智能指针可以被当作常规的引用一样处理。
常规的引用就是一种指针,可以使用解引用运算符*
:
fn main() {
let x = 5;
let y = &x;
assert_eq!(x, 5);
assert_eq!(*y, 5);
}
可以使用Box<T>
替换上面程序中的引用:
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(x, 5);
assert_eq!(*y, 5);
}
Box<T>
可以被看作只有一个元素的元组结构体。基于这个原则,我们可以实现自定义的MyBox<T>
,并实现Deref trait。标准库中的Deref trait只要求我们实现一个deref
方法,这个方法借用self,并返回一个指向内部元素的引用。
use std::ops::Deref;
struct MyBox<T> (T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T; // 定义Deref的关联类型,关联类型是一类特殊的泛型
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(x, 5);
assert_eq!(*y, 5); // *(y.deref())
}
Deref trait的另一个重要作用是函数和方法的隐式解引用转化(Deref Coercion),它是为函数或者方法提供的一种便捷特性。假设T实现了Deref trait,那么Deref Coercion就会把T的引用转化为T经过Deref操作生成后的引用。当把某类型的引用传递给函数或者方法时,但是引用的类型和函数/方法定义的类型不匹配,那么Deref Coercion就会自动发生,编译器会在编译阶段堆deref进行一系列调用,来把它转化为所需的参数类型。这个过程在编译期就会完成,所以不会产生任何的运行时开销。
在之前的学习中,如果一个函数/方法的参数是字符串切片,那么它也可以接收一个字符串的引用作为参数。这是因为String实现了Deref这个trait,且其中的方法就是将&String
类型转化为&str
类型,所以参数类型可以匹配。
可以使用DerefMut trait重载可变引用的解引用运算符*
。当类型和trait遵循下面三种情况时,Rust会执行deref coercion:
- 当
T:Deref<Target = U>
,允许将&T
转换为&U
- 当
T:DerefMut<Target = U>
,允许将&mut T
转换为&mut U
- 当
T:Deref<Target = U>
,允许将&mut T
转换为&U
(反之不可)
2.2 Drop trait
类型实现Drop trait后,我们就可以自定义当它的值离开作用域的动作。任何类型都可以实现Drop trait,它只要求实现一个drop
方法,它的参数是&mut self
。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data {}", self.data);
}
}
fn main() {
let c = CustomSmartPointer {data: String::from("my stuff")};
let d = CustomSmartPointer {data: String::from("other stuff")};
println!("CustomSmartPointer Created!");
}
我们不能手动调用drop方法,但是可以调用drop函数(每个类型创建后只会被销毁一次):
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data {}", self.data);
}
}
fn main() {
let c = CustomSmartPointer {data: String::from("my stuff")};
drop(c);
let d = CustomSmartPointer {data: String::from("other stuff")};
println!("CustomSmartPointer Created!");
}
3 引用计数 Rc<T>
Rc<T>
类型是一个用于引用计数的智能指针(reference counting),它除了指针的基本作用外,还记录着指向某一块数据的引用的数量。它指向的数据允许多个不可变引用同时指向该数据,这种设计使得它可以实现图数据结构。
当我们需要在heap上分配数据,但是这些数据被程序的多个部分读取(只读),但是在编译时又无法确定哪个部分最后使用完这些数据,这时候就可以考虑使用Rc<T>
,否则考虑使用所有权转移。
Rc<T>
只适用于单线程场景,多线程场景下有另外的智能指针。
Rc<T>
不在预导入模块中,并且有以下几个常用函数:
Rc::clone(&a)
:增加引用计数,不会深度拷贝数据Rc::strong_count(&a)
(Rc::weak_count(&a)
):获取引用计数值
下面看一个例子:
use std::rc::Rc;
use crate::List::{Cons, Nil};
enum List {
Cons(i32, Rc<List>),
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));
}
use std::rc::Rc;
use crate::List::{Cons, Nil};
enum List {
Cons(i32, Rc<List>),
Nil,
}
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("Rc a strong count: {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("Rc a strong count: {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("Rc a strong count: {}", Rc::strong_count(&a));
}
println!("Rc a strong count: {}", Rc::strong_count(&a));
}
4 RefCell<T> 和内部可变性
RefCell<T>
是对数据保有唯一所有权的智能指针,和Box<T>
类似,但是 它的特殊功能是不在编译时检查借用规则,而是在运行时检查。如果在运行时违反了借用规则,那么程序就会panic。
借用规则:在一个给定的时刻,数据只能拥有任意数量的不可变引用或者唯一的可变引用;引用总是有效的。
RefCell<T>
的最大作用是在特定的内存安全环境下,在不可变环境中改变数据的内部数据。
和Rc<T>
类似,RefCell<T>
只适用于单线程场景。
下面看一个例子:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: 'a + Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You hare over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.8 {
self.messenger.send("Warning: You've used up over 80% of your quota!")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, msg: &str) {
self.sent_messages.push(String::from(msg)); // 报错,无法使用不可变引用改变变量
}
}
#[test]
fn it_send_over_75_percent_warning_message () {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
对于上面的程序,我们想使用一个不可变借用改变内部的数据值,这时就可以使用RefCell<T>
:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: 'a + Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You hare over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.8 {
self.messenger.send("Warning: You've used up over 80% of your quota!")
}
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>, // 将数据套在一个RefCell<T>内
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]), // 修改构造函数
}
}
}
impl Messenger for MockMessenger {
fn send(&self, msg: &str) {
self.sent_messages.borrow_mut().push(String::from(msg)); // borrow_mut() 返回可变借用
}
}
#[test]
fn it_send_over_75_percent_warning_message () {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); // borrow() 返回不可变借用
}
}
在上面的例子中,我们使用了RefCell<T>
的两个安全接口:
borrow()
:返回智能指针Ref<T>
,内部数据的不可变借用borrow_mut()
:返回智能指针RefMut<T>
,内部数据的可变借用
RefCell<T>
会记录当前存在多少个活跃的Ref<T>
和RefMut<T>
。每次调用borrow()
,不可变借用数+1,借用离开作用域时计数-1;每次调用borrow_mut()
,可变借用数+1,借用离开作用域时计数-1。在运行时,Rust就依靠这个技术实现借用规则的检查,当程序违反借用规则时,程序就会panic。
另一个常用的场景是将Rc<T>和RefCell<T>结合使用,这样数据就可以在编译时定义多个可变的借用了,但是它们依然会在运行时进行借用规则检查。
use std::cell::RefCell;
use std::rc::Rc;
use crate::List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("{:?}", a); // Cons(RefCell { value: 15 }, Nil)
println!("{:?}", b); // Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
println!("{:?}", c); // Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
}
除了RefCell<T>
,Rust还提供了Cell<T>
(通过复制来访问数据)、Mutex<T>
(用于跨线程情景下的内部可变性模式)等可以实现内部可变性的类型。
5 循环引用导致的内存溢出
使用Rc<T>
和RefCell<T>
可以形成“a引用b,同时b也引用了a”的循环引用,这样当变量走出作用域时,两个变量的引用计数都不是0,所以都不会被释放:
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
// 取出第二个元素(以引用形式,不取得所有权)
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count: {}", Rc::strong_count(&a));
println!("a next item: {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation: {}", Rc::strong_count(&a));
println!("b initial rc count: {}", Rc::strong_count(&b));
println!("a next item: {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a: {}", Rc::strong_count(&b));
println!("a rc count after changing a: {}", Rc::strong_count(&a));
// println!("a next item:{:?}", a.tail()); // stack overflow
}
解决内存泄漏的方法:要么靠开发人员检查代码,要么重构代码逻辑,将引用分为持有所有权和不持有所有权两种:把Rc<T>
换成Weak<T>
。
Rc::clone()
会把Rc<T>
的实例的strong_count加一,Rc<T>
的实例只有在strong_count为0的时候才会被清理。Rc<T>
实例通过调用Rc::downgrade
方法可以创建值的Weak Reference(弱引用),返回值就是Weak<T>
。调用该方法会使得weak_count加1。Rc<T>
使用weak_count来追踪存在多少Weak<T>
,weak_count不为0不影响Rc<T>
实例的清理。
看下面这个例子:创建一个树结构,使得所有的节点都可以指向它的父节点,同时可以指向所有的子节点。
use std::cell::RefCell;
use std::hint::black_box;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
parent: RefCell::new(Weak::new()),
});
println!("leaf parent: {:?}", leaf.parent.borrow().upgrade()); // upgrade() 把 Weak<T>转化为Rc<T>
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]), // 树干指向叶子
parent: RefCell::new(Weak::new()),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch); // 叶子指向树干
println!("leaf parent: {:?}", leaf.parent.borrow().upgrade());
}