Rust 通過(guò) Deref trait 將智能指針當(dāng)作常規(guī)引用處理

2023-03-22 15:13 更新
ch15-02-deref.md
commit 5bebc80f61d33438f5598c1f7a20cc16be88ed08

實(shí)現(xiàn) Deref trait 允許我們重載 解引用運(yùn)算符dereference operator*(與乘法運(yùn)算符或通配符相區(qū)別)。通過(guò)這種方式實(shí)現(xiàn) Deref trait 的智能指針可以被當(dāng)作常規(guī)引用來(lái)對(duì)待,可以編寫(xiě)操作引用的代碼并用于智能指針。

讓我們首先看看解引用運(yùn)算符如何處理常規(guī)引用,接著嘗試定義我們自己的類(lèi)似 Box<T> 的類(lèi)型并看看為何解引用運(yùn)算符不能像引用一樣工作。我們會(huì)探索如何實(shí)現(xiàn) Deref trait 使得智能指針以類(lèi)似引用的方式工作變?yōu)榭赡?。最后,我們?huì)討論 Rust 的 Deref 強(qiáng)制轉(zhuǎn)換deref coercions)功能以及它是如何處理引用或智能指針的。

我們將要構(gòu)建的 ?MyBox<T>? 類(lèi)型與真正的 ?Box<T>? 有一個(gè)很大的區(qū)別:我們的版本不會(huì)在堆上儲(chǔ)存數(shù)據(jù)。這個(gè)例子重點(diǎn)關(guān)注 ?Deref?,所以其數(shù)據(jù)實(shí)際存放在何處,相比其類(lèi)似指針的行為來(lái)說(shuō)不算重要。

通過(guò)解引用運(yùn)算符追蹤指針的值

常規(guī)引用是一個(gè)指針類(lèi)型,一種理解指針的方式是將其看成指向儲(chǔ)存在其他某處值的箭頭。在示例 15-6 中,創(chuàng)建了一個(gè) i32 值的引用,接著使用解引用運(yùn)算符來(lái)跟蹤所引用的數(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)算符來(lái)跟蹤 i32 值的引用

變量 x 存放了一個(gè) i32 值 5。y 等于 x 的一個(gè)引用??梢詳嘌?nbsp;x 等于 5。然而,如果希望對(duì) y 的值做出斷言,必須使用 *y 來(lái)追蹤引用所指向的值(也就是 解引用)。一旦解引用了 y,就可以訪問(wèn) y 所指向的整型值并可以與 5 做比較。

相反如果嘗試編寫(xiě) assert_eq!(5, y);,則會(huì)得到如下編譯錯(cuò)誤:

$ 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)樗鼈兪遣煌念?lèi)型。必須使用解引用運(yùn)算符追蹤引用所指向的值。

像引用一樣使用 Box<T>

可以使用 Box<T> 代替引用來(lái)重寫(xiě)示例 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è)置為一個(gè)指向 x 值拷貝的 box 實(shí)例,而不是指向 x 值的引用。在最后的斷言中,可以使用解引用運(yùn)算符以 y 為引用時(shí)相同的方式追蹤 box 的指針。接下來(lái)讓我們通過(guò)實(shí)現(xiàn)自己的 box 類(lèi)型來(lái)探索 Box<T> 能這么做有何特殊之處。

自定義智能指針

為了體會(huì)默認(rèn)情況下智能指針與引用的不同,讓我們創(chuàng)建一個(gè)類(lèi)似于標(biāo)準(zhǔn)庫(kù)提供的 Box<T> 類(lèi)型的智能指針。接著學(xué)習(xí)如何增加使用解引用運(yùn)算符的功能。

從根本上說(shuō),Box<T> 被定義為包含一個(gè)元素的元組結(jié)構(gòu)體,所以示例 15-8 以相同的方式定義了 MyBox<T> 類(lèi)型。我們還定義了 new 函數(shù)來(lái)對(duì)應(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> 類(lèi)型

這里定義了一個(gè)結(jié)構(gòu)體 MyBox 并聲明了一個(gè)泛型參數(shù) T,因?yàn)槲覀兿M淇梢源娣湃魏晤?lèi)型的值。MyBox 是一個(gè)包含 T 類(lèi)型元素的元組結(jié)構(gòu)體。MyBox::new 函數(shù)獲取一個(gè) T 類(lèi)型的參數(shù)并返回一個(gè)存放傳入值的 MyBox 實(shí)例。

嘗試將示例 15-7 中的代碼加入示例 15-8 中并修改 main 使用我們定義的 MyBox<T> 類(lèi)型代替 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>

得到的編譯錯(cuò)誤是:

$ 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> 類(lèi)型不能解引用,因?yàn)槲覀兩形丛谠擃?lèi)型實(shí)現(xiàn)這個(gè)功能。為了啟用 * 運(yùn)算符的解引用功能,需要實(shí)現(xiàn) Deref trait。

