ch15-02-deref.md
commit 5bebc80f61d33438f5598c1f7a20cc16be88ed08
實(shí)現(xiàn) Deref
trait 允許我們重載 解引用運(yùn)算符(dereference operator)*
(與乘法運(yùn)算符或通配符相區(qū)別)。通過這種方式實(shí)現(xiàn) Deref
trait 的智能指針可以被當(dāng)作常規(guī)引用來對待,可以編寫操作引用的代碼并用于智能指針。
讓我們首先看看解引用運(yùn)算符如何處理常規(guī)引用,接著嘗試定義我們自己的類似 Box<T>
的類型并看看為何解引用運(yùn)算符不能像引用一樣工作。我們會探索如何實(shí)現(xiàn) Deref
trait 使得智能指針以類似引用的方式工作變?yōu)榭赡?。最后,我們會討?Rust 的 Deref 強(qiáng)制轉(zhuǎn)換(deref coercions)功能以及它是如何處理引用或智能指針的。
我們將要構(gòu)建的 ?
MyBox<T>
? 類型與真正的 ?Box<T>
? 有一個很大的區(qū)別:我們的版本不會在堆上儲存數(shù)據(jù)。這個例子重點(diǎn)關(guān)注 ?Deref
?,所以其數(shù)據(jù)實(shí)際存放在何處,相比其類似指針的行為來說不算重要。
常規(guī)引用是一個指針類型,一種理解指針的方式是將其看成指向儲存在其他某處值的箭頭。在示例 15-6 中,創(chuàng)建了一個 i32
值的引用,接著使用解引用運(yùn)算符來跟蹤所引用的數(shù)據(jù):
文件名: src/main.rs
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
示例 15-6:使用解引用運(yùn)算符來跟蹤 i32
值的引用
變量 x
存放了一個 i32
值 5
。y
等于 x
的一個引用??梢詳嘌?nbsp;x
等于 5
。然而,如果希望對 y
的值做出斷言,必須使用 *y
來追蹤引用所指向的值(也就是 解引用)。一旦解引用了 y
,就可以訪問 y
所指向的整型值并可以與 5
做比較。
相反如果嘗試編寫 assert_eq!(5, y);
,則會得到如下編譯錯誤:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` due to previous error
不允許比較數(shù)字的引用與數(shù)字,因?yàn)樗鼈兪遣煌念愋?。必須使用解引用運(yùn)算符追蹤引用所指向的值。
可以使用 Box<T>
代替引用來重寫示例 15-6 中的代碼,解引用運(yùn)算符也一樣能工作,如示例 15-7 所示:
文件名: src/main.rs
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
示例 15-7:在 Box<i32>
上使用解引用運(yùn)算符
示例 15-7 相比示例 15-6 主要不同的地方就是將 y
設(shè)置為一個指向 x
值拷貝的 box 實(shí)例,而不是指向 x
值的引用。在最后的斷言中,可以使用解引用運(yùn)算符以 y
為引用時相同的方式追蹤 box 的指針。接下來讓我們通過實(shí)現(xiàn)自己的 box 類型來探索 Box<T>
能這么做有何特殊之處。
為了體會默認(rèn)情況下智能指針與引用的不同,讓我們創(chuàng)建一個類似于標(biāo)準(zhǔn)庫提供的 Box<T>
類型的智能指針。接著學(xué)習(xí)如何增加使用解引用運(yùn)算符的功能。
從根本上說,Box<T>
被定義為包含一個元素的元組結(jié)構(gòu)體,所以示例 15-8 以相同的方式定義了 MyBox<T>
類型。我們還定義了 new
函數(shù)來對應(yīng)定義于 Box<T>
的 new
函數(shù):
文件名: src/main.rs
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
示例 15-8:定義 MyBox<T>
類型
這里定義了一個結(jié)構(gòu)體 MyBox
并聲明了一個泛型參數(shù) T
,因?yàn)槲覀兿M淇梢源娣湃魏晤愋偷闹怠?code>MyBox 是一個包含 T
類型元素的元組結(jié)構(gòu)體。MyBox::new
函數(shù)獲取一個 T
類型的參數(shù)并返回一個存放傳入值的 MyBox
實(shí)例。
嘗試將示例 15-7 中的代碼加入示例 15-8 中并修改 main
使用我們定義的 MyBox<T>
類型代替 Box<T>
。示例 15-9 中的代碼不能編譯,因?yàn)?Rust 不知道如何解引用 MyBox
:
文件名: src/main.rs
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
示例 15-9:嘗試以使用引用和 Box<T>
相同的方式使用 MyBox<T>
得到的編譯錯誤是:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` due to previous error
MyBox<T>
類型不能解引用,因?yàn)槲覀兩形丛谠擃愋蛯?shí)現(xiàn)這個功能。為了啟用 *
運(yùn)算符的解引用功能,需要實(shí)現(xiàn) Deref
trait。
如第十章 “為類型實(shí)現(xiàn) trait” 部分所討論的,為了實(shí)現(xiàn) trait,需要提供 trait 所需的方法實(shí)現(xiàn)。Deref
trait,由標(biāo)準(zhǔn)庫提供,要求實(shí)現(xiàn)名為 deref
的方法,其借用 self
并返回一個內(nèi)部數(shù)據(jù)的引用。示例 15-10 包含定義于 MyBox
之上的 Deref
實(shí)現(xiàn):
文件名: src/main.rs
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
示例 15-10:MyBox<T>
上的 Deref
實(shí)現(xiàn)
type Target = T;
語法定義了用于此 trait 的關(guān)聯(lián)類型。關(guān)聯(lián)類型是一個稍有不同的定義泛型參數(shù)的方式,現(xiàn)在還無需過多的擔(dān)心它;第十九章會詳細(xì)介紹。
deref
方法體中寫入了 &self.0
,這樣 deref
返回了我希望通過 *
運(yùn)算符訪問的值的引用?;貞浺幌碌谖逭?nbsp;“使用沒有命名字段的元組結(jié)構(gòu)體來創(chuàng)建不同的類型” 部分 .0
用來訪問元組結(jié)構(gòu)體的第一個元素。示例 15-9 中的 main
函數(shù)中對 MyBox<T>
值的 *
調(diào)用現(xiàn)在可以編譯并能通過斷言了!
沒有 Deref
trait 的話,編譯器只會解引用 &
引用類型。deref
方法向編譯器提供了獲取任何實(shí)現(xiàn)了 Deref
trait 的類型的值,并且調(diào)用這個類型的 deref
方法來獲取一個它知道如何解引用的 &
引用的能力。
當(dāng)我們在示例 15-9 中輸入 *y
時,Rust 事實(shí)上在底層運(yùn)行了如下代碼:
*(y.deref())
Rust 將 *
運(yùn)算符替換為先調(diào)用 deref
方法再進(jìn)行普通解引用的操作,如此我們便不用擔(dān)心是否還需手動調(diào)用 deref
方法了。Rust 的這個特性可以讓我們寫出行為一致的代碼,無論是面對的是常規(guī)引用還是實(shí)現(xiàn)了 Deref
的類型。
deref
方法返回值的引用,以及 *(y.deref())
括號外邊的普通解引用仍為必須的原因在于所有權(quán)。如果 deref
方法直接返回值而不是值的引用,其值(的所有權(quán))將被移出 self
。在這里以及大部分使用解引用運(yùn)算符的情況下我們并不希望獲取 MyBox<T>
內(nèi)部值的所有權(quán)。
注意,每次當(dāng)我們在代碼中使用 *
時, *
運(yùn)算符都被替換成了先調(diào)用 deref
方法再接著使用 *
解引用的操作,且只會發(fā)生一次,不會對 *
操作符無限遞歸替換,解引用出上面 i32
類型的值就停止了,這個值與示例 15-9 中 assert_eq!
的 5
相匹配。
Deref 強(qiáng)制轉(zhuǎn)換(deref coercions)是 Rust 在函數(shù)或方法傳參上的一種便利。Deref 強(qiáng)制轉(zhuǎn)換只能作用于實(shí)現(xiàn)了 Deref
trait 的類型。Deref 強(qiáng)制轉(zhuǎn)換將這樣一個類型的引用轉(zhuǎn)換為另一個類型的引用。例如,Deref 強(qiáng)制轉(zhuǎn)換 可以將 &String
轉(zhuǎn)換為 &str
,因?yàn)?nbsp;String
實(shí)現(xiàn)了 Deref
trait 因此可以返回 &str
。當(dāng)這種特定類型的引用作為實(shí)參傳遞給和形參類型不同的函數(shù)或方法時,Deref 強(qiáng)制轉(zhuǎn)換將自動發(fā)生。這時會有一系列的 deref
方法被調(diào)用,把我們提供的類型轉(zhuǎn)換成了參數(shù)所需的類型。
Deref 強(qiáng)制轉(zhuǎn)換的加入使得 Rust 程序員編寫函數(shù)和方法調(diào)用時無需增加過多顯式使用 &
和 *
的引用和解引用。這個功能也使得我們可以編寫更多同時作用于引用或智能指針的代碼。
作為展示 Deref 強(qiáng)制轉(zhuǎn)換的實(shí)例,讓我們使用示例 15-8 中定義的 MyBox<T>
,以及示例 15-10 中增加的 Deref
實(shí)現(xiàn)。示例 15-11 展示了一個有著字符串 slice 參數(shù)的函數(shù)定義:
文件名: src/main.rs
fn hello(name: &str) {
println!("Hello, {}!", name);
}
示例 15-11:hello
函數(shù)有著 &str
類型的參數(shù) name
可以使用字符串 slice 作為參數(shù)調(diào)用 hello
函數(shù),比如 hello("Rust");
。Deref 強(qiáng)制轉(zhuǎn)換使得用 MyBox<String>
類型值的引用調(diào)用 hello
成為可能,如示例 15-12 所示:
文件名: src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
示例 15-12:因?yàn)?Deref 強(qiáng)制轉(zhuǎn)換,使用 MyBox<String>
的引用調(diào)用 hello
是可行的
這里使用 &m
調(diào)用 hello
函數(shù),其為 MyBox<String>
值的引用。因?yàn)槭纠?15-10 中在 MyBox<T>
上實(shí)現(xiàn)了 Deref
trait,Rust 可以通過 deref
調(diào)用將 &MyBox<String>
變?yōu)?nbsp;&String
。標(biāo)準(zhǔn)庫中提供了 String
上的 Deref
實(shí)現(xiàn),其會返回字符串 slice,這可以在 Deref
的 API 文檔中看到。Rust 再次調(diào)用 deref
將 &String
變?yōu)?nbsp;&str
,這就符合 hello
函數(shù)的定義了。
如果 Rust 沒有實(shí)現(xiàn) Deref 強(qiáng)制轉(zhuǎn)換,為了使用 &MyBox<String>
類型的值調(diào)用 hello
,則不得不編寫示例 15-13 中的代碼來代替示例 15-12:
文件名: src/main.rs
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
示例 15-13:如果 Rust 沒有 Deref 強(qiáng)制轉(zhuǎn)換則必須編寫的代碼
(*m)
將 MyBox<String>
解引用為 String
。接著 &
和 [..]
獲取了整個 String
的字符串 slice 來匹配 hello
的簽名。沒有 Deref 強(qiáng)制轉(zhuǎn)換所有這些符號混在一起將更難以讀寫和理解。Deref 強(qiáng)制轉(zhuǎn)換使得 Rust 自動的幫我們處理這些轉(zhuǎn)換。
當(dāng)所涉及到的類型定義了 Deref
trait,Rust 會分析這些類型并使用任意多次 Deref::deref
調(diào)用以獲得匹配參數(shù)的類型。這些解析都發(fā)生在編譯時,所以利用 Deref 強(qiáng)制轉(zhuǎn)換并沒有運(yùn)行時損耗!
類似于如何使用 Deref
trait 重載不可變引用的 *
運(yùn)算符,Rust 提供了 DerefMut
trait 用于重載可變引用的 *
運(yùn)算符。
Rust 在發(fā)現(xiàn)類型和 trait 實(shí)現(xiàn)滿足三種情況時會進(jìn)行 Deref 強(qiáng)制轉(zhuǎn)換:
T: Deref<Target=U>
? 時從 ?&T
? 到 ?&U
?。T: DerefMut<Target=U>
? 時從 ?&mut T
? 到 ?&mut U
?。T: Deref<Target=U>
? 時從 ?&mut T
? 到 ?&U
?。頭兩個情況除了可變性之外是相同的:第一種情況表明如果有一個 &T
,而 T
實(shí)現(xiàn)了返回 U
類型的 Deref
,則可以直接得到 &U
。第二種情況表明對于可變引用也有著相同的行為。
第三個情況有些微妙:Rust 也會將可變引用強(qiáng)轉(zhuǎn)為不可變引用。但是反之是 不可能 的:不可變引用永遠(yuǎn)也不能強(qiáng)轉(zhuǎn)為可變引用。因?yàn)楦鶕?jù)借用規(guī)則,如果有一個可變引用,其必須是這些數(shù)據(jù)的唯一引用(否則程序?qū)o法編譯)。將一個可變引用轉(zhuǎn)換為不可變引用永遠(yuǎn)也不會打破借用規(guī)則。將不可變引用轉(zhuǎn)換為可變引用則需要初始的不可變引用是數(shù)據(jù)唯一的不可變引用,而借用規(guī)則無法保證這一點(diǎn)。因此,Rust 無法假設(shè)將不可變引用轉(zhuǎn)換為可變引用是可能的。
更多建議: