揭開智能指針 Box 的神秘面紗
本文轉(zhuǎn)載自微信公眾號(hào)「董澤潤的技術(shù)筆記」,作者董澤潤。轉(zhuǎn)載本文請(qǐng)聯(lián)系董澤潤的技術(shù)筆記公眾號(hào)。
熟悉 c++ 的肯定知道 shared_ptr, unique_ptr, 而 Rust 也有智能指針 Box, Rc, Arc, RefCell 等等,本文分享 Box 底層實(shí)現(xiàn)
Box
入門例子
例子來自 the rust book, 為了演示方便,去掉打印語句
- fn main() {
- let _ = Box::new(0x11223344);
- }
將變量 0x11223344 分配在堆上,所謂的裝箱,java 同學(xué)肯定很熟悉。讓我們掛載 docker, 使用 rust-gdb 查看匯編實(shí)現(xiàn)
- Dump of assembler code for function hello_cargo::main:
- 0x000055555555bdb0 <+0>: sub $0x18,%rsp
- 0x000055555555bdb4 <+4>: movl $0x11223344,0x14(%rsp)
- => 0x000055555555bdbc <+12>: mov $0x4,%esi
- 0x000055555555bdc1 <+17>: mov %rsi,%rdi
- 0x000055555555bdc4 <+20>: callq 0x55555555b5b0 <alloc::alloc::exchange_malloc>
- 0x000055555555bdc9 <+25>: mov %rax,%rcx
- 0x000055555555bdcc <+28>: mov %rcx,%rax
- 0x000055555555bdcf <+31>: movl $0x11223344,(%rcx)
- 0x000055555555bdd5 <+37>: mov %rax,0x8(%rsp)
- 0x000055555555bdda <+42>: lea 0x8(%rsp),%rdi
- 0x000055555555bddf <+47>: callq 0x55555555bd20 <core::ptr::drop_in_place<alloc::boxed::Box<i32>>>
- 0x000055555555bde4 <+52>: add $0x18,%rsp
- 0x000055555555bde8 <+56>: retq
- End of assembler dump.
關(guān)鍵點(diǎn)就兩條,alloc::alloc::exchange_malloc 在堆上分配內(nèi)存空間,然后將 0x11223344 存儲(chǔ)到這個(gè) malloc 的地址上
函數(shù)結(jié)束時(shí),將地址傳遞給 core::ptr::drop_in_place 去釋放,因?yàn)榫幾g器知道類型是 alloc::boxed::Box
單純的看這個(gè)例子,Box 并不神秘,對(duì)應(yīng)匯編實(shí)現(xiàn),和普通指針沒區(qū)別,一切約束都是編譯期行為
所有權(quán)
- fn main() {
- let x = Box::new(String::from("Rust"));
- let y = *x;
- println!("x is {}", x);
- }
這個(gè)例子中將字符串裝箱,其實(shí)沒必要這么寫,因?yàn)?String 廣義來講本身就是一種智能指針。這個(gè)例子會(huì)報(bào)錯(cuò)
- 3 | let y = *x;
- | -- value moved here
- 4 | println!("x is {}", x);
- | ^ value borrowed here after move
*x 解引用后對(duì)應(yīng) String, 賦值給 y 時(shí)執(zhí)行 move 語義,所有權(quán)不在了,所以后續(xù) println 不能打印 x
- let y = &*x;
可以取字符串的不可變引用來 fix
底層實(shí)現(xiàn)
- pub struct Box<
- T: ?Sized,
- #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
- >(Unique<T>, A);
上面是 Box 的定義,可以看到是一個(gè)元組結(jié)構(gòu)體,有兩個(gè)泛型參數(shù):T 代表任意類型,A 代表內(nèi)存分配器。標(biāo)準(zhǔn)庫里 A 是 Gloal 默認(rèn)值。其中 T 有一個(gè)泛型約束 ?Sized, 表示在編譯時(shí)可能知道類型大小,也可能不知道,當(dāng)然一般都用于不知道大小的場(chǎng)景,很少像上文一樣存儲(chǔ) int
- #[stable(feature = "rust1", since = "1.0.0")]
- unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Box<T, A> {
- fn drop(&mut self) {
- // FIXME: Do nothing, drop is currently performed by compiler.
- }
- }
這是 Drop 實(shí)現(xiàn),源碼里也說了,由編譯器實(shí)現(xiàn)
- #[stable(feature = "rust1", since = "1.0.0")]
- impl<T: ?Sized, A: Allocator> Deref for Box<T, A> {
- type Target = T;
- fn deref(&self) -> &T {
- &**self
- }
- }
- #[stable(feature = "rust1", since = "1.0.0")]
- impl<T: ?Sized, A: Allocator> DerefMut for Box<T, A> {
- fn deref_mut(&mut self) -> &mut T {
- &mut **self
- }
- }
實(shí)現(xiàn)了 Deref 可以定義解引用行為,DerefMut 可變解引用。所以 *x 對(duì)應(yīng)著操作 *(x.deref())
適用場(chǎng)景
官網(wǎng)提到以下三個(gè)場(chǎng)景,本質(zhì)上 Box 和普通指針區(qū)別不大,所以用處不如 Rc, Arc, RefCell 廣
- 當(dāng)類型在編譯期不知道大小,但代碼場(chǎng)景還要求確認(rèn)類型大小的時(shí)候
- 當(dāng)你有大量數(shù)據(jù),需要移動(dòng)所有權(quán),而不想 copy 數(shù)據(jù)的時(shí)候
- trait 對(duì)象,或者稱為 dyn 動(dòng)態(tài)分發(fā)常用在一個(gè)集合中存儲(chǔ)不同的類型上,或者參數(shù)指定不同的類型
官網(wǎng)有一個(gè)鏈表的實(shí)現(xiàn)
- enum List {
- Cons(i32, List),
- Nil,
- }
上面代碼是無法運(yùn)行的,道理也很簡(jiǎn)單,這是一種遞歸定義。對(duì)應(yīng) c 代碼也是不行的,我們一般要給 next 類型定義成指針才行
- enum List {
- Cons(i32, Box<List>),
- Nil,
- }
- use crate::List::{Cons, Nil};
- fn main() {
- let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
- }
官網(wǎng)給的解決方案,就是將 next 變成了指針 Box
, 算是常識(shí)吧,沒什么好說的