【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序
【跟小嘉学 Rust 编程】十三、函数式语言特性:迭代器和闭包
【跟小嘉学 Rust 编程】十四、关于 Cargo 和 Crates.io
【跟小嘉学 Rust 编程】十五、智能指针
指针是一个包含了内存地址的变量,该内存地址引用或执行了另外的数据。在Rust中最常见的指针类型就是引用。不同的是在Rust中引用被赋予更深的含义就是借用其他变量的值。
主要教材参考 《The Rust Programming Language》
智能指针是一个复杂的数据结构,包含了比引用更多的信息,例如元数据,当前长度,最大可用长度等。
在之前章节实际上我们已经见识过多种智能指针了,例如动态字符串 String 和动态数据 Vec。
智能指针往往是基于结构体实现,它与我们自定义的结构体最大的区别在于它实现了 Deref 和 Drop 特征:
在Rust 中,所有值默认都是在栈内存上分配,通过创建 Box
可用把值装箱,使它在堆上分配。Box
是一个智能指针,因为它实现了 Deref trait,它允许Box
值被当作引用对待,当 Box
值离开作用域时,由于它实现了 Drop trait ,首先删除其指向堆堆数据,然后删除自身。
使用场景
fn main() {
let a = Box::new(1); // Immutable
println!("{}", a); // Output: 1
let mut b = Box::new(1); // Mutable
*b += 1;
println!("{}", b); // Output: 2
}
Box 的主要特性是单一所有权,即同时智能有一个人拥有对其指向数据的所有权,并且同时智能存在一个可变引用或多个不可变引用,这一点与Rust中其他属于堆上的数据行为一致。
cons list 是来自 Lisp 语言的一种数据结构。cons list 里面每个成员都包含两个元素:当前项都值和下一个元素。cons list 里的最后一个成员只包含一个 nil 值,没有下一个元素。
Box
是一个指针,Rust知道它需要多少空间,因为指针的大小不会基于它指向的数据的大小变化而变化。
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3,Box::new(Nil))))));
}
enum List {
Cons(i32, Box<List>),
Nil,
}
Deref Trait 允许我们重载解引用运算符 *
。实现 Deref 的智能指针可以被当作引用来对待,也就是说可以对智能指针使用 *
运算符来解引用。
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Deref for Box<T> {
type Target = T;
fn deref(&self) -> &T {
&**self
}
}
在之前,我们讲的都是不可变的 Deref 转换,实际上 Rust 还支持将一个可变的引用转换成另一个可变的引用以及将一个可变引用转换成不可变的引用,规则如下:
当 T: Deref
当 T: DerefMut
当 T: Deref
Drop trait 主要作用是释放实现者实例拥有的资源,它只有一个方法 drop。当实例离开作用域时会自动调用该方法,从而调用实现者指定的代码。
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized> Drop for Box<T> {
fn drop(&mut self) {
// FIXME: Do nothing, drop is currently performed by compiler.
}
}
Rust 不允许手动调用 Drop trait 的 drop 方法,但是可以 使用标准库的 std::mem::drop 来提前 drop。
RC
和 Arc
)RC
RC
主要用于同一个堆上所有分配的数据区域需要多个只读访问的情况,比起使用比起使用 Box
然后创建多个不可变引用的方法更优雅也更直观一些,以及比起单一所有权,Rc
支持多所有权。
Rc 为 Reference Counter 的缩写,即为引用计数,Rust 的 Runtime 会实时记录一个 Rc
当前被引用的次数,并在引用计数归零时对数据进行释放(类似 Python 的 GC 机制)。因为需要维护一个记录 Rc
类型被引用的次数,所以这个实现需要 Runtime Cost。
use std::rc::Rc;
fn main() {
let a = Rc::new(1);
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Rc::clone(&a);
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Rc::clone(&a);
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
需要注意
RC
是完全不可变,可以理解为同一个内存上的数据同时存在多个只读指针RC
只适用单线程,尽管从概念上讲不同线程间只读指针是完全安全的,但是由于 RC
没有实现多个线程间保证计数一致性,如果你尝试多线程内使用,会报错;此时引用计数就可以在不同线程中安全的被使用了。
use std::thread;
use std::sync::Arc;
fn main() {
let a = Arc::new(1);
thread::spawn(move || {
let b = Arc::clone(&a);
println!("{}", b); // Output: 1
}).join();
}
内部可变性(interior mutability) 是 Rust 的设计模式之一,它允许你在只持有不可变引用的前提下对数据进行修改,数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性和借用规则。
Cell 和 Refcell 在功能上没有区别,区别在于 Cell 适用于 T 实现 Copy 的情况
由于 Cell 类型针对的是实现了 Copy 特征的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的。
Rust 规则 | 智能指针带来的额外规则 |
---|---|
一个数据只有一个所有者 | Rc/Arc 让一个数据可以拥有多个所有者 |
要么多个不可变借用,要么一个可变借用 | RefCell 实现编译器可变、不可变引用共存 |
违背规则导致编译错误 | 违背规则导致运行时 panic |
可以看出,Rc/Arc 和 RefCell 合在一起,解决了 Rust 中严苛的所有权和借用规则带来的某些场景下难使用的问题。但是它们并不是银弹,例如 RefCell 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 panic 异常:
从 CPU 来看,损耗如下:
在 Rust 1.37 版本中新增了两个非常实用的方法:
Rust 的内存安全机制可以保证很难发生内存泄漏。但是不代表不会内存泄漏。一个典型的例子就是同时使用 Rc 和 RefCell 创建循环引用,最终这些引用的计数都无法被归零,因此 Rc 拥有的值也不会被释放清理。
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的初始化rc计数 = {}", Rc::strong_count(&a));
println!("a指向的节点 = {:?}", a.tail());
// 创建`b`到`a`的引用
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("在b创建后,a的rc计数 = {}", Rc::strong_count(&a));
println!("b的初始化rc计数 = {}", Rc::strong_count(&b));
println!("b指向的节点 = {:?}", b.tail());
// 利用RefCell的可变性,创建了`a`到`b`的引用
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("在更改a后,b的rc计数 = {}", Rc::strong_count(&b));
println!("在更改a后,a的rc计数 = {}", Rc::strong_count(&a));
// 下面一行println!将导致循环引用
// 我们可怜的8MB大小的main线程栈空间将被它冲垮,最终造成栈溢出
// println!("a next item = {:?}", a.tail());
}
如何防止循环引用
Weak 类似 RC 但是和 RC持有所有权不同,Weak 不必持有所有权,仅仅保存一份指向数据的弱引用,如果你要想访问数据,需要通过 Weak 指针的 upgrade 方法实现,该方法返回个类型为 Option
的值。
所谓弱引用就是不保证引用关系存在,如果不存在,就返回None。
因为 Weak 引用不计入所有权,因此它无法阻止所引用的内存值被释放掉,而且 Weak 本身不对值的存在性做任何担保,引用的值还存在就返回 Some,不存在就返回 None。
Weak | RC |
---|---|
不计数 | 计数 |
不拥有所有权 | 拥有值的所有权 |
不阻止值被释放(drop) | 所有权计数归零,才能drop |
引用存在返回some,不存在返回None | 引用值必定存在 |
通过 upgrade 取到Option 再取值 |
通过 Deref 自动解引用,取值无需任何操作 |
弱引用非常适合如下场景
除了使用 Rust 标准库提供的这些类型,你还可以使用 unsafe 里的裸指针来解决这些棘手的问题,但是由于我们还没有讲解 unsafe。
虽然 unsafe 不安全,但是在各种库的代码中依然很常见用它来实现自引用结构,主要优点如下:
Option>>
以上就是今天要讲的内容