系列文章目錄
【跟小嘉學(xué) Rust 編程】一、Rust 編程基礎(chǔ)
【跟小嘉學(xué) Rust 編程】二、Rust 包管理工具使用
【跟小嘉學(xué) Rust 編程】三、Rust 的基本程序概念
【跟小嘉學(xué) Rust 編程】四、理解 Rust 的所有權(quán)概念
【跟小嘉學(xué) Rust 編程】五、使用結(jié)構(gòu)體關(guān)聯(lián)結(jié)構(gòu)化數(shù)據(jù)
【跟小嘉學(xué) Rust 編程】六、枚舉和模式匹配
【跟小嘉學(xué) Rust 編程】七、使用包(Packages)、單元包(Crates)和模塊(Module)來管理項目
【跟小嘉學(xué) Rust 編程】八、常見的集合
【跟小嘉學(xué) Rust 編程】九、錯誤處理(Error Handling)
【跟小嘉學(xué) Rust 編程】十一、編寫自動化測試
【跟小嘉學(xué) Rust 編程】十二、構(gòu)建一個命令行程序
【跟小嘉學(xué) Rust 編程】十三、函數(shù)式語言特性:迭代器和閉包
【跟小嘉學(xué) Rust 編程】十四、關(guān)于 Cargo 和 Crates.io
【跟小嘉學(xué) Rust 編程】十五、智能指針
前言
指針是一個包含了內(nèi)存地址的變量,該內(nèi)存地址引用或執(zhí)行了另外的數(shù)據(jù)。在Rust中最常見的指針類型就是引用。不同的是在Rust中引用被賦予更深的含義就是借用其他變量的值。
主要教材參考 《The Rust Programming Language》
一、智能指針
1.1、智能指針(smart point)
智能指針是一個復(fù)雜的數(shù)據(jù)結(jié)構(gòu),包含了比引用更多的信息,例如元數(shù)據(jù),當(dāng)前長度,最大可用長度等。
在之前章節(jié)實際上我們已經(jīng)見識過多種智能指針了,例如動態(tài)字符串 String 和動態(tài)數(shù)據(jù) Vec。
智能指針往往是基于結(jié)構(gòu)體實現(xiàn),它與我們自定義的結(jié)構(gòu)體最大的區(qū)別在于它實現(xiàn)了 Deref 和 Drop 特征:
- Deref:可以讓智能指針像引用那樣工作,這樣你就可以寫出同時支持智能指針和引用的代碼,例如 *T
- Drop:允許你就指定智能指針超出作用域后自動執(zhí)行的代碼,例如數(shù)據(jù)清理等收尾工作
1.2、Box 堆內(nèi)存分配
在Rust 中,所有值默認(rèn)都是在棧內(nèi)存上分配,通過創(chuàng)建 Box<T>
可用把值裝箱,使它在堆上分配。Box<T>
是一個智能指針,因為它實現(xiàn)了 Deref trait,它允許Box<T>
值被當(dāng)作引用對待,當(dāng) Box<T>
值離開作用域時,由于它實現(xiàn)了 Drop trait ,首先刪除其指向堆堆數(shù)據(jù),然后刪除自身。
使用場景
- 在編譯時,某類型的大小無法確定,但使用該類型時,上下文卻需要知道它確切的大??;
- 當(dāng)你有大量數(shù)據(jù),想移交所有權(quán),但需要確保在操作時數(shù)據(jù)不會被復(fù)制;
- 使用某個值,你只關(guān)心它是否實現(xiàn)了特定的 trait ,而不關(guān)心它的具體類型;
1.2.1、場景1:堆內(nèi)存上分配數(shù)據(jù)
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 的主要特性是單一所有權(quán),即同時智能有一個人擁有對其指向數(shù)據(jù)的所有權(quán),并且同時智能存在一個可變引用或多個不可變引用,這一點與Rust中其他屬于堆上的數(shù)據(jù)行為一致。
1.2.2、場景2: cons list
cons list 是來自 Lisp 語言的一種數(shù)據(jù)結(jié)構(gòu)。cons list 里面每個成員都包含兩個元素:當(dāng)前項都值和下一個元素。cons list 里的最后一個成員只包含一個 nil 值,沒有下一個元素。
Box<T>
是一個指針,Rust知道它需要多少空間,因為指針的大小不會基于它指向的數(shù)據(jù)的大小變化而變化。
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,
}
1.3、Deref 解引用
1.3.1、Deref trait
Deref Trait 允許我們重載解引用運算符 *
。實現(xiàn) Deref 的智能指針可以被當(dāng)作引用來對待,也就是說可以對智能指針使用 *
運算符來解引用。
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Deref for Box<T> {
type Target = T;
fn deref(&self) -> &T {
&**self
}
}
1.3.2、三種 Deref 轉(zhuǎn)換
在之前,我們講的都是不可變的 Deref 轉(zhuǎn)換,實際上 Rust 還支持將一個可變的引用轉(zhuǎn)換成另一個可變的引用以及將一個可變引用轉(zhuǎn)換成不可變的引用,規(guī)則如下:
當(dāng) T: Deref<Target=U>,可以將 &T 轉(zhuǎn)換成 &U,也就是我們之前看到的例子
當(dāng) T: DerefMut<Target=U>,可以將 &mut T 轉(zhuǎn)換成 &mut U
當(dāng) T: Deref<Target=U>,可以將 &mut T 轉(zhuǎn)換成 &U
1.4、Drop 釋放資源
1.4.1、Drop trait
Drop trait 主要作用是釋放實現(xiàn)者實例擁有的資源,它只有一個方法 drop。當(dāng)實例離開作用域時會自動調(diào)用該方法,從而調(diào)用實現(xiàn)者指定的代碼。
#[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.
}
}
1.4.2、使用 std::mem::drop 來提前 drop
Rust 不允許手動調(diào)用 Drop trait 的 drop 方法,但是可以 使用標(biāo)準(zhǔn)庫的 std::mem::drop 來提前 drop。
1.5、引用計數(shù)智能指針(RC<T> 和 Arc<T>)
1.5.1、RC<T>
RC<T>
主要用于同一個堆上所有分配的數(shù)據(jù)區(qū)域需要多個只讀訪問的情況,比起使用比起使用 Box<T>
然后創(chuàng)建多個不可變引用的方法更優(yōu)雅也更直觀一些,以及比起單一所有權(quán),Rc<T>
支持多所有權(quán)。
Rc 為 Reference Counter 的縮寫,即為引用計數(shù),Rust 的 Runtime 會實時記錄一個 Rc<T>
當(dāng)前被引用的次數(shù),并在引用計數(shù)歸零時對數(shù)據(jù)進(jìn)行釋放(類似 Python 的 GC 機制)。因為需要維護(hù)一個記錄 Rc<T>
類型被引用的次數(shù),所以這個實現(xiàn)需要 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<T>
是完全不可變,可以理解為同一個內(nèi)存上的數(shù)據(jù)同時存在多個只讀指針 -
RC<T>
只適用單線程,盡管從概念上講不同線程間只讀指針是完全安全的,但是由于RC<T>
沒有實現(xiàn)多個線程間保證計數(shù)一致性,如果你嘗試多線程內(nèi)使用,會報錯;
1.5.1、原子引用計數(shù)(Atomic reference counter)
此時引用計數(shù)就可以在不同線程中安全的被使用了。
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();
}
1.6、Cell 與 RefCell 內(nèi)部可變性
1.6.1、內(nèi)部可變性(interior mutability)
內(nèi)部可變性(interior mutability) 是 Rust 的設(shè)計模式之一,它允許你在只持有不可變引用的前提下對數(shù)據(jù)進(jìn)行修改,數(shù)據(jù)結(jié)構(gòu)中使用了 unsafe 代碼來繞過 Rust 正常的可變性和借用規(guī)則。
1.6.2、Cell
Cell 和 Refcell 在功能上沒有區(qū)別,區(qū)別在于 Cell 適用于 T 實現(xiàn) Copy 的情況
1.6.3、RefCell
由于 Cell 類型針對的是實現(xiàn)了 Copy 特征的值類型,因此在實際開發(fā)中,Cell 使用的并不多,因為我們要解決的往往是可變、不可變引用共存導(dǎo)致的問題,此時就需要借助于 RefCell 來達(dá)成目的。
Rust 規(guī)則 | 智能指針帶來的額外規(guī)則 |
---|---|
一個數(shù)據(jù)只有一個所有者 | Rc/Arc 讓一個數(shù)據(jù)可以擁有多個所有者 |
要么多個不可變借用,要么一個可變借用 | RefCell 實現(xiàn)編譯器可變、不可變引用共存 |
違背規(guī)則導(dǎo)致編譯錯誤 | 違背規(guī)則導(dǎo)致運行時 panic |
可以看出,Rc/Arc 和 RefCell 合在一起,解決了 Rust 中嚴(yán)苛的所有權(quán)和借用規(guī)則帶來的某些場景下難使用的問題。但是它們并不是銀彈,例如 RefCell 實際上并沒有解決可變引用和引用可以共存的問題,只是將報錯從編譯期推遲到運行時,從編譯器錯誤變成了 panic 異常:
1.6.4、Cell 和 RefCell
- Cell 只適用于 Copy 類型,用于提供值,而RefCell 用于提供引用
- Cell 不會panic ,而 RefCell 會
- Cell 沒有額外的性能損耗
從 CPU 來看,損耗如下:
- 對 Rc 解引用是免費的(編譯期),但是 * 帶來的間接取值并不免費
- 克隆 Rc 需要將當(dāng)前的引用計數(shù)跟 0 和 usize::Max 進(jìn)行一次比較,然后將計數(shù)值加 1
- 釋放(drop) Rc 需要將計數(shù)值減 1, 然后跟 0 進(jìn)行一次比較
- 對 RefCell 進(jìn)行不可變借用,需要將 isize 類型的借用計數(shù)加 1,然后跟 0 進(jìn)行比較
- 對 RefCell 的不可變借用進(jìn)行釋放,需要將 isize 減 1
- 對 RefCell 的可變借用大致流程跟上面差不多,但是需要先跟 0 比較,然后再減 1
- 對 RefCell 的可變借用進(jìn)行釋放,需要將 isize 加 1
1.6.5、解決借用沖突
在 Rust 1.37 版本中新增了兩個非常實用的方法:
- Cell::from_mut,該方法將 &mut T 轉(zhuǎn)為 &Cell
- Cell::as_slice_of_cells,該方法將 &Cell<[T]> 轉(zhuǎn)為 &[Cell]
1.7、Weak 和引用循環(huán)
1.7.1、引用循環(huán)和內(nèi)存泄漏
Rust 的內(nèi)存安全機制可以保證很難發(fā)生內(nèi)存泄漏。但是不代表不會內(nèi)存泄漏。一個典型的例子就是同時使用 Rc 和 RefCell 創(chuàng)建循環(huán)引用,最終這些引用的計數(shù)都無法被歸零,因此 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計數(shù) = {}", Rc::strong_count(&a));
println!("a指向的節(jié)點 = {:?}", a.tail());
// 創(chuàng)建`b`到`a`的引用
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("在b創(chuàng)建后,a的rc計數(shù) = {}", Rc::strong_count(&a));
println!("b的初始化rc計數(shù) = {}", Rc::strong_count(&b));
println!("b指向的節(jié)點 = {:?}", b.tail());
// 利用RefCell的可變性,創(chuàng)建了`a`到`b`的引用
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("在更改a后,b的rc計數(shù) = {}", Rc::strong_count(&b));
println!("在更改a后,a的rc計數(shù) = {}", Rc::strong_count(&a));
// 下面一行println!將導(dǎo)致循環(huán)引用
// 我們可憐的8MB大小的main線程??臻g將被它沖垮,最終造成棧溢出
// println!("a next item = {:?}", a.tail());
}
如何防止循環(huán)引用
- 開發(fā)者去注意細(xì)節(jié)
- 使用 Weak
1.7.2、Weak
Weak 類似 RC 但是和 RC持有所有權(quán)不同,Weak 不必持有所有權(quán),僅僅保存一份指向數(shù)據(jù)的弱引用,如果你要想訪問數(shù)據(jù),需要通過 Weak 指針的 upgrade 方法實現(xiàn),該方法返回個類型為 Option<Rc<T>>
的值。
所謂弱引用就是不保證引用關(guān)系存在,如果不存在,就返回None。
因為 Weak 引用不計入所有權(quán),因此它無法阻止所引用的內(nèi)存值被釋放掉,而且 Weak 本身不對值的存在性做任何擔(dān)保,引用的值還存在就返回 Some,不存在就返回 None。
Weak | RC |
---|---|
不計數(shù) | 計數(shù) |
不擁有所有權(quán) | 擁有值的所有權(quán) |
不阻止值被釋放(drop) | 所有權(quán)計數(shù)歸零,才能drop |
引用存在返回some,不存在返回None | 引用值必定存在 |
通過 upgrade 取到Option<Rc<T>> 再取值 |
通過 Deref 自動解引用,取值無需任何操作 |
弱引用非常適合如下場景
- 持有一個 Rc 對象的臨時引用,并且不在乎引用的值是否依然存在
- 阻止 Rc 導(dǎo)致的循環(huán)引用,因為 Rc 的所有權(quán)機制,會導(dǎo)致多個 Rc 都無法計數(shù)歸零
1.7.3、unsafe
除了使用 Rust 標(biāo)準(zhǔn)庫提供的這些類型,你還可以使用 unsafe 里的裸指針來解決這些棘手的問題,但是由于我們還沒有講解 unsafe。
雖然 unsafe 不安全,但是在各種庫的代碼中依然很常見用它來實現(xiàn)自引用結(jié)構(gòu),主要優(yōu)點如下:文章來源:http://www.zghlxwxcb.cn/news/detail-674971.html
- 性能高,畢竟直接用裸指針操作
- 代碼更簡單更符合直覺: 對比下
Option<Rc<RefCell<Node>>>
總結(jié)
以上就是今天要講的內(nèi)容文章來源地址http://www.zghlxwxcb.cn/news/detail-674971.html
到了這里,關(guān)于【跟小嘉學(xué) Rust 編程】十五、智能指針的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!