通過(guò)實(shí)現(xiàn) Deref trait 將某類(lèi)型像引用一樣處理

如第十章 “為類(lèi)型實(shí)現(xiàn) trait” 部分所討論的,為了實(shí)現(xiàn) trait,需要提供 trait 所需的方法實(shí)現(xiàn)。Deref trait,由標(biāo)準(zhǔn)庫(kù)提供,要求實(shí)現(xiàn)名為 deref 的方法,其借用 self 并返回一個(gè)內(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; 語(yǔ)法定義了用于此 trait 的關(guān)聯(lián)類(lèi)型。關(guān)聯(lián)類(lèi)型是一個(gè)稍有不同的定義泛型參數(shù)的方式,現(xiàn)在還無(wú)需過(guò)多的擔(dān)心它;第十九章會(huì)詳細(xì)介紹。

deref 方法體中寫(xiě)入了 &self.0,這樣 deref 返回了我希望通過(guò) * 運(yùn)算符訪問(wèn)的值的引用?;貞浺幌碌谖逭?nbsp;“使用沒(méi)有命名字段的元組結(jié)構(gòu)體來(lái)創(chuàng)建不同的類(lèi)型” 部分 .0 用來(lái)訪問(wèn)元組結(jié)構(gòu)體的第一個(gè)元素。示例 15-9 中的 main 函數(shù)中對(duì) MyBox<T> 值的 * 調(diào)用現(xiàn)在可以編譯并能通過(guò)斷言了!

沒(méi)有 Deref trait 的話(huà),編譯器只會(huì)解引用 & 引用類(lèi)型。deref 方法向編譯器提供了獲取任何實(shí)現(xiàn)了 Deref trait 的類(lèi)型的值,并且調(diào)用這個(gè)類(lèi)型的 deref 方法來(lái)獲取一個(gè)它知道如何解引用的 & 引用的能力。

當(dāng)我們?cè)谑纠?15-9 中輸入 *y 時(shí),Rust 事實(shí)上在底層運(yùn)行了如下代碼:

*(y.deref())

Rust 將 * 運(yùn)算符替換為先調(diào)用 deref 方法再進(jìn)行普通解引用的操作,如此我們便不用擔(dān)心是否還需手動(dòng)調(diào)用 deref 方法了。Rust 的這個(gè)特性可以讓我們寫(xiě)出行為一致的代碼,無(wú)論是面對(duì)的是常規(guī)引用還是實(shí)現(xiàn)了 Deref 的類(lèi)型。

deref 方法返回值的引用,以及 *(y.deref()) 括號(hào)外邊的普通解引用仍為必須的原因在于所有權(quán)。如果 deref 方法直接返回值而不是值的引用,其值(的所有權(quán))將被移出 self。在這里以及大部分使用解引用運(yùn)算符的情況下我們并不希望獲取 MyBox<T> 內(nèi)部值的所有權(quán)。

注意,每次當(dāng)我們?cè)诖a中使用 * 時(shí), * 運(yùn)算符都被替換成了先調(diào)用 deref 方法再接著使用 * 解引用的操作,且只會(huì)發(fā)生一次,不會(huì)對(duì) * 操作符無(wú)限遞歸替換,解引用出上面 i32 類(lèi)型的值就停止了,這個(gè)值與示例 15-9 中 assert_eq! 的 5 相匹配。

函數(shù)和方法的隱式 Deref 強(qiáng)制轉(zhuǎn)換

Deref 強(qiáng)制轉(zhuǎn)換deref coercions)是 Rust 在函數(shù)或方法傳參上的一種便利。Deref 強(qiáng)制轉(zhuǎn)換只能作用于實(shí)現(xiàn)了 Deref trait 的類(lèi)型。Deref 強(qiáng)制轉(zhuǎn)換將這樣一個(gè)類(lèi)型的引用轉(zhuǎn)換為另一個(gè)類(lèi)型的引用。例如,Deref 強(qiáng)制轉(zhuǎn)換 可以將 &String 轉(zhuǎn)換為 &str,因?yàn)?nbsp;String 實(shí)現(xiàn)了 Deref trait 因此可以返回 &str。當(dāng)這種特定類(lèi)型的引用作為實(shí)參傳遞給和形參類(lèi)型不同的函數(shù)或方法時(shí),Deref 強(qiáng)制轉(zhuǎn)換將自動(dòng)發(fā)生。這時(shí)會(huì)有一系列的 deref 方法被調(diào)用,把我們提供的類(lèi)型轉(zhuǎn)換成了參數(shù)所需的類(lèi)型。

Deref 強(qiáng)制轉(zhuǎn)換的加入使得 Rust 程序員編寫(xiě)函數(shù)和方法調(diào)用時(shí)無(wú)需增加過(guò)多顯式使用 & 和 * 的引用和解引用。這個(gè)功能也使得我們可以編寫(xiě)更多同時(shí)作用于引用或智能指針的代碼。

