介绍
相关概念
指针:一个变量在内存中包含的是另一个地址(指向其他数据)
Rust中最常见的指针就是“引用”
引用:
使用&
借用它指向的值
没有其余的开销
智能指针
智能指针是这样的一些数据结构:
行为和指针相似
有额外的元数据和功能
引用计数(reference counting)智能指针类型
通过记录所有者的数量,使一份数据被多个所有者同时持有
并在没有任何所有者时自动清理数据
引用和智能指针的其他不同
引用:只借用数据
智能指针:很多时候都拥有它所指向的数据
智能指针的例子
String和Vec<T>
都拥有一片内存区域,且允许用户对其操作
还拥有元数据(例如容量等)
提供额外的功能或保障(String保障其数据是合法的UTF-8编码)
智能指针的实现
智能指针通常使用struct实现,并且实现了:
Deref和Drop这两个trait
Deref trait:允许智能指针struct的实例像引用一样使用
Drop trait:允许你自定义当智能指针实例走出作用域时的代码
使用Box指向Heap上的数据
使用Box<T>来指向Heap(堆内存)上的数据
Box<T>
Box<T>是最简单的智能指针:
允许你在heap上存储数据(而不是stack)
stack上是指向heap数据的指针
没有性能开销
没有其他额外的功能
实现了Deref trait和Drop trait
Box<T>的常用场景
在编译时,某类型的大小无法确定。但使用该类型时,上下文却需要知道它的确切大小。
当你有大量数据,想移交所有权,但需要确保在操作时数据不会被复制。
使用某个值时,你只关心它是否实现了特定的trait,而不是它的具体类型。
使用Box<T>在Heap上存储数据
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
使用Box 赋能递归类型
在编译时,Rust需要知道一个类型所占的空间大小。
而递归类型的大小无法在编译时确定
但Box类型大小确定
在递归类型中使用Box就可以解决上述问题
函数语言中的Cons list
关于Cons List
Cons List是来自Lisp语言的一种数据结构。
Cons List里每一个成员由两个元素组成。
当前项的值
下一个元素
Cons List里最后一个成员只包含一个Nil值(相当于终止的标记),没有下一个元素
Cons List并不是Rust的常用集合
通常情况下,Vec<T>是更好的选择
创建一个Cons List
Try:
use crate::List::{ Cons, Nil };
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
enum List {
Cons(i32, List),
Nil,
}
Rust如何确定为枚举分配的空间大小
enum Message {
Quit,
Move {x:i32, y:i32},
Write(String),
ChangeColor(i32, i32, i32),
}
使用Box来获得确定大小的递归类型
Box<T>是一个指针,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,
}
Box<T>:
只提供了“间接”存储和heap内存分配的功能
没有其他额外功能
没有性能开销
适用于需要“间接”存储的场景,例如Cons List
实现了Deref trait和Drop trait
(如果rust结构体包含自身的时候最好用引用是吗?如果必须拥有成员的所有权就得使用box这种?)
Deref Trait
实现Deref Trait使我们可以自定义解引用运算符*的行为。
通过实现Deref,智能指针可像常规引用一样来处理
解引用运算符
常规引用是一种指针
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y); // *是解引用符号
}
把Box<T>当做引用
Box<T>可以替代上例中的引用
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // *是解引用符号
}
定义自己的智能指针
Box<T>被定义成拥有一个元素的tuple struct
(例子)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这个trait的关联类型
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
// let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // *是解引用符号
// *(y.deref())
}
(Deref本质是引用的转换,把自身的引用转换成内部数据的引用。)
函数和方法的隐式解引用转化(Deref Coercion)
隐式解引用转化(Deref Coercion)是为函数和方法提供的一种便捷特性
假设T实现了Deref Trait:
Deref Coercion可以把T的引用转化为T经过Deref操作后生成的引用
当把某类型的引用传递给函数或方法时,但它的类型与定义的参数类型不匹配:
Deref Coercion就会自动发生
编译器会对deref进行一系列调用,来把它转化为所需的参数类型
在编译时完成,没有额外性能开销
use std::ops::Deref;
fn hello(name: &str) {
println!("Hello, {}", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
// &m &MyBox<String>
// deref &String
// deref &str
hello(&m);
// hello(&(*m)[ .. ]);
}
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这个trait的关联类型
fn deref(&self) -> &T {
&self.0
}
}
解引用与可变性
可使用DerefMut trait重载可变引用的*运算符
在类型和trait在下列三种情况发生时,Rust会执行deref coercion:
Drop Trait
实现Drop Trait,可以让我们自定义当值将要离开作用域时发生的动作
例如:文件、网络资源释放等
任何类型都可以实现Drop Trait
Drop trait只要求你实现drop方法
参数:对self的可变引用
Drop trait在预导入模块里(prelude)
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!("CustomSmartPointers created.");
}
使用std::mem::drop来提前drop值
很难直接禁用自动的drop功能,也没必要
Drop trait的目的就是进行自动的释放处理逻辑
Rust不允许手动调用Drop trait的drop方法
但可以使用标准库的std::mem::drop函数,来提前drop值
Rc<T>:引用计数智能指针
Rc<T>:引用计数智能指针
有时,一个值会有多个所有者
为了支持多重所有权:Re<T>
reference counting(引用计数)
追踪所有值得引用
0个引用:该值可以被清理掉
Re<T>使用场景
需要在heap上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定那个部分最后使用完这些数据
Re<T>只能用于单线程场景
例子
Re<T>不在预导入模块(prelude)
Re::clone(&a)函数:增加引用计数
Re::strong_count(&a):获得引用计数
还有Re::weak_count函数
例子
两个List共享另一个List所有权
use crate::List::{Cons, Nil};
use std::rc::Rc;
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 crate::List::{Cons, Nil};
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil
}
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));
}
Rc<T>
Rc<T>通过不可变引用,使你可以在程序不同部分之间共享只读数据
RefCell<T>和内部可变性
内部可变性
内部可变性时Rust的设计模式之一
它允许你在支持有不可变引用的前提下对数据进行修改
数据结构中使用了unsafe代码来绕过rust正常的可变性和借用规则
RefCell<T>
与Re<T>不同,RefCell<T>类型代表了其持有数据的唯一所有权
RefCell<T>与Box<T>的区别
借用规则在不同阶段进行检查的比较
RefCell<T>也只能用于单线程场景
选择Box<T>, Rc<T>,RefCell<T>的依据
内部可变性:可变的借用一个不可变的值
使用RefCell<T>在运行时记录借用信息
两个方法(安全接口)
borrow方法
返回智能指针Ref<T>,它实现了Deref
borrow_mut方法
返回智能指针RefMut<T>,它实现了Deref
将Rc<T>和RefCell<T>结合使用来实现一个拥有多重所有权的可变数据
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{ Cons, Nil };
use std::rc::Rc;
use std::cell::RefCell;
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 after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
其他可实现内部可变性的类型
Cell<T>:通过复制来访问数据
Mutex<T>:用于实现跨线程情形下的内部可变性模式
循环引用可导致内存泄漏
Rust可能发生内存泄漏
Rust的内存安全机制可以保证很难发生内存泄漏,但不是不可能
例如使用Re<T>和RefCell<T>就可能创造出循环引用,从而发生内存泄漏:
每个项的引用数量不会变成0,值也不会被处理掉。
例子:
防止内存泄漏的解决办法
依靠开发者来保证,不能依靠Rust
重组数据结构:一些引用来表达所有权,一些引用不表达所有权
循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系
而只有所有权关系才影响值得清理
防止循环引用:把Rc<T>换成Weak<T>
Rc::clone为Rc<T>实例的strong_count加1,Rc<T>的实例只有在strong_count为0的时候才会被清理
Rc<T>实例通过调用Rc::downgrade方法可以创建值得Weak Reference(弱引用)
返回类型Weak<T>(智能指针)
调用Rc::downgrade会为weak_count加1
Rc<T>使用weak_count来追踪存在多少Weak<T>
weak_count不为0并不影响Rc<T>实例的清理
Strong Vs Weak
总结
接着奏乐接着舞。
所谓成长,不过是用时间慢慢擦亮你的眼睛,少时看重的,年长后却视若鸿毛,少时看轻的,年长后却视若泰山,成长之路,亦是渐渐放下执念,内心归于平静的旅程。也许,我们永远都不会知道自己走向何方,遇见何人,最后会变成什么样的人,但请一定要记住,能让自己登高的,永远不是别人的肩膀。人生的道路刚刚启程,接下来的旅程全由你自己选择,而当你累了倦了也不要迷茫,回头看一看,家里的大门永远为你敞开。
--萧炎之父萧战