對(duì)比現(xiàn)代語法的高級(jí)語言如Java/Go/Python等冬三,Rust需要對(duì)內(nèi)存進(jìn)行控制尤勋,即程序可在代碼中編寫專屬內(nèi)存管理系統(tǒng)佣谐,并將內(nèi)存管理系統(tǒng)與語言類型相關(guān)聯(lián),在內(nèi)存塊與語言類型間能夠自如的進(jìn)行轉(zhuǎn)換涎劈。但相對(duì)于C來說,rust的現(xiàn)代語法特性及內(nèi)存安全會(huì)導(dǎo)致rust的內(nèi)存塊與類型系統(tǒng)的轉(zhuǎn)換細(xì)節(jié)相對(duì)非常復(fù)雜阅茶,不易被透徹理解蛛枚。接下來從代碼層面來深入理解rust內(nèi)存及內(nèi)存安全:
Rust標(biāo)準(zhǔn)庫內(nèi)存模塊代碼
%本地代碼路徑%/src\rust\library\core\src\alloc*.*
%本地代碼路徑%\src\rust\library\core\src\ptr*.*
%本地代碼路徑%\src\rust\library\core\src\mem*.*
%本地代碼路徑%\src\rust\library\core\src\intrinsic.rs
%本地代碼路徑%\src\rust\library\alloc\src\alloc.rs
從內(nèi)存角度來考察一個(gè)變量,則每個(gè)變量具備統(tǒng)一的內(nèi)存參數(shù):
- 變量的首地址脸哀,是一個(gè)usize的數(shù)值
- 變量類型占用的內(nèi)存塊大小
- 變量類型內(nèi)存字節(jié)對(duì)齊的基數(shù)
- 變量類型中成員內(nèi)存順序
若是變量成員是復(fù)合類型蹦浦,可遞歸上面的四個(gè)參數(shù).
rust與c的“異同”
不同于C,rust則認(rèn)為變量類型、成員順序與編譯優(yōu)化不可分割送漠,因此,變量成員內(nèi)存順序完全由編譯器控制,C中變量類型成員的順序是不能被編譯器改動(dòng)的博投。這使得C變量的內(nèi)存布局對(duì)程序員是透明的尿瞭。這種透明性導(dǎo)致了C語言在設(shè)計(jì)類型內(nèi)存布局的操作中會(huì)出現(xiàn)一些"壞代碼"
充石。如,直接用頭指針+偏移數(shù)值來獲得類型內(nèi)部變量的指針丛版,直接導(dǎo)致變量類型可修改性極差好芭。
但與C相同之處: rust能夠?qū)⒁粔K內(nèi)存塊直接轉(zhuǎn)換成某一類型變量恩尾。這也是rust能夠操作系統(tǒng)內(nèi)核編程及其高效的“核心”弛说。不過也因這個(gè)轉(zhuǎn)換使得代碼可以繞過編譯器的類型系統(tǒng)檢查,造成了bug也繞過了編譯器的某些錯(cuò)誤檢查翰意,而這些錯(cuò)誤很可能在系統(tǒng)運(yùn)行很久之后才真正的出錯(cuò)木人,造成排錯(cuò)的極高成本。
而rust在確保這些的同時(shí)冀偶,并通過明確標(biāo)識(shí)unsafe, 再加上整體的內(nèi)存安全框架設(shè)計(jì)醒第,使得此類錯(cuò)誤更易被發(fā)現(xiàn),更易被定位进鸠,極大的降低了錯(cuò)誤的數(shù)目及排錯(cuò)的成本稠曼。
不過unsafe讓初學(xué)rust語言的程序員產(chǎn)生“排斥”,但unsafe實(shí)際上是rust不可分割的部分客年,一個(gè)好的rust程序員絕不是不使用unsafe霞幅,而是能夠準(zhǔn)確的把握好unsafe使用的合適場(chǎng)合及合適范圍,必要的時(shí)候必須使用量瓜,但不濫用司恳。
關(guān)于rust中的“安全”與“不安全”
接下來為了掌握rust的內(nèi)存我們會(huì)從如下幾個(gè)部分入手:
- 編譯器提供的固有內(nèi)存操作函數(shù)
- 內(nèi)存塊與類型系統(tǒng)的結(jié)合點(diǎn):裸指針
*const T/*mut T
- 裸指針的包裝結(jié)構(gòu):
NonNull<T>/Unique<T>
- 未初始化內(nèi)存塊的處理:
MaybeUninit<T>/ManuallyDrop<T>
- 堆內(nèi)存申請(qǐng)及釋放
針對(duì)初始化變量相關(guān)的指針操作
關(guān)于裸指針
裸指針*const T/* mut T
將內(nèi)存和類型系統(tǒng)相關(guān)聯(lián):
1、 *const T代表了一個(gè)內(nèi)存塊绍傲,指示了內(nèi)存塊首地址扔傅,大小,對(duì)齊等屬性烫饼,以及元數(shù)據(jù)猎塞,但不保證這個(gè)內(nèi)存塊的有效性和安全性
。
2杠纵、與*const T/* mu T
不同: &T/&mut T
則保證內(nèi)存塊是安全和有效的邢享,這表示&T/&mut T
滿足內(nèi)存塊首地址對(duì)齊,內(nèi)存塊已經(jīng)完成了初始化淡诗。
在rust中骇塘,
&T/&mut T
是被綁定在某一內(nèi)存塊上伊履,只能對(duì)這一內(nèi)存塊讀寫。
對(duì)于內(nèi)存塊更復(fù)雜的操作款违,由*const T/*mut T
負(fù)責(zé)
主要有:
將usize類型數(shù)值強(qiáng)制轉(zhuǎn)換成裸指針類型唐瀑,以此數(shù)值為首地址的內(nèi)存塊被轉(zhuǎn)換為相應(yīng)的類型; 不過若是對(duì)這一轉(zhuǎn)換后的內(nèi)存塊進(jìn)行讀寫插爹,可能造成內(nèi)存安全問題哄辣。
在不同的裸指針類型之間進(jìn)行強(qiáng)制轉(zhuǎn)換,實(shí)質(zhì)上完成了裸指針指向的內(nèi)存塊的類型強(qiáng)轉(zhuǎn)赠尾,若是對(duì)這一轉(zhuǎn)換后的內(nèi)存塊進(jìn)行讀寫力穗,可能造成內(nèi)存安全問題。
*const u8
作為堆內(nèi)存申請(qǐng)的內(nèi)存塊綁定變量 气嫁。內(nèi)存塊置值操作当窗,如清零或置一個(gè)魔術(shù)值 。
顯式的內(nèi)存塊拷貝操作寸宵,某些情況下崖面,內(nèi)存塊拷貝是必須的高性能方式。
利用指針偏移計(jì)算獲取新的內(nèi)存塊梯影, 比如在數(shù)組及切片訪問巫员,字符串,協(xié)議字節(jié)填寫甲棍,文件緩存等都需要指針偏移計(jì)算简识。
從外部的C函數(shù)接口對(duì)接的指針參數(shù)。
等等
rust的裸指針類型不像C語言的指針類型那樣僅僅是一個(gè)地址值感猛,為滿足實(shí)現(xiàn)內(nèi)存安全的類型系統(tǒng)需求财异,并兼顧內(nèi)存使用效率和方便性,rust的裸指針實(shí)質(zhì)是一個(gè)較復(fù)雜的類型結(jié)構(gòu)體唱遭。
裸指針具體實(shí)現(xiàn)
*const T/*mut T
實(shí)質(zhì)是個(gè)結(jié)構(gòu)體戳寸,由兩個(gè)部分組成:第一個(gè)部分是一個(gè)內(nèi)存地址;第二個(gè)部分對(duì)這個(gè)內(nèi)存地址的約束性描述-元數(shù)據(jù)拷泽。
偽碼如下(并非真實(shí)的代碼定義)
struct Pointer {
address: usize, // 當(dāng)前裸指針的地址
metadata: T, // 針對(duì)當(dāng)前指針地址的描述
}
接下來看看rust關(guān)于這塊的定義疫鹊,從下面結(jié)構(gòu)定義可以看到,裸指針本質(zhì)就是PtrComponents<T>
pub(crate) union PtrRepr<T: ?Sized> {
pub(crate) const_ptr: *const T, // 只讀指針
pub(crate) mut_ptr: *mut T, // 可變指針
pub(crate) components: PtrComponents<T>, /
}
pub(crate) struct PtrComponents<T: ?Sized> {
//*const ()保證元數(shù)據(jù)部分是空
pub(crate) data_address: *const (),
//不同類型指針的元數(shù)據(jù)
pub(crate) metadata: <T as Pointee>::Metadata,
}
// Pointee只用來指定Metadata的類型司致。
pub trait Pointee {
/// The type for metadata in pointers and references to `Self`.
type Metadata: Copy + Send + Sync + Ord + Hash + Unpin;
}
// thin廋指針元數(shù)據(jù)是單元類型拆吆,即是空
pub trait Thin = Pointee<Metadata = ()>;
元數(shù)據(jù)的規(guī)則:
- 對(duì)于固定大小類型的指針(實(shí)現(xiàn)了
Sized
Trait), 在rust被定義為廋指針(thin pointer),元數(shù)據(jù)大小為0脂矫,類型為(),
需要注意的:rust中數(shù)組也是固定大小的類型枣耀,運(yùn)行中對(duì)數(shù)組下標(biāo)合法性的檢測(cè)比較是否已經(jīng)越過了數(shù)組的內(nèi)存大小。
- 對(duì)于動(dòng)態(tài)大小類型的指針(DST 類型)庭再,被定義為胖指針(fat pointer 或 wide pointer), 元數(shù)據(jù)為:
- 對(duì)于結(jié)構(gòu)類型捞奕,如果最后一個(gè)成員是動(dòng)態(tài)類型(struct中的其他成員不允許為動(dòng)態(tài)類型)牺堰,則元數(shù)據(jù)為此動(dòng)態(tài)類型的元數(shù)據(jù);
- 對(duì)于
str
類型, 元數(shù)據(jù)是按字節(jié)計(jì)算的長(zhǎng)度值颅围,元數(shù)據(jù)類型是usize伟葫; - 對(duì)于切片類型,例如
[T]
類型院促,元數(shù)據(jù)是數(shù)組元素的數(shù)目值筏养,元數(shù)據(jù)類型是usize; - 對(duì)于trait對(duì)象常拓,例如 dyn XXXTrait渐溶, 元數(shù)據(jù)則是DynMetadata<Self>
(例如:DynMetadata<dyn XXXTrait>);
伴隨著rust的發(fā)展弄抬,后期有可能會(huì)根據(jù)需要引入新的元數(shù)據(jù)種類茎辐。
在標(biāo)準(zhǔn)庫代碼當(dāng)中沒有指針類型如何實(shí)現(xiàn)Pointee Trait的代碼,編譯器針對(duì)每個(gè)類型自動(dòng)的實(shí)現(xiàn)了Pointee眉睹。
看看如下rust編譯器實(shí)現(xiàn)的代碼
pub fn ptr_metadata_ty(&'tcx self, tcx: TyCtxt<'tcx>) -> Ty<'tcx> {
// FIXME: should this normalize?
let tail = tcx.struct_tail_without_normalization(self);
match tail.kind() {
// Sized types
ty::Infer(ty::IntVar(_) | ty::FloatVar(_))
| ty::Uint(_)
| ty::Int(_)
| ty::Bool
| ty::Float(_)
| ty::FnDef(..)
| ty::FnPtr(_)
| ty::RawPtr(..)
| ty::Char
| ty::Ref(..)
| ty::Generator(..)
| ty::GeneratorWitness(..)
| ty::Array(..)
| ty::Closure(..)
| ty::Never
| ty::Error(_)
| ty::Foreign(..)
| ty::Adt(..)
// 當(dāng)是固定類型,元數(shù)據(jù)是單元類型 tcx.types.unit废膘,即為空
| ty::Tuple(..) => tcx.types.unit,
// 當(dāng)為字符串和切片類型竹海,元數(shù)據(jù)為長(zhǎng)度tcx.types.usize,是元素長(zhǎng)度
ty::Str | ty::Slice(_) => tcx.types.usize,
// 對(duì)于dyn Trait類型丐黄, 元數(shù)據(jù)從具體的DynMetadata獲取*
ty::Dynamic(..) => {
let dyn_metadata = tcx.lang_items().dyn_metadata().unwrap();
tcx.type_of(dyn_metadata).subst(tcx, &[tail.into()])
},
// 并不是所有的類型 都需要具有元數(shù)據(jù)的
// 以下類型不應(yīng)有元數(shù)據(jù)
ty::Projection(_)
| ty::Param(_)
| ty::Opaque(..)
| ty::Infer(ty::TyVar(_))
| ty::Bound(..)
| ty::Placeholder(..)
| ty::Infer(ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_)) => {
bug!("`ptr_metadata_ty` applied to unexpected type: {:?}", tail)
}
}
}
以上代碼說明了編譯器對(duì)每一個(gè)類型(或類型指針)都實(shí)現(xiàn)了Pointee中元數(shù)據(jù)類型的獲取斋配。
對(duì)于Trait對(duì)象的元數(shù)據(jù)的具體結(jié)構(gòu)定義見如下代碼:
//dyn Trait裸指針的元數(shù)據(jù)結(jié)構(gòu)
pub struct DynMetadata<Dyn: ?Sized> {
//堆中的VTable變量的引用
vtable_ptr: &'static VTable,
// 標(biāo)識(shí)結(jié)構(gòu)對(duì)Dyn的所有權(quán)關(guān)系,
//其中PhantomData與具體變量的聯(lián)系在初始化時(shí)由編譯器自行推斷完成,
// 這里PhantomData主要對(duì)編譯器做出提示:在做Drop check時(shí)注意本結(jié)構(gòu)體會(huì)負(fù)責(zé)對(duì)Dyn類型變量做drop灌闺。
phantom: crate::marker::PhantomData<Dyn>,
}
struct VTable {
//trait對(duì)象的drop方法的指針艰争,這里trait對(duì)象是一個(gè)具體的結(jié)構(gòu)體,它實(shí)現(xiàn)了trait
drop_in_place: fn(*mut ()),
//trait對(duì)象類型的內(nèi)存大小
size_of: usize,
//trait對(duì)象類型的字節(jié)對(duì)齊大小
align_of: usize,
//后續(xù)是trait對(duì)象的所有方法實(shí)現(xiàn)的指針數(shù)組
}
元數(shù)據(jù)類型相同的裸指針可以任意的轉(zhuǎn)換桂对,例如:可以有 * const [usize; 3] as * const[usize; 5] ;
元數(shù)據(jù)類型不同的裸指針之間不能轉(zhuǎn)換甩卓,例如;* const [usize;3] as *const[usize] 而這種語句無法通過編譯器
裸指針的操作函數(shù)——intrinsic模塊內(nèi)存相關(guān)固有函數(shù)
intrinsics模塊中的函數(shù)由編譯器內(nèi)置實(shí)現(xiàn)蕉斜,并提供給其他模塊使用逾柿。intrinsics模塊的內(nèi)存函數(shù)一般不被庫以外的代碼直接調(diào)用,而是由mem模塊和ptr模塊封裝后再提供給其他模塊宅此。
相關(guān)內(nèi)存申請(qǐng)及釋放函數(shù):
-
intrinsics::drop_in_place<T: ?Sized>(to_drop: * mut T)
在某些情況下机错,可能會(huì)將變量設(shè)置成不允許編譯器自動(dòng)調(diào)用變量的drop函數(shù)父腕, 此時(shí)若是仍需要對(duì)變量調(diào)用drop弱匪,則在代碼中顯式調(diào)用此函數(shù)以出發(fā)對(duì)T類型的drop調(diào)用。 -
intrinsics::forget<T: ?Sized> (_:T)
代碼中調(diào)用這個(gè)函數(shù)后璧亮,編譯器不對(duì)forget的變量自動(dòng)調(diào)用變量的drop函數(shù)萧诫。 -
intrinsics::needs_drop<T>()->bool
判斷T類型是否需要做drop操作斥难,如若實(shí)現(xiàn)了Copy trait的類型會(huì)返回false
類型轉(zhuǎn)換:
-
intrinsics::transmute<T,U>(e:T)->U
對(duì)于內(nèi)存布局相同的類型 T和U, 完成將類型T變量轉(zhuǎn)換為類型U變量,此時(shí)T的所有權(quán)將轉(zhuǎn)換為U的所有權(quán)
指針偏移函數(shù):
-
intrinsics::offset<T>(dst: *const T, offset: usize)->* const T
類似C的類型指針加計(jì)算 -
intrinsics::ptr_offset_from<T>(ptr: *const T, base: *const T) -> isize
基于類型T內(nèi)存布局的兩個(gè)裸指針之間的偏移量
內(nèi)存塊內(nèi)容修改函數(shù):
-
intrinsics::copy<T>(src:*const T, dst: *mut T, count:usize)
內(nèi)存拷貝财搁, src和dst內(nèi)存可重疊蘸炸, 類似c語言中的memmove, 此時(shí)dst原有內(nèi)存如果已經(jīng)初始化,則會(huì)出現(xiàn)內(nèi)存泄漏尖奔。src的所有權(quán)實(shí)際會(huì)被復(fù)制搭儒,從而也造成重復(fù)drop問題。 -
intrinsics::copy_no_overlapping<T>(src:*const T, dst: * mut T, count:usize)
內(nèi)存拷貝提茁, src和dst內(nèi)存不重疊 -
intrinsics::write_bytes(dst: *mut T, val:u8, count:usize)
C語言的memset的rust實(shí)現(xiàn), 此時(shí)淹禾,原內(nèi)存如果已經(jīng)初始化,則原內(nèi)存的變量可能造成內(nèi)存泄漏茴扁,且因?yàn)榫幾g器會(huì)繼續(xù)對(duì)dst的內(nèi)存塊做drop調(diào)用铃岔,有可能會(huì)UB。
類型內(nèi)存參數(shù)函數(shù):
-
intrinsics::size_of<T>()->usize
類型內(nèi)存空間字節(jié)大小 -
intrinsics::min_align_of<T>()->usize
返回類型對(duì)齊字節(jié)大小 -
intrinsics::size_of_val<T>(_:*const T)->usize
返回指針指向的變量?jī)?nèi)存空間字節(jié)大小 -
intrinsics::min_align_of_val<T>(_: * const T)->usize
返回指針指向的變量對(duì)齊字節(jié)大小
禁止優(yōu)化的內(nèi)存函數(shù):
類似
volatile_xxxx
的函數(shù)是通知編譯器不做內(nèi)存優(yōu)化的操作函數(shù),一般硬件相關(guān)操作需要禁止優(yōu)化峭火。
-
intrinsics::volatile_copy_nonoverlapping_memory<T>(dst: *mut T, src: *const T, count: usize)
內(nèi)存拷貝 -
intrinsics::volatile_copy_memory<T>(dst: *mut T, src: *const T, count: usize)
功能類似C語言memmove -
intrinsics::volatile_set_memory<T>(dst: *mut T, val: u8, count: usize)
功能類似C語言memset -
intrinsics::volatile_load<T>(src: *const T) -> T
讀取內(nèi)存或寄存器毁习,T類型字節(jié)對(duì)齊到2的冪次 -
intrinsics::volatile_store<T>(dst: *mut T, val: T)
內(nèi)存或寄存器寫入,字節(jié)對(duì)齊 -
intrinsics::unaligned_volatile_load<T>(src: *const T) -> T
字節(jié)非對(duì)齊 -
intrinsics::unaligned_volatile_store<T>(dst: *mut T, val: T)
字節(jié)非對(duì)齊
內(nèi)存比較函數(shù):
-
intrinsics::raw_eq<T>(a: &T, b: &T) -> bool
內(nèi)存比較卖丸,類似C語言memcmp -
pub fn ptr_guaranteed_eq<T>(ptr: *const T, other: *const T) -> bool
判斷兩個(gè)指針是否判斷, 相等返回ture, 不等返回false -
pub fn ptr_guaranteed_ne<T>(ptr: *const T, other: *const T) -> bool
判斷兩個(gè)指針是否不等纺且,不等返回true
裸指針方法
在rust中針對(duì)*const T/*mut T
的類型實(shí)現(xiàn)了若干方法,是對(duì)語言的原生類型實(shí)現(xiàn)方法稍浆,并擴(kuò)展的實(shí)例:
impl <T:?Sized> * const T { // 只讀指針
//省略部分代碼
}
impl <T:?Sized> *mut T{ // 可變指針
// 省略部分代碼
}
impl <T> *const [T] { // 只讀[T]指針
// 省略部分代碼
}
impl <T> *mut [T] { // 可變[T]指針
// 省略部分代碼
}
對(duì)于裸指針载碌,rust標(biāo)準(zhǔn)庫包含了最基礎(chǔ)的 * const T/* mut T
, 以及在* const T/*mut T
基礎(chǔ)上特化的切片類型[T]的裸指針* const [T]/*mut [T]
衅枫。
在標(biāo)準(zhǔn)庫針對(duì)兩種基礎(chǔ)類型指針實(shí)現(xiàn)了一些關(guān)聯(lián)函數(shù)及方法嫁艇。這里一定注意,所有針對(duì) * const T
的方法在* const [T]
上都是適用的弦撩。
以上有幾點(diǎn)值得注意:
- 可以針對(duì)原生類型實(shí)現(xiàn)方法(實(shí)現(xiàn)trait)步咪,這也是rust類型系統(tǒng)的強(qiáng)大擴(kuò)展性,也是對(duì)函數(shù)式編程的強(qiáng)大支持益楼;
- 針對(duì)泛型約束實(shí)現(xiàn)方法歧斟,我們可以大致認(rèn)為
*const T/* mut T
實(shí)質(zhì)是一種泛型約束,*const [T]/*mut [T]
是更進(jìn)一步的約束偏形,這使得rust可以具備更好的數(shù)據(jù)抽象能力静袖,簡(jiǎn)化代碼,復(fù)用模塊俊扭。
裸指針的創(chuàng)建
1队橙、從已經(jīng)初始化的變量創(chuàng)建裸指針:
&T as *const T;
&mut T as * mut T;
2、用usize的數(shù)值創(chuàng)建裸指針:并使用了unsafe
{
let a: usize = 0xf000000000000000;
unsafe {a as * const i32};
}
在操作系統(tǒng)內(nèi)核時(shí)需要直接將一個(gè)地址數(shù)值轉(zhuǎn)換為某一類型的裸指針, 故而rust也提供了一些其他的裸指針創(chuàng)建關(guān)聯(lián)函數(shù):
-
ptr::null<T>() -> *const T
創(chuàng)建一個(gè)0值的*const T
捐康,等同于是0 as *const T
仇矾,用null()函數(shù)明顯更符合程序員的習(xí)慣 ; -
ptr::null_mut<T>()->*mut T
功能同上解总,創(chuàng)建可變裸指針贮匕; -
ptr::from_raw_parts<T: ?Sized>(data_address: *const (), metadata: <T as Pointee>::Metadata) -> *const T
從內(nèi)存地址和元數(shù)據(jù)創(chuàng)建裸指針; -
ptr::from_raw_parts_mut<T: ?Sized>(data_address: *mut (), metadata: <T as Pointee>::Metadata) -> *mut T
功能同上花枫,創(chuàng)建可變裸指針刻盐;
在進(jìn)行rust裸指針類型轉(zhuǎn)換時(shí),經(jīng)常使用以上兩個(gè)函數(shù)獲得需要的指針類型劳翰。
切片類型的裸指針創(chuàng)建函數(shù)如下:
-
ptr::slice_from_raw_parts<T>(data: *const T, len: usize) -> *const [T]
ptr::slice_from_raw_parts_mut<T>(data: *mut T, len: usize) -> *mut [T]
由裸指針類型及切片長(zhǎng)度獲得切片類型裸指針敦锌,調(diào)用代碼應(yīng)保證data是切片的裸指針地址
。
由類型裸指針轉(zhuǎn)換為切片類型裸指針最突出的應(yīng)用之一是內(nèi)存申請(qǐng)
佳簸,申請(qǐng)的內(nèi)存返回 * const u8的指針乙墙,這個(gè)裸指針是沒有包含內(nèi)存大小的,只有頭地址生均,因此需要將這個(gè)指針轉(zhuǎn)換為 * const [u8]听想,將申請(qǐng)的內(nèi)存大小包含入裸指針結(jié)構(gòu)體中。
slice_from_raw_parts代碼如下:
pub const fn slice_from_raw_parts<T>(data: *const T, len: usize) -> *const [T] {
// data.cast()將*const T轉(zhuǎn)換為 *const()
from_raw_parts(data.cast(), len)
}
pub const fn from_raw_parts<T: ?Sized>(
data_address: *const (),
metadata: <T as Pointee>::Metadata,
) -> *const T {
//由以下代碼可以確認(rèn) * const T實(shí)質(zhì)就是PtrRepr類型結(jié)構(gòu)體马胧。
unsafe {
PtrRepr {
components: PtrComponents { data_address, metadata }
}.const_ptr
}
}
裸指針函數(shù)(不屬于方法)
-
ptr::drop_in_place<T: ?Sized>(to_drop: *mut T)
此函數(shù)是編譯器實(shí)現(xiàn)的汉买,用于由程序代碼人工釋放所有權(quán),而不是交由rust編譯器處理漓雅。此函數(shù)會(huì)引發(fā)T內(nèi)部成員的系列drop調(diào)用录别。 -
ptr::metadata<T: ?Sized>(ptr: *const T) -> <T as Pointee>::Metadata
用來返回裸指針的元數(shù)據(jù) -
ptr::eq<T>(a: *const T, b: *const T)->bool
比較指針朽色,此處需要注意邻吞,地址比較不但是地址,也比較元數(shù)據(jù)
ptr模塊的函數(shù)大部分邏輯都比較簡(jiǎn)單葫男。很多就是對(duì)intrinsic 函數(shù)做調(diào)用抱冷。
裸指針類型轉(zhuǎn)換方法
裸指針類型之間的轉(zhuǎn)換:
-
*const T::cast<U>(self) -> *const U
本質(zhì)上就是一個(gè)*const T as *const U
。利用rust的類型推斷梢褐,此函數(shù)可以簡(jiǎn)化代碼并支持鏈?zhǔn)秸{(diào)用旺遮。 -
*mut T::cast<U>(self)->*mut U
功能同上
調(diào)用以上的函數(shù)要注意,若是后續(xù)要把返回的指針轉(zhuǎn)換成引用盈咳,
必須保證T類型與U類型內(nèi)存布局完全一致
耿眉。
如果僅僅是將返回值做數(shù)值應(yīng)用,則此約束可以不遵守鱼响,cast函數(shù)轉(zhuǎn)換后的類型通常由編譯器自行推斷鸣剪,有時(shí)需要仔細(xì)分析。
裸指針與引用之間的類型轉(zhuǎn)換:
-
*const T::as_ref<`a>(self) -> Option<&`a T>
將裸指針轉(zhuǎn)換為引用,由于*const T可能為零筐骇,所有需要轉(zhuǎn)換為Option<& `a T>
類型债鸡,轉(zhuǎn)換的安全性由程序員保證
,尤其注意滿足rust對(duì)引用的安全要求铛纬。這里要注意厌均,轉(zhuǎn)換后的生命周期實(shí)際上與原變量的生命周期相獨(dú)立。因此告唆,生命周期的正確性將由調(diào)用代碼保證棺弊。 -
*mut T::as_ref<`a>(self)->Option<&`a T>
同上 -
*mut T::as_mut<`a>(self)->Option<&`a mut T>
同上,但轉(zhuǎn)化類型為 &mut T悔详。
切片類型裸指針類型轉(zhuǎn)換:
-
ptr::*const [T]::as_ptr(self) -> *const T
將切片類型的裸指針轉(zhuǎn)換為切片成員類型的裸指針镊屎, 這個(gè)轉(zhuǎn)換會(huì)導(dǎo)致指針的元數(shù)據(jù)丟失 -
ptr::*mut [T]::as_mut_ptr(self) -> *mut T
同上
裸指針結(jié)構(gòu)體屬性相關(guān)方法:
-
ptr::*const T::to_raw_parts(self) -> (*const (), <T as super::Pointee>::Metadata)
ptr::*mut T::to_raw_parts(self)->(* const (), <T as super::Pointee>::Metadata)
由裸指針獲得地址及元數(shù)據(jù) -
ptr::*const T::is_null(self)->bool
ptr::*mut T::is_null(self)->bool此
函數(shù)判斷裸指針的地址值是否為0
切片類型裸指針:
-
ptr::*const [T]:: len(self) -> usize
獲取切片長(zhǎng)度,直接從裸指針的元數(shù)據(jù)獲取長(zhǎng)度 -
ptr:: *mut [T]:: len(self) -> usize
同上
裸指針偏移計(jì)算相關(guān)方法
-
ptr::*const T::offset(self, count:isize)->* const T
得到偏移后的裸指針 -
ptr::*const T::wrapping_offset(self, count: isize) -> *const T
考慮溢出繞回的offset -
ptr::*const T::offset_from(self, origin: *const T) -> isize
計(jì)算兩個(gè)裸指針的offset值 -
ptr::*mut T::offset(self, count:isize)->* mut T
偏移后的裸指針 -
ptr::*const T::wrapping_offset(self, count: isize) -> *const T
考慮溢出繞回的offset -
ptr::*const T::offset_from(self, origin: *const T) -> isize
計(jì)算兩個(gè)裸指針的offset值
以上兩個(gè)方法基本上通過intrinsic的函數(shù)實(shí)現(xiàn)
ptr::*const T::add(self, count: usize) -> Self
ptr::*const T::wraping_add(self, count: usize)->Self
ptr::*const T::sub(self, count:usize) -> Self
ptr::*const T::wrapping_sub(self, count:usize) -> Self
ptr::*mut T::add(self, count: usize) -> Self
ptr::*mut T::wraping_add(self, count: usize)->Self
ptr::*mut T::sub(self, count:usize) -> Self
ptr::*mut T::wrapping_sub(self, count:usize) -> Self
以上是對(duì)offset函數(shù)的包裝茄螃,使之更符合語義習(xí)慣缝驳,并便于理解
裸指針直接賦值方法
//該方法用于僅給指針結(jié)構(gòu)體的 address部分賦值
pub fn set_ptr_value(mut self, val: *const u8) -> Self {
// 以下代碼因?yàn)橹恍薷腜trComponent.address,所以不能直接用相等
// 代碼采取的方案是取self的可變引用归苍,將此引用轉(zhuǎn)換為裸指針的裸指針用狱,
let thin = &mut self as *mut *const T as *mut *const u8;
// 這個(gè)賦值僅僅做了address的賦值,對(duì)于瘦指針拼弃,這個(gè)相當(dāng)于賦值操作夏伊,
// 對(duì)于胖指針,則沒有改變胖指針的元數(shù)據(jù)吻氧。這種操作方式僅僅在極少數(shù)的情況下
// 可以使用溺忧,極度危險(xiǎn)。
unsafe { *thin = val };
self
}
rust引用&T
的安全要求
- 引用的內(nèi)存地址必須滿足類型T的內(nèi)存對(duì)齊要求
- 引用的內(nèi)存內(nèi)容必須是初始化過的
舉例:
#[repr(packed)]
struct RefTest {a:u8, b:u16, c:u32}
fn main() {
let test = RefTest{a:1, b:2, c:3};
//下面代碼編譯會(huì)有告警盯孙,因?yàn)閠est.b 內(nèi)存字節(jié)位于奇數(shù)鲁森,無法用于借用
let ref1 = &test.b
}
編譯器出現(xiàn)如下警告
|
9 | let ref1 = &test.b;
| ^^^^^^^
|
= note: `#[warn(unaligned_references)]` on by default
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #82523 <https://github.com/rust-lang/rust/issues/82523>
= note: fields of packed structs are not properly aligned, and creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
= help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers)
針對(duì)未初始化變量相關(guān)的指針操作
MaybeUninit<T>
標(biāo)準(zhǔn)庫代碼分析
通常rust中對(duì)于變量的要求是必須初始化后才能使用,否則就會(huì)編譯告警振惰。但在程序中歌溉,總有內(nèi)存還未初始化,卻需要使用的情況:
- 從堆申請(qǐng)的內(nèi)存塊骑晶,這些內(nèi)存塊都是沒有初始化的
- 需要定義一個(gè)新的泛型變量時(shí)痛垛,并且不合適用轉(zhuǎn)移所有權(quán)進(jìn)行賦值時(shí)
- 需要定義一個(gè)新的變量铸抑,但希望不初始化便能使用其引用時(shí)
- 定義一個(gè)數(shù)組蹬碧,但必須在后繼代碼對(duì)數(shù)組成員初始化時(shí)
......
為了處理這種需要在代碼中使用未初始化內(nèi)存的情況,rust標(biāo)準(zhǔn)庫定義了MaybeUninit<T>
MaybeUninit<T>
結(jié)構(gòu)定義
#[repr(transparent)]
pub union MaybeUninit<T> {
uninit: (),
value: ManuallyDrop<T>,
}
說明:
屬性repr(transparent)
實(shí)際上表示外部的封裝結(jié)構(gòu)在內(nèi)存中等價(jià)于內(nèi)部的變量,
MaybeUninit<T>
的內(nèi)存布局就是ManuallyDrop<T>
的內(nèi)存布局.
在后面的內(nèi)容中可以看到
ManuallyDrop<T>
實(shí)際就是T的內(nèi)存布局桃移。故而MaybeUninit<T>
在內(nèi)存中實(shí)質(zhì)也就是T類型仔雷。
MaybeUninit<T>
容器來實(shí)現(xiàn)對(duì)未初始化變量的封裝蹂析,以便在不引發(fā)編譯錯(cuò)誤完成對(duì)T類型未初始化變量的相關(guān)操作.
如果T類型的變量未初始化抖剿,那需要顯式的提醒編譯器不做T類型的drop操作,因?yàn)閐rop操作可能會(huì)對(duì)T類型內(nèi)部的變量做連鎖drop處理识窿,從而引用未初始化的內(nèi)容斩郎,造成未定義行為(undefined behavior)
。
而rust是用ManuallyDrop<T>
封裝結(jié)構(gòu)完成了對(duì)編譯器的顯式提示:對(duì)于用ManuallyDrop<T>
封裝的變量喻频,生命周期終止的時(shí)候編譯器不會(huì)調(diào)用drop操作缩宜。
ManuallyDrop<T>
結(jié)構(gòu)及方法
源代碼如下:
#[repr(transparent)]
pub struct ManuallyDrop<T: ?Sized> {
value: T,
}
重點(diǎn)關(guān)注的一些方法:
-
ManuallyDrop<T>::new(val:T) -> ManuallyDrop<T>
此函數(shù)返回ManuallyDrop變量擁有傳入的T類型變量所有權(quán),并將此塊內(nèi)存直接用ManuallyDrop封裝, 對(duì)于val甥温,編譯器不再主動(dòng)做drop操作锻煌。
pub const fn new(value: T) -> ManuallyDrop<T> {
//所有權(quán)轉(zhuǎn)移到結(jié)構(gòu)體內(nèi)部,value生命周期結(jié)束時(shí)不會(huì)引發(fā)drop
ManuallyDrop { value }
}
-
ManuallyDrop<T>::into_inner(slot: ManuallyDrop<T>)->T
將封裝的T類型變量所有權(quán)轉(zhuǎn)移出來姻蚓,轉(zhuǎn)移出來的變量生命周期終止時(shí)宋梧,編譯器會(huì)自動(dòng)調(diào)用類型的drop。
pub const fn into_inner(slot: ManuallyDrop<T>) -> T {
//將value解封裝狰挡,所有權(quán)轉(zhuǎn)移到返回值中捂龄,編譯器重新對(duì)所有權(quán)做處理
slot.value
}
-
ManuallyDrop<T>::drop(slot: &mut ManuallyDrop<T>)
drop掉內(nèi)部變量,封裝入ManuallyDrop<T>
的變量一定是在程序運(yùn)行的某一時(shí)期不需要編譯器drop加叁,所以調(diào)用這個(gè)函數(shù)的時(shí)候一定要注意正確性倦沧。 -
ManuallyDrop<T>::deref(&self)-> & T
返回內(nèi)部包裝的變量的引用
fn deref(&self) -> &T {
//返回后,代碼可以用&T對(duì)self.value做讀操作,但不改變drop的規(guī)則
&self.value
}
-
ManuallyDrop<T>::deref_mut(&mut self)-> & mut T
返回內(nèi)部包裝的變量的可變引用它匕,調(diào)用代碼可以利用可變引用對(duì)內(nèi)部變量賦值展融,但不改變drop機(jī)制
ManuallyDrop樣例:
use std::mem::ManuallyDrop;
let mut x = ManuallyDrop::new(String::from("Hello World!"));
x.truncate(5); // 此時(shí)會(huì)調(diào)用deref
assert_eq!(*x, "Hello");
// 但對(duì)x的drop不會(huì)再發(fā)生
MaybeUninit<T>
創(chuàng)建方法
-
MaybeUninit<T>::uninit()->MaybeUninit<T>
可視為在棧空間上申請(qǐng)內(nèi)存的方法豫柬,申請(qǐng)的內(nèi)存大小是T類型的內(nèi)存大小告希,該內(nèi)存沒有初始化。利用泛型和Union內(nèi)存布局烧给,rust巧妙的利用此函數(shù)在棧上申請(qǐng)一塊未初始化內(nèi)存燕偶。此函數(shù)非常非常非常值得關(guān)注
,在需要在棿匆梗空間定義一個(gè)未初始化泛型時(shí)杭跪,應(yīng)第一時(shí)間想到MaybeUninit::<T>::uninit()
仙逻。
pub const fn uninit() -> MaybeUninit<T> {
//變量?jī)?nèi)存布局與T類型完全一致
MaybeUninit { uninit: () }
}
-
MaybeUninit<T>::new(val:T)->MaybeUninit<T>
內(nèi)部用ManuallyDrop封裝了val, 然后用MaybeUninit封裝ManuallyDrop驰吓。如果T沒有初始化過,調(diào)用這個(gè)函數(shù)會(huì)編譯失敗系奉,此時(shí)內(nèi)存實(shí)際上已經(jīng)初始化過了檬贰。調(diào)用此函數(shù)要額外注意val的drop必須在后續(xù)有交代。
pub const fn new(val: T) -> MaybeUninit<T> {
//val這個(gè)時(shí)候是初始化過的缺亮。
MaybeUninit { value: ManuallyDrop::new(val) }
}
-
MaybeUninit<T>::zeroed()->MaybeUninit<T>
申請(qǐng)了T類型內(nèi)存并清零翁涤。
pub fn zeroed() -> MaybeUninit<T> {
let mut u = MaybeUninit::<T>::uninit();
unsafe {
//因?yàn)闆]有初始化,所以不存在所有權(quán)問題,
//必須使用ptr::write_bytes葵礼,否則無法給內(nèi)存清0
//ptr::write_bytes直接調(diào)用了intrinsics::write_bytes
u.as_mut_ptr().write_bytes(0u8, 1);
}
u
}
對(duì)未初始化的變量賦值的方法
- 將值寫入
MaybeUninit<T>
:MaybeUninit<T>::write(val)->&mut T
這個(gè)函數(shù)是在未初始化時(shí)使用号阿,如果已經(jīng)調(diào)用過write,且不希望解封裝鸳粉,那后續(xù)的賦值使用返回的&mut T扔涧。代碼如下:
pub const fn write(&mut self, val: T) -> &mut T {
//下面這個(gè)賦值,會(huì)導(dǎo)致原*self的MaybeUninit<T>的變量生命周期截止届谈,會(huì)調(diào)用drop枯夜。但不會(huì)對(duì)內(nèi)部的T類型變量做drop調(diào)用。所以如果*self內(nèi)部的T類型變量已經(jīng)被初始化且需要做drop艰山,那會(huì)造成內(nèi)存泄漏湖雹。所以下面這個(gè)等式實(shí)際上隱含了self內(nèi)部的T類型變量必須是未初始化的或者T類型變量不需要drop。
*self = MaybeUninit::new(val);
// 函數(shù)調(diào)用后的賦值用返回的&mut T來做曙搬。
unsafe { self.assume_init_mut() }
}
初始化后解封裝的方法
用assume_init返回初始化后的變量并消費(fèi)掉MaybeUninit<T>
變量摔吏,這是最標(biāo)準(zhǔn)的做法:
MaybeUninit<T>::assume_init()->T
,代碼如下:
pub const unsafe fn assume_init(self) -> T {
// 調(diào)用者必須保證self已經(jīng)初始化了
unsafe {
intrinsics::assert_inhabited::<T>();
//把T的所有權(quán)返回,編譯器會(huì)主動(dòng)對(duì)T調(diào)用drop
ManuallyDrop::into_inner(self.value)
}
}
assume_init_read是不消費(fèi)self的情況下獲得內(nèi)部T變量纵装,內(nèi)部T變量的所有權(quán)已經(jīng)轉(zhuǎn)移到返回變量舔腾,后繼要注意不能再次調(diào)用其他解封裝函數(shù)。否則解封裝后搂擦,會(huì)出現(xiàn)雙份所有權(quán)稳诚,引發(fā)兩次對(duì)同一變量的drop,導(dǎo)致UB瀑踢。
pub const unsafe fn assume_init_read(&self) -> T {
unsafe {
intrinsics::assert_inhabited::<T>();
//會(huì)調(diào)用ptr::read
self.as_ptr().read()
}
}
//此函即ptr::read, 會(huì)復(fù)制一個(gè)變量扳还,此時(shí)注意,實(shí)際上src指向的變量的所有權(quán)已經(jīng)轉(zhuǎn)移給了返回變量橱夭,
//所以調(diào)用此函數(shù)的前提是src后繼一定不能調(diào)用T類型的drop函數(shù)氨距,例如src本身處于ManallyDrop,或后繼對(duì)src調(diào)用forget棘劣,或給src綁定新變量俏让。
//在rust中,不支持 let xxx = *(&T) 這種轉(zhuǎn)移所有權(quán)的方式茬暇,因此對(duì)于只有指針輸入首昔,又要轉(zhuǎn)移所有權(quán)的,智能利用淺拷貝進(jìn)行粗暴轉(zhuǎn)移糙俗。
pub const unsafe fn read<T>(src: *const T) -> T {`
//利用MaybeUninit::uninit申請(qǐng)未初始化的T類型內(nèi)存
let mut tmp = MaybeUninit::<T>::uninit();
unsafe {
//完成內(nèi)存拷貝
copy_nonoverlapping(src, tmp.as_mut_ptr(), 1);
//初始化后的內(nèi)存解封裝并返回
tmp.assume_init()
}
}
與上個(gè)函數(shù)比較類似的ManuallyDrop<T>::take
方法勒奇,用take函數(shù)將變量復(fù)制并獲得變量的所有權(quán)。此時(shí)原變量仍然保留在ManuallyDrop中巧骚,后繼不能再調(diào)用其他解封裝函數(shù)赊颠,否則可能會(huì)出現(xiàn)UB格二。這里要特別注意理解take已經(jīng)把變量的所有權(quán)轉(zhuǎn)移到返回變量中。
pub unsafe fn take(slot: &mut ManuallyDrop<T>) -> T {
// 拷貝內(nèi)部變量竣蹦,并返回內(nèi)部變量的所有權(quán)
// 返回后顶猜,原有的變量所有權(quán)已經(jīng)消失,不能再用into_inner來返回
// 否則會(huì)UB
unsafe { ptr::read(&slot.value) }
}
-
MaybeUninit<T>::assume_init_drop(&self)
對(duì)于已經(jīng)初始化過的MaybeUninit<T>痘括, 如果所有權(quán)一直沒有轉(zhuǎn)移驶兜,則必須調(diào)用此函數(shù)以觸發(fā)T類型的drop函數(shù)完成所有權(quán)的釋放。 -
MaybeUninit<T>::assume_init_ref(&self)->&T
返回內(nèi)部T類型變量的借用远寸,調(diào)用者應(yīng)保證內(nèi)部T類型變量已經(jīng)初始化抄淑,返回值按照一個(gè)普通的引用使用。應(yīng)注意返回值的生命周期應(yīng)該小于self的生命周期 -
MaybeUninit<T>::assume_init_mut(&mut self)->&mut T
返回內(nèi)部T類型變量的可變借用驰后,調(diào)用者應(yīng)保證內(nèi)部T類型變量已經(jīng)初始化肆资,返回值按照一個(gè)普通的可變引用使用。應(yīng)注意返回值的生命周期應(yīng)該小于self的生命周期
MaybeUninit<[T]>
的方法
創(chuàng)建一個(gè)MaybeUninit的未初始化數(shù)組:
-
MaybeUninit<T>::uninit_array<const LEN:usize>()->[Self; LEN]
此處對(duì)LEN的使用方式需要注意灶芝,這是不常見的一個(gè)泛型寫法,這個(gè)函數(shù)同樣的申請(qǐng)了一塊內(nèi)存郑原。代碼:
pub const fn uninit_array<const LEN: usize>() -> [Self; LEN] {
unsafe { MaybeUninit::<[MaybeUninit<T>; LEN]>::uninit().assume_init() }
}
這里要注意區(qū)別數(shù)組類型和數(shù)組元素的初始化。對(duì)于數(shù)組[MaybeUninit<T>;LEN]
這一類型本身來說夜涕,初始化就是確定整體的內(nèi)存大小犯犁,所以數(shù)組類型的初始化在聲明后就已經(jīng)完成了。這時(shí)assume_init()是正確的女器。這是一個(gè)理解上的盲點(diǎn)酸役。
-
MaybeUninit<T>::array_assume_init<const N:usize>(array: [Self; N]) -> [T; N]
這個(gè)函數(shù)沒有把所有權(quán)轉(zhuǎn)移出來,代碼分析如下:
pub unsafe fn array_assume_init<const N: usize>(array: [Self; N]) -> [T; N] {
unsafe {
//最后調(diào)用是*const T::read()驾胆,此處 as *const _的寫法可以簡(jiǎn)化代碼,read后涣澡,所有權(quán)已經(jīng)轉(zhuǎn)移到返回值
//返回后,此數(shù)組內(nèi)所有的MaybeUninit變量成員不能再解封裝
(&array as *const _ as *const [T; N]).read()
}
}
MaybeUnint<T>典型案列
對(duì)T類型變量申請(qǐng)內(nèi)存及賦值:
use std::mem::MaybeUninit;
// 獲得一個(gè)未初始化的i32引用類型內(nèi)存
let mut x = MaybeUninit::<&i32>::uninit();
// 將&0寫入變量丧诺,完成初始化
x.write(&0);
// 將初始化后的變量解封裝供后繼的代碼使用入桂。
let x = unsafe { x.assume_init() };
以上代碼,編譯器不會(huì)對(duì)x.write進(jìn)行報(bào)警驳阎,這是MaybeUninit<T>
的最重要的應(yīng)用抗愁,這個(gè)例子展示了rust如何給未初始化內(nèi)存賦值的處理方式。調(diào)用assume_init前呵晚,必須保證變量已經(jīng)被正確初始化蜘腌。
更復(fù)雜的初始化例子:
use std::mem::{self, MaybeUninit};
let data = {
// data在聲明后實(shí)際上就已經(jīng)初始化完畢。
let mut data: [MaybeUninit<Vec<u32>>; 1000] = unsafe {
//這里注意實(shí)際調(diào)用是MaybeUninit::<[MaybeUninit<Vec<u32>>;1000]>::uninit(), rust的類型推斷機(jī)制完成了泛型實(shí)例化
MaybeUninit::uninit().assume_init()
};
for elem in &mut data[..] {
elem.write(vec![42]);
}
// 直接用transmute完成整個(gè)數(shù)組類型的轉(zhuǎn)換
// 仔細(xì)思考一下劣纲,這里除了用transmute逢捺,似乎沒有其他辦法了谁鳍,
unsafe { mem::transmute::<_, [Vec<u32>; 1000]>(data) }
};
assert_eq!(&data[0], &[42]);
下面例子說明一塊內(nèi)存被 MaybeUnint<T>
封裝后癞季,編譯器將不再對(duì)其做釋放劫瞳,必須在代碼中顯式釋放:
use std::mem::MaybeUninit;
use std::ptr;
let mut data: [MaybeUninit<String>; 1000] = unsafe { MaybeUninit::uninit().assume_init() };
// 初始化了500個(gè)String變量
let mut data_len: usize = 0;
for elem in &mut data[0..500] {
//write沒有將所有權(quán)轉(zhuǎn)移出ManuallyDrop
elem.write(String::from("hello"));
data_len += 1;
}
//編譯器無法自動(dòng)調(diào)用drop釋放String變量, 必須顯式用drop_in_place釋放
for elem in &mut data[0..data_len] {
//實(shí)際上也可以調(diào)用assume_init_drop來完成此工作
unsafe { ptr::drop_in_place(elem.as_mut_ptr()); }
}
上例中,在沒有assume_init()調(diào)用的情況下绷柒,必須手工調(diào)用drop_in_place釋放內(nèi)存志于。
MaybeUninit<T>
是一個(gè)非常重要的類型結(jié)構(gòu),未初始化內(nèi)存是編程中不可避免要遇到的情況废睦,MaybeUninit<T>
也就是rust編程中必須熟練使用的一個(gè)類型伺绽。