ch19-01-unsafe-rust.md
commit 1524fa89fbaa4d52c4a2095141f6eaa6c22f8bd0
目前為止討論過(guò)的代碼都有 Rust 在編譯時(shí)會(huì)強(qiáng)制執(zhí)行的內(nèi)存安全保證。然而,Rust 還隱藏有第二種語(yǔ)言,它不會(huì)強(qiáng)制執(zhí)行這類(lèi)內(nèi)存安全保證:這被稱(chēng)為 不安全 Rust(unsafe Rust)。它與常規(guī) Rust 代碼無(wú)異,但是會(huì)提供額外的超能力。
盡管代碼可能沒(méi)問(wèn)題,但如果 Rust 編譯器沒(méi)有足夠的信息可以確定,它將拒絕代碼。
不安全 Rust 之所以存在,是因?yàn)殪o態(tài)分析本質(zhì)上是保守的。當(dāng)編譯器嘗試確定一段代碼是否支持某個(gè)保證時(shí),拒絕一些合法的程序比接受錯(cuò)誤的程序要好一些。這必然意味著有時(shí)代碼 可能 是合法的,但如果 Rust 編譯器沒(méi)有足夠的信息來(lái)確定,它將拒絕該代碼。在這種情況下,可以使用不安全代碼告訴編譯器,“相信我,我知道我在干什么?!边@么做的缺點(diǎn)就是你只能靠自己了:如果不安全代碼出錯(cuò)了,比如解引用空指針,可能會(huì)導(dǎo)致不安全的內(nèi)存使用。
另一個(gè) Rust 存在不安全一面的原因是:底層計(jì)算機(jī)硬件固有的不安全性。如果 Rust 不允許進(jìn)行不安全操作,那么有些任務(wù)則根本完成不了。Rust 需要能夠進(jìn)行像直接與操作系統(tǒng)交互,甚至于編寫(xiě)你自己的操作系統(tǒng)這樣的底層系統(tǒng)編程!這也是 Rust 語(yǔ)言的目標(biāo)之一。讓我們看看不安全 Rust 能做什么,和怎么做。
可以通過(guò) unsafe
關(guān)鍵字來(lái)切換到不安全 Rust,接著可以開(kāi)啟一個(gè)新的存放不安全代碼的塊。這里有五類(lèi)可以在不安全 Rust 中進(jìn)行而不能用于安全 Rust 的操作,它們稱(chēng)之為 “不安全的超能力?!?這些超能力是:
union
? 的字段有一點(diǎn)很重要,unsafe
并不會(huì)關(guān)閉借用檢查器或禁用任何其他 Rust 安全檢查:如果在不安全代碼中使用引用,它仍會(huì)被檢查。unsafe
關(guān)鍵字只是提供了那五個(gè)不會(huì)被編譯器檢查內(nèi)存安全的功能。你仍然能在不安全塊中獲得某種程度的安全。
再者,unsafe
不意味著塊中的代碼就一定是危險(xiǎn)的或者必然導(dǎo)致內(nèi)存安全問(wèn)題:其意圖在于作為程序員你將會(huì)確保 unsafe
塊中的代碼以有效的方式訪(fǎng)問(wèn)內(nèi)存。
人是會(huì)犯錯(cuò)誤的,錯(cuò)誤總會(huì)發(fā)生,不過(guò)通過(guò)要求這五類(lèi)操作必須位于標(biāo)記為 unsafe
的塊中,就能夠知道任何與內(nèi)存安全相關(guān)的錯(cuò)誤必定位于 unsafe
塊內(nèi)。保持 unsafe
塊盡可能小,如此當(dāng)之后調(diào)查內(nèi)存 bug 時(shí)就會(huì)感謝你自己了。
為了盡可能隔離不安全代碼,將不安全代碼封裝進(jìn)一個(gè)安全的抽象并提供安全 API 是一個(gè)好主意,當(dāng)我們學(xué)習(xí)不安全函數(shù)和方法時(shí)會(huì)討論到。標(biāo)準(zhǔn)庫(kù)的一部分被實(shí)現(xiàn)為在被評(píng)審過(guò)的不安全代碼之上的安全抽象。這個(gè)技術(shù)防止了 unsafe
泄露到所有你或者用戶(hù)希望使用由 unsafe
代碼實(shí)現(xiàn)的功能的地方,因?yàn)槭褂闷浒踩橄笫前踩摹?
讓我們按順序依次介紹上述五個(gè)超能力,同時(shí)我們會(huì)看到一些提供不安全代碼的安全接口的抽象。
回到第四章的 “懸垂引用” 部分,那里提到了編譯器會(huì)確保引用總是有效的。不安全 Rust 有兩個(gè)被稱(chēng)為 裸指針(raw pointers)的類(lèi)似于引用的新類(lèi)型。和引用一樣,裸指針是不可變或可變的,分別寫(xiě)作 *const T
和 *mut T
。這里的星號(hào)不是解引用運(yùn)算符;它是類(lèi)型名稱(chēng)的一部分。在裸指針的上下文中,不可變 意味著指針解引用之后不能直接賦值。
裸指針與引用和智能指針的區(qū)別在于
通過(guò)去掉 Rust 強(qiáng)加的保證,你可以放棄安全保證以換取性能或使用另一個(gè)語(yǔ)言或硬件接口的能力,此時(shí) Rust 的保證并不適用。
示例 19-1 展示了如何從引用同時(shí)創(chuàng)建不可變和可變裸指針。
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
示例 19-1: 通過(guò)引用創(chuàng)建裸指針
注意這里沒(méi)有引入 unsafe
關(guān)鍵字。可以在安全代碼中 創(chuàng)建 裸指針,只是不能在不安全塊之外 解引用 裸指針,稍后便會(huì)看到。
這里使用 as
將不可變和可變引用強(qiáng)轉(zhuǎn)為對(duì)應(yīng)的裸指針類(lèi)型。因?yàn)橹苯訌谋WC安全的引用來(lái)創(chuàng)建他們,可以知道這些特定的裸指針是有效,但是不能對(duì)任何裸指針做出如此假設(shè)。
接下來(lái)會(huì)創(chuàng)建一個(gè)不能確定其有效性的裸指針,示例 19-2 展示了如何創(chuàng)建一個(gè)指向任意內(nèi)存地址的裸指針。嘗試使用任意內(nèi)存是未定義行為:此地址可能有數(shù)據(jù)也可能沒(méi)有,編譯器可能會(huì)優(yōu)化掉這個(gè)內(nèi)存訪(fǎng)問(wèn),或者程序可能會(huì)出現(xiàn)段錯(cuò)誤(segmentation fault)。通常沒(méi)有好的理由編寫(xiě)這樣的代碼,不過(guò)卻是可行的:
let address = 0x012345usize;
let r = address as *const i32;
示例 19-2: 創(chuàng)建指向任意內(nèi)存地址的裸指針
記得我們說(shuō)過(guò)可以在安全代碼中創(chuàng)建裸指針,不過(guò)不能 解引用 裸指針和讀取其指向的數(shù)據(jù)?,F(xiàn)在我們要做的就是對(duì)裸指針使用解引用運(yùn)算符 *
,這需要一個(gè) unsafe
塊,如示例 19-3 所示:
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
示例 19-3: 在 unsafe
塊中解引用裸指針
創(chuàng)建一個(gè)指針不會(huì)造成任何危險(xiǎn);只有當(dāng)訪(fǎng)問(wèn)其指向的值時(shí)才有可能遇到無(wú)效的值。
還需注意示例 19-1 和 19-3 中創(chuàng)建了同時(shí)指向相同內(nèi)存位置 num
的裸指針 *const i32
和 *mut i32
。相反如果嘗試同時(shí)創(chuàng)建 num
的不可變和可變引用,將無(wú)法通過(guò)編譯,因?yàn)?Rust 的所有權(quán)規(guī)則不允許在擁有任何不可變引用的同時(shí)再創(chuàng)建一個(gè)可變引用。通過(guò)裸指針,就能夠同時(shí)創(chuàng)建同一地址的可變指針和不可變指針,若通過(guò)可變指針修改數(shù)據(jù),則可能潛在造成數(shù)據(jù)競(jìng)爭(zhēng)。請(qǐng)多加小心!
既然存在這么多的危險(xiǎn),為何還要使用裸指針呢?一個(gè)主要的應(yīng)用場(chǎng)景便是調(diào)用 C 代碼接口,這在下一部分 “調(diào)用不安全函數(shù)或方法” 中會(huì)講到。另一個(gè)場(chǎng)景是構(gòu)建借用檢查器無(wú)法理解的安全抽象。讓我們先介紹不安全函數(shù),接著看一看使用不安全代碼的安全抽象的例子。
第二類(lèi)要求使用不安全塊的操作是調(diào)用不安全函數(shù)。不安全函數(shù)和方法與常規(guī)函數(shù)方法十分類(lèi)似,除了其開(kāi)頭有一個(gè)額外的 unsafe
。在此上下文中,關(guān)鍵字unsafe
表示該函數(shù)具有調(diào)用時(shí)需要滿(mǎn)足的要求,而 Rust 不會(huì)保證滿(mǎn)足這些要求。通過(guò)在 unsafe
塊中調(diào)用不安全函數(shù),表明我們已經(jīng)閱讀過(guò)此函數(shù)的文檔并對(duì)其是否滿(mǎn)足函數(shù)自身的契約負(fù)責(zé)。
如下是一個(gè)沒(méi)有做任何操作的不安全函數(shù) dangerous
的例子:
unsafe fn dangerous() {}
unsafe {
dangerous();
}
必須在一個(gè)單獨(dú)的 unsafe
塊中調(diào)用 dangerous
函數(shù)。如果嘗試不使用 unsafe
塊調(diào)用 dangerous
,則會(huì)得到一個(gè)錯(cuò)誤:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function is unsafe and requires unsafe function or block
--> src/main.rs:4:5
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` due to previous error
通過(guò)將 dangerous
調(diào)用插入 unsafe
塊中,我們就向 Rust 保證了我們已經(jīng)閱讀過(guò)函數(shù)的文檔,理解如何正確使用,并驗(yàn)證過(guò)其滿(mǎn)足函數(shù)的契約。
不安全函數(shù)體也是有效的 unsafe
塊,所以在不安全函數(shù)中進(jìn)行另一個(gè)不安全操作時(shí)無(wú)需新增額外的 unsafe
塊。
僅僅因?yàn)楹瘮?shù)包含不安全代碼并不意味著整個(gè)函數(shù)都需要標(biāo)記為不安全的。事實(shí)上,將不安全代碼封裝進(jìn)安全函數(shù)是一個(gè)常見(jiàn)的抽象。作為一個(gè)例子,標(biāo)準(zhǔn)庫(kù)中的函數(shù),split_at_mut
,它需要一些不安全代碼,讓我們探索如何可以實(shí)現(xiàn)它。這個(gè)安全函數(shù)定義于可變 slice 之上:它獲取一個(gè) slice 并從給定的索引參數(shù)開(kāi)始將其分為兩個(gè) slice。split_at_mut
的用法如示例 19-4 所示:
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
示例 19-4: 使用安全的 split_at_mut
函數(shù)
這個(gè)函數(shù)無(wú)法只通過(guò)安全 Rust 實(shí)現(xiàn)。一個(gè)嘗試可能看起來(lái)像示例 19-5,它不能編譯。出于簡(jiǎn)單考慮,我們將 split_at_mut
實(shí)現(xiàn)為函數(shù)而不是方法,并只處理 i32
值而非泛型 T
的 slice。
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!(mid <= len);
(&mut slice[..mid], &mut slice[mid..])
}
示例 19-5: 嘗試只使用安全 Rust 來(lái)實(shí)現(xiàn) split_at_mut
此函數(shù)首先獲取 slice 的長(zhǎng)度,然后通過(guò)檢查參數(shù)是否小于或等于這個(gè)長(zhǎng)度來(lái)斷言參數(shù)所給定的索引位于 slice 當(dāng)中。該斷言意味著如果傳入的索引比要分割的 slice 的索引更大,此函數(shù)在嘗試使用這個(gè)索引前 panic。
之后我們?cè)谝粋€(gè)元組中返回兩個(gè)可變的 slice:一個(gè)從原始 slice 的開(kāi)頭直到 mid
索引,另一個(gè)從 mid
直到原 slice 的結(jié)尾。
如果嘗試編譯示例 19-5 的代碼,會(huì)得到一個(gè)錯(cuò)誤:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
--> src/main.rs:6:30
|
1 | fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&mut slice[..mid], &mut slice[mid..])
| -------------------------^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*slice` is borrowed for `'1`
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` due to previous error
Rust 的借用檢查器不能理解我們要借用這個(gè) slice 的兩個(gè)不同部分:它只知道我們借用了同一個(gè) slice 兩次。本質(zhì)上借用 slice 的不同部分是可以的,因?yàn)榻Y(jié)果兩個(gè) slice 不會(huì)重疊,不過(guò) Rust 還沒(méi)有智能到能夠理解這些。當(dāng)我們知道某些事是可以的而 Rust 不知道的時(shí)候,就是觸及不安全代碼的時(shí)候了
示例 19-6 展示了如何使用 unsafe
塊,裸指針和一些不安全函數(shù)調(diào)用來(lái)實(shí)現(xiàn) split_at_mut
:
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
示例 19-6: 在 split_at_mut
函數(shù)的實(shí)現(xiàn)中使用不安全代碼
回憶第四章的 “Slice 類(lèi)型” 部分,slice 是一個(gè)指向一些數(shù)據(jù)的指針,并帶有該 slice 的長(zhǎng)度??梢允褂?nbsp;len
方法獲取 slice 的長(zhǎng)度,使用 as_mut_ptr
方法訪(fǎng)問(wèn) slice 的裸指針。在這個(gè)例子中,因?yàn)橛幸粋€(gè) i32
值的可變
slice,as_mut_ptr
返回一個(gè) *mut i32
類(lèi)型的裸指針,儲(chǔ)存在 ptr
變量中。
我們保持索引 mid
位于 slice 中的斷言。接著是不安全代碼:slice::from_raw_parts_mut
函數(shù)獲取一個(gè)裸指針和一個(gè)長(zhǎng)度來(lái)創(chuàng)建一個(gè) slice。這里使用此函數(shù)從 ptr
中創(chuàng)建了一個(gè)有 mid
個(gè)項(xiàng)的 slice。之后在 ptr
上調(diào)用 add
方法并使用 mid
作為參數(shù)來(lái)獲取一個(gè)從 mid
開(kāi)始的裸指針,使用這個(gè)裸指針并以 mid
之后項(xiàng)的數(shù)量為長(zhǎng)度創(chuàng)建一個(gè)
slice。
slice::from_raw_parts_mut
函數(shù)是不安全的因?yàn)樗@取一個(gè)裸指針,并必須確信這個(gè)指針是有效的。裸指針上的 add
方法也是不安全的,因?yàn)槠浔仨毚_信此地址偏移量也是有效的指針。因此必須將 slice::from_raw_parts_mut
和 add
放入 unsafe
塊中以便能調(diào)用它們。通過(guò)觀(guān)察代碼,和增加 mid
必然小于等于 len
的斷言,我們可以說(shuō) unsafe
塊中所有的裸指針將是有效的
slice 中數(shù)據(jù)的指針。這是一個(gè)可以接受的 unsafe
的恰當(dāng)用法。
注意無(wú)需將 split_at_mut
函數(shù)的結(jié)果標(biāo)記為 unsafe
,并可以在安全 Rust 中調(diào)用此函數(shù)。我們創(chuàng)建了一個(gè)不安全代碼的安全抽象,其代碼以一種安全的方式使用了 unsafe
代碼,因?yàn)槠渲粡倪@個(gè)函數(shù)訪(fǎng)問(wèn)的數(shù)據(jù)中創(chuàng)建了有效的指針。
與此相對(duì),示例 19-7 中的 slice::from_raw_parts_mut
在使用 slice 時(shí)很有可能會(huì)崩潰。這段代碼獲取任意內(nèi)存地址并創(chuàng)建了一個(gè)長(zhǎng)為一萬(wàn)的 slice:
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let slice: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
示例 19-7: 通過(guò)任意內(nèi)存地址創(chuàng)建 slice
我們并不擁有這個(gè)任意地址的內(nèi)存,也不能保證這段代碼創(chuàng)建的 slice 包含有效的 i32
值。試圖使用臆測(cè)為有效的 slice
會(huì)導(dǎo)致未定義的行為。
有時(shí)你的 Rust 代碼可能需要與其他語(yǔ)言編寫(xiě)的代碼交互。為此 Rust 有一個(gè)關(guān)鍵字,extern
,有助于創(chuàng)建和使用 外部函數(shù)接口(Foreign Function Interface, FFI)。外部函數(shù)接口是一個(gè)編程語(yǔ)言用以定義函數(shù)的方式,其允許不同(外部)編程語(yǔ)言調(diào)用這些函數(shù)。
示例 19-8 展示了如何集成 C 標(biāo)準(zhǔn)庫(kù)中的 abs
函數(shù)。extern
塊中聲明的函數(shù)在 Rust 代碼中總是不安全的。因?yàn)槠渌Z(yǔ)言不會(huì)強(qiáng)制執(zhí)行 Rust 的規(guī)則且 Rust 無(wú)法檢查它們,所以確保其安全是程序員的責(zé)任:
文件名: src/main.rs
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
示例 19-8: 聲明并調(diào)用另一個(gè)語(yǔ)言中定義的 extern
函數(shù)
在 extern "C"
塊中,列出了我們希望能夠調(diào)用的另一個(gè)語(yǔ)言中的外部函數(shù)的簽名和名稱(chēng)。"C"
部分定義了外部函數(shù)所使用的 應(yīng)用二進(jìn)制接口(application binary interface,ABI) —— ABI 定義了如何在匯編語(yǔ)言層面調(diào)用此函數(shù)。"C"
ABI 是最常見(jiàn)的,并遵循 C 編程語(yǔ)言的 ABI。
從其它語(yǔ)言調(diào)用 Rust 函數(shù)
也可以使用
extern
來(lái)創(chuàng)建一個(gè)允許其他語(yǔ)言調(diào)用 Rust 函數(shù)的接口。不同于extern
塊,就在fn
關(guān)鍵字之前增加extern
關(guān)鍵字并指定所用到的 ABI。還需增加#[no_mangle]
注解來(lái)告訴 Rust 編譯器不要 mangle 此函數(shù)的名稱(chēng)。Mangling 發(fā)生于當(dāng)編譯器將我們指定的函數(shù)名修改為不同的名稱(chēng)時(shí),這會(huì)增加用于其他編譯過(guò)程的額外信息,不過(guò)會(huì)使其名稱(chēng)更難以閱讀。每一個(gè)編程語(yǔ)言的編譯器都會(huì)以稍微不同的方式 mangle 函數(shù)名,所以為了使 Rust 函數(shù)能在其他語(yǔ)言中指定,必須禁用 Rust 編譯器的 name mangling。
在如下的例子中,一旦其編譯為動(dòng)態(tài)庫(kù)并從 C 語(yǔ)言中鏈接,
call_from_c
函數(shù)就能夠在 C 代碼中訪(fǎng)問(wèn):
#[no_mangle] pub extern "C" fn call_from_c() { println!("Just called a Rust function from C!"); }
extern
的使用無(wú)需unsafe
。
目前為止全書(shū)都盡量避免討論 全局變量(global variables),Rust 確實(shí)支持他們,不過(guò)這對(duì)于 Rust 的所有權(quán)規(guī)則來(lái)說(shuō)是有問(wèn)題的。如果有兩個(gè)線(xiàn)程訪(fǎng)問(wèn)相同的可變?nèi)肿兞?,則可能會(huì)造成數(shù)據(jù)競(jìng)爭(zhēng)。
全局變量在 Rust 中被稱(chēng)為 靜態(tài)(static)變量。示例 19-9 展示了一個(gè)擁有字符串 slice 值的靜態(tài)變量的聲明和應(yīng)用:
文件名: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("name is: {}", HELLO_WORLD);
}
示例 19-9: 定義和使用一個(gè)不可變靜態(tài)變量
靜態(tài)(static
)變量類(lèi)似于第三章 “變量和常量的區(qū)別” 部分討論的常量。通常靜態(tài)變量的名稱(chēng)采用 SCREAMING_SNAKE_CASE
寫(xiě)法。靜態(tài)變量只能儲(chǔ)存擁有 'static
生命周期的引用,這意味著 Rust 編譯器可以自己計(jì)算出其生命周期而無(wú)需顯式標(biāo)注。訪(fǎng)問(wèn)不可變靜態(tài)變量是安全的。
常量與不可變靜態(tài)變量可能看起來(lái)很類(lèi)似,不過(guò)一個(gè)微妙的區(qū)別是靜態(tài)變量中的值有一個(gè)固定的內(nèi)存地址。使用這個(gè)值總是會(huì)訪(fǎng)問(wèn)相同的地址。另一方面,常量則允許在任何被用到的時(shí)候復(fù)制其數(shù)據(jù)。
常量與靜態(tài)變量的另一個(gè)區(qū)別在于靜態(tài)變量可以是可變的。訪(fǎng)問(wèn)和修改可變靜態(tài)變量都是 不安全 的。示例 19-10 展示了如何聲明、訪(fǎng)問(wèn)和修改名為 COUNTER
的可變靜態(tài)變量:
文件名: src/main.rs
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
示例 19-10: 讀取或修改一個(gè)可變靜態(tài)變量是不安全的
就像常規(guī)變量一樣,我們使用 mut
關(guān)鍵來(lái)指定可變性。任何讀寫(xiě) COUNTER
的代碼都必須位于 unsafe
塊中。這段代碼可以編譯并如期打印出 COUNTER: 3
,因?yàn)檫@是單線(xiàn)程的。擁有多個(gè)線(xiàn)程訪(fǎng)問(wèn) COUNTER
則可能導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)。
擁有可以全局訪(fǎng)問(wèn)的可變數(shù)據(jù),難以保證不存在數(shù)據(jù)競(jìng)爭(zhēng),這就是為何 Rust 認(rèn)為可變靜態(tài)變量是不安全的。任何可能的情況,請(qǐng)優(yōu)先使用第十六章討論的并發(fā)技術(shù)和線(xiàn)程安全智能指針,這樣編譯器就能檢測(cè)不同線(xiàn)程間的數(shù)據(jù)訪(fǎng)問(wèn)是否是安全的。
unsafe
的另一個(gè)操作用例是實(shí)現(xiàn)不安全 trait。當(dāng) trait 中至少有一個(gè)方法中包含編譯器無(wú)法驗(yàn)證的不變式(invariant)時(shí) trait 是不安全的。可以在 trait
之前增加 unsafe
關(guān)鍵字將 trait 聲明為 unsafe
,同時(shí) trait 的實(shí)現(xiàn)也必須標(biāo)記為 unsafe
,如示例 19-11 所示:
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
fn main() {}
示例 19-11: 定義并實(shí)現(xiàn)不安全 trait
通過(guò) unsafe impl
,我們承諾將保證編譯器所不能驗(yàn)證的不變量。
作為一個(gè)例子,回憶第十六章 “使用 Sync 和 Send trait 的可擴(kuò)展并發(fā)” 部分中的 Sync
和 Send
標(biāo)記 trait,編譯器會(huì)自動(dòng)為完全由 Send
和 Sync
類(lèi)型組成的類(lèi)型自動(dòng)實(shí)現(xiàn)他們。如果實(shí)現(xiàn)了一個(gè)包含一些不是 Send
或 Sync
的類(lèi)型,比如裸指針,并希望將此類(lèi)型標(biāo)記為 Send
或 Sync
,則必須使用 unsafe
。Rust 不能驗(yàn)證我們的類(lèi)型保證可以安全的跨線(xiàn)程發(fā)送或在多線(xiàn)程間訪(fǎng)問(wèn),所以需要我們自己進(jìn)行檢查并通過(guò) unsafe
表明。
僅適用于 unsafe
的最后一個(gè)操作是訪(fǎng)問(wèn) 聯(lián)合體 中的字段,union
和 struct
類(lèi)似,但是在一個(gè)實(shí)例中同時(shí)只能使用一個(gè)聲明的字段。聯(lián)合體主要用于和 C 代碼中的聯(lián)合體交互。訪(fǎng)問(wèn)聯(lián)合體的字段是不安全的,因?yàn)?Rust 無(wú)法保證當(dāng)前存儲(chǔ)在聯(lián)合體實(shí)例中數(shù)據(jù)的類(lèi)型??梢圆榭?nbsp;參考文檔 了解有關(guān)聯(lián)合體的更多信息。
使用 unsafe
來(lái)進(jìn)行這五個(gè)操作(超能力)之一是沒(méi)有問(wèn)題的,甚至是不需要深思熟慮的,不過(guò)使得 unsafe
代碼正確也實(shí)屬不易,因?yàn)榫幾g器不能幫助保證內(nèi)存安全。當(dāng)有理由使用 unsafe
代碼時(shí),是可以這么做的,通過(guò)使用顯式的 unsafe
標(biāo)注可以更容易地在錯(cuò)誤發(fā)生時(shí)追蹤問(wèn)題的源頭。
更多建議: