Rust 從 0 到 1 - 智能指針 - Deref trait

 實現了 Deref trait 的智能指針可以被看作普通的引用,適用於引用的代碼同樣也可以作用於智能指針。

實現 Deref trait 使我們可以自定義解引用操作符的行爲(*,dereference operator,不是乘法運算符或通配符)。實現了 Deref trait 的智能指針可以被看作普通的引用,適用於引用的代碼同樣也可以作用於智能指針。

下面我們首先看看解引用操作對於普通的引用時如何工作的,接着嘗試定義一個類似 Box 的類型,並分析在我們新定義的類型中解引用運算符爲什麼不能像普通引用一樣工作。我們會討論如何實現 Deref trait 才能使得智能指針以類似普通引用的方式工作。最後,我們會討論 Rust 中的強制隱式轉換(deref coercions)功能以及它是如何讓我們可以不用關心它是普通引用或智能指針的。

解引用操作

普通的引用是一個指針,我們可以把指針理解爲指向儲存在某處值的箭頭。下面的例子中,我們創建了一個 i32 類型值的引用,接着使用解引用運算符來訪問其所引用的數據:

fn main() {
    let x = 5;
    let y = &x;
    assert_eq!(5, x);
    assert_eq!(5, *y);
}

變量 x 存放了一個 i32 類型的值,5。y 是 x 的一個引用。我們可以斷言 x 等於 5。然而,如果希望同樣對 y 的值做出斷言,必須使用 * 來訪問引用所指向的值(即,dereference,解引用)。我們可以通過解引用 y,訪問 y 所指向的整型值並將其與 5 做比較。

如果我們嘗試對 y 直接進行斷言: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 a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example`
To learn more, run the command again with --verbose.

數值和數值的引用是不同的類型,在 Rust 中它們不允許直接比較,因此必須使用解引用操作獲得引用所指向的值。

解引用 Box

如果我們使用 Box 重寫上面的例子,也一樣可以使用解引用操作,參考下面的例子:

fn main() {
    let x = 5;
    let y = Box::new(x);
    assert_eq!(5, x);
    assert_eq!(5, *y);
}

上面的例子中 y 的值爲一個 box 實例,它指向 x 值的拷貝 ,即存儲在堆上的 5。在斷言中,我們可以像解引用普通引用一樣,通過解引用來獲得 box 所指向的值。下面我們將通過實現一個類似 box 的智能指針類型來討論 Box 爲什麼可以做到這一點。

自定義智能指針

讓我們通過創建一個類似標準庫中 Box 類型的智能指針,切身體會下智能指針與普通引用的不同。接着我們將介紹如何爲其增加解引用的能力。

Box 實際上就是一個包含一個元素的元組結構體,因此我們也使用同樣的方式創建我們自定義的 MyBox 類型,包括定義於 Box 的 new 函數:

struct MyBox<T>(T);
impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

MyBox 是一個包含 T 類型元素的元組結構體。MyBox::new 函數獲取一個 T 類型的參數並返回一個包含傳入值的 MyBox 實例。將前面例子中的 Box 替換爲我們自定義的 MyBox 。嘗試編譯,將得到類似下面的錯誤:

$ 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);
   |                   ^^
error: aborting due to previous error
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example`
To learn more, run the command again with --verbose.

MyBox 不能被解引用,因爲我們尚未爲其實現這個功能。爲了可以使用 * 操作符進行解引,我們需要實現 Deref trait。

實現 Deref trait

Deref trait,由標準庫提供,要求實現 deref 方法,其僅包含一個參數,self 的引用(方法默認的第一個參數),並返回一個內部數據的引用。參考下面的例子:

use std::ops::Deref;
impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0
    }
}

type Target = T; 語法定義了 Deref  trait 中使用的關聯類型(associated type)。現在我們先忽略它,後面的章節會進行詳細的討論。deref 方法中通過 &self.0 返回了 MyBox 中存儲的值的引用。現在我的例子可以編譯通過了,並且斷言 assert_eq!(5, *y); 也可以通過!

沒有 Deref trait 的話,編譯器只能解引用使用 & 操作符的引用。deref 方法讓編譯器可以處理任何實現了 Deref trait 的值,它通過調用其 deref 方法得到如何進行解引用的 & 引用。即,當我們輸入 *y 時,Rust 事實上在底層運行了如下代碼:

*(y.deref())

Rust 會將 * 替換爲先調用 deref 方法再進行解引用的操作,這讓我們可以寫出一致的代碼而不用對實現了 Deref trait 的類型進行特殊的處理(手動調用 deref 方法)。

deref 方法返回值的引用,以及 * (y.deref()) 括號外邊的仍然需要解引用的操作和所有權相關。如果 deref 方法直接返回值而不是其引用,所有權將從 self 移動走,而在我們前面的例子裏或是大多數使用解引用操作的場景,我們並不希望獲取所有權。

注意,我們在代碼中使用 * 時, * 替換爲先調用 deref 方法再進行解引用的操作,只會發生一次。因爲對 * 的替換不會無限遞歸(我理解是,如果解引用以後的值所屬類型也實現了 deref 方法,不會再調用其 deref 方法), 在上面的例子中 ,在 i32 類型的值時就終止了,其值就是 5,與 assert_eq!(5, *y); 中的值相匹配。

函數和方法的強制隱式轉換

強制隱式轉換(deref coercions)是 Rust 爲函數或方法傳參提供的一種便利。強制隱式轉換隻作用於實現了 Deref trait 的類型,它會將原類型轉換爲另一種類型的引用。舉例來說,強制隱式轉換可以把 &String 轉換爲 &str,這是因爲 String 類型實現了 Deref trait ,並返回了 str 類型。當我們所傳給函數或方法的參數值的引用類型與其定義所不同時,就會自動進行強制隱式轉換。一系列的 deref 方法會被調用,把我們提供的參數值的類型轉換成其所定義的類型。

強制隱式轉換讓我們可以減少在進行函數和方法調用時顯式的進行引用和解引用。同時也讓我們編寫的代碼可以同時作用於引用和智能指針。參考下面的例子,我們定義了一個參數爲 &str 類型的函數:

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

我們可以使用字符串切片作爲 hello 函數的參數,如 hello("Rust");。但是強制隱式轉換使 MyBox 類型的值也可以做爲 hello 函數的參數,參考下面的例子:

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

上例中,&m 爲 MyBox 類型值的引用。因爲 MyBox 實現了 Deref trait,Rust 會通過調用 deref 方法將 &MyBox 變爲 &String。而標準庫提供的 String 類型也實現了 Deref trait,並會返回字符串切片,因此,Rust 再次調用 deref 方法將 &String 變爲 &str,這樣就與 hello 函數的定義相匹配了。

如果 Rust 沒有強制隱式轉換機制,爲了將 &MyBox 類型的值傳遞給 hello 函數做爲參數,我們需要編寫類似下面的代碼:

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

上面的代碼中,我們首先通過 (*m) 將 MyBox 解引用,獲得 String 類型的值;然後通過 & 和 [..] 操作得到整個 String 類型值的字符串切片,從而與 hello 函數的定義相匹配。這些代碼看上去更加難以閱讀和理解。強制隱式轉換使得 Rust 可以自動的處理這些轉換,同時這些分析和轉換都是發生在編譯時的,因此不會在運行時帶來任何額外的開銷。

強制隱式轉換與可變性

和我們使用 Deref trait 重載不可變引用的 * 操作類似,Rust 提供了 DerefMut trait 用於重載可變引用的 * 操作。

Rust 在類型和 trait 實現滿足以下三種情況時會進行強制隱式轉換:

前兩種情況除了可變性之外是相同的:如果類型 T 實現了返回 U 類型的 Deref trait,則可以直接從 &T 轉換爲 &U(或是 &mut T 轉換爲 &mut U)。

第三個情況有所不同:Rust 會將可變引用轉爲不可變引用,但是反過來是不行的,也就是說不可變引用永遠也不能轉爲可變引用。這是因爲根據借用規則,如果有一個可變引用,其在作用裏必須是唯一的,否則程序是無法編譯通過的。將一個可變引用轉換爲不可變引用不存在打破這個規則的可能,但是將不可變引用轉換爲可變引用則可能打破這個規則。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/NYyCZhOyW-ZbxBTvHRI63A