揭開智能指針 Box 的神祕面紗
熟悉 c++ 的肯定知道 shared_ptr
, unique_ptr
, 而 Rust 也有智能指針 Box
, Rc
, Arc
, RefCell
等等,本文分享 Box
底層實現
Box<T>
會在堆上分配空間,存儲 T 值,並返回對應的指針。同時 Box
也實現了 trait Deref
解引用和 Drop
析構,當 Box
離開作用域時自動釋放空間
入門例子
例子來自 the rust book, 爲了演示方便,去掉打印語句
fn main() {
let _ = Box::new(0x11223344);
}
將變量 0x11223344 分配在堆上,所謂的裝箱,java 同學肯定很熟悉。讓我們掛載 docker, 使用 rust-gdb 查看彙編實現
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.
關鍵點就兩條,alloc::alloc::exchange_malloc
在堆上分配內存空間,然後將 0x11223344
存儲到這個 malloc 的地址上
函數結束時,將地址傳遞給 core::ptr::drop_in_place
去釋放,因爲編譯器知道類型是 alloc::boxed::Box<i32>
, 會掉用 Box
相應的 drop 函數
單純的看這個例子,****Box
並不神祕,對應彙編實現,和普通指針沒區別,一切約束都是編譯期行爲
所有權
fn main() {
let x = Box::new(String::from("Rust"));
let y = *x;
println!("x is {}", x);
}
這個例子中將字符串裝箱,其實沒必要這麼寫,因爲 String
廣義來講本身就是一種智能指針。這個例子會報錯
3 | let y = *x;
| -- value moved here
4 | println!("x is {}", x);
| ^ value borrowed here after move
*x
解引用後對應 String
, 賦值給 y 時執行 move 語義,所有權不在了,所以後續 println 不能打印 x
let y = &*x;
可以取字符串的不可變引用來 fix
底層實現
pub struct Box<
T: ?Sized,
#[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
>(Unique<T>, A);
上面是 Box
的定義,可以看到是一個元組結構體,有兩個泛型參數:T
代表任意類型,A
代表內存分配器。標準庫裏 A
是 Gloal 默認值。其中 T
有一個泛型約束 ?Sized
, 表示在編譯時可能知道類型大小,也可能不知道,當然一般都用於不知道大小的場景,很少像上文一樣存儲 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
實現,源碼裏也說了,由編譯器實現
#[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
}
}
實現了 Deref
可以定義解引用行爲,DerefMut
可變解引用。所以 *x
對應着操作 *(x.deref())
適用場景
官網提到以下三個場景,本質上 Box
和普通指針區別不大,所以用處不如 Rc
, Arc
, RefCell
廣
-
當類型在編譯期不知道大小,但代碼場景還要求確認類型大小的時候
-
當你有大量數據,需要移動所有權,而不想 copy 數據的時候
-
trait 對象,或者稱爲 dyn 動態分發常用在一個集合中存儲不同的類型上,或者參數指定不同的類型
官網有一個鏈表的實現
enum List {
Cons(i32, List),
Nil,
}
上面代碼是無法運行的,道理也很簡單,這是一種遞歸定義。對應 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))))));
}
官網給的解決方案,就是將 next 變成了指針 Box<List>
, 算是常識吧,沒什麼好說的
公衆號
小結
寫文章不容易,如果對大家有所幫助和啓發,請大家幫忙點擊在看
,點贊
,分享
三連
關於 Box
大家有什麼看法,歡迎留言一起討論,大牛多留言 ^_^
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/-B0PD2kKGZB2RjxeIih4_A