作為展示 Deref 強(qiáng)制轉(zhuǎn)換的實(shí)例,讓我們使用示例 15-8 中定義的 MyBox<T>,以及示例 15-10 中增加的 Deref 實(shí)現(xiàn)。示例 15-11 展示了一個(gè)有著字符串 slice 參數(shù)的函數(shù)定義:

文件名: src/main.rs

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

示例 15-11:hello 函數(shù)有著 &str 類(lèi)型的參數(shù) name

可以使用字符串 slice 作為參數(shù)調(diào)用 hello 函數(shù),比如 hello("Rust");。Deref 強(qiáng)制轉(zhuǎn)換使得用 MyBox<String> 類(lèi)型值的引用調(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 可以通過(guò) deref 調(diào)用將 &MyBox<String> 變?yōu)?nbsp;&String。標(biāo)準(zhǔn)庫(kù)中提供了 String 上的 Deref 實(shí)現(xiàn),其會(huì)返回字符串 slice,這可以在 Deref 的 API 文檔中看到。Rust 再次調(diào)用 deref 將 &String 變?yōu)?nbsp;&str,這就符合 hello 函數(shù)的定義了。

如果 Rust 沒(méi)有實(shí)現(xiàn) Deref 強(qiáng)制轉(zhuǎn)換,為了使用 &MyBox<String> 類(lèi)型的值調(diào)用 hello,則不得不編寫(xiě)示例 15-13 中的代碼來(lái)代替示例 15-12:

文件名: src/main.rs

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

示例 15-13:如果 Rust 沒(méi)有 Deref 強(qiáng)制轉(zhuǎn)換則必須編寫(xiě)的代碼

(*m) 將 MyBox<String> 解引用為 String。接著 & 和 [..] 獲取了整個(gè) String 的字符串 slice 來(lái)匹配 hello 的簽名。沒(méi)有 Deref 強(qiáng)制轉(zhuǎn)換所有這些符號(hào)混在一起將更難以讀寫(xiě)和理解。Deref 強(qiáng)制轉(zhuǎn)換使得 Rust 自動(dòng)的幫我們處理這些轉(zhuǎn)換。

當(dāng)所涉及到的類(lèi)型定義了 Deref trait,Rust 會(huì)分析這些類(lèi)型并使用任意多次 Deref::deref 調(diào)用以獲得匹配參數(shù)的類(lèi)型。這些解析都發(fā)生在編譯時(shí),所以利用 Deref 強(qiáng)制轉(zhuǎn)換并沒(méi)有運(yùn)行時(shí)損耗!

Deref 強(qiáng)制轉(zhuǎn)換如何與可變性交互

類(lèi)似于如何使用 Deref trait 重載不可變引用的 * 運(yùn)算符,Rust 提供了 DerefMut trait 用于重載可變引用的 * 運(yùn)算符。

Rust 在發(fā)現(xiàn)類(lèi)型和 trait 實(shí)現(xiàn)滿(mǎn)足三種情況時(shí)會(huì)進(jìn)行 Deref 強(qiáng)制轉(zhuǎn)換:

  • 當(dāng) ?T: Deref<Target=U>? 時(shí)從 ?&T? 到 ?&U?。
  • 當(dāng) ?T: DerefMut<Target=U>? 時(shí)從 ?&mut T? 到 ?&mut U?。
  • 當(dāng) ?T: Deref<Target=U>? 時(shí)從 ?&mut T? 到 ?&U?。

頭兩個(gè)情況除了可變性之外是相同的:第一種情況表明如果有一個(gè) &T,而 T 實(shí)現(xiàn)了返回 U 類(lèi)型的 Deref,則可以直接得到 &U。第二種情況表明對(duì)于可變引用也有著相同的行為。

第三個(gè)情況有些微妙:Rust 也會(huì)將可變引用強(qiáng)轉(zhuǎn)為不可變引用。但是反之是 不可能 的:不可變引用永遠(yuǎn)也不能強(qiáng)轉(zhuǎn)為可變引用。因?yàn)楦鶕?jù)借用規(guī)則,如果有一個(gè)可變引用,其必須是這些數(shù)據(jù)的唯一引用(否則程序?qū)o(wú)法編譯)。將一個(gè)可變引用轉(zhuǎn)換為不可變引用永遠(yuǎn)也不會(huì)打破借用規(guī)則。將不可變引用轉(zhuǎn)換為可變引用則需要初始的不可變引用是數(shù)據(jù)唯一的不可變引用,而借用規(guī)則無(wú)法保證這一點(diǎn)。因此,Rust 無(wú)法假設(shè)將不可變引用轉(zhuǎn)換為可變引用是可能的。


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)