關(guān)于rust中trait(二)

在Rust中默認(rèn)定義了一些比較常用的trait,主要是為了滿足不同的場景下使用。但其中一些trait(Deref/AsRef/Borrow/Cow)的概念理解起來有點“晦澀”;
對于初學(xué) Rust 的新手,對這幾個概念會十分迷惑。所以,現(xiàn)在就讓我們一起來探索一下。

一、按模塊分類理解

其實按標(biāo)準(zhǔn)庫的分類,首先就可以略知一二它們的作用。

  1. std::ops::Deref :Deref 是被歸類為 ops 模塊。這個模塊下放的都是可重載的操作符。這些操作符都有對應(yīng)的 trait:比如Add trait 對應(yīng)的就是 +;
    Deref trait 則對應(yīng)共享(不可變)借用 的解引用操作,比如 *v; 相應(yīng)的,也有 DerefMut trait,對應(yīng)獨占(可變)借用的解引用操作。由于 Rust 所有權(quán)語義是貫穿整個語言特性,所以 擁有(Owner)/不可變借用(&T)/可變借用(&mut T)的語義都是配套出現(xiàn)的。
  2. std::convert::AsRef :AsRef 被歸類到 convert 模塊。這個模塊下放的都是擁有類型轉(zhuǎn)換的 trait 。比如熟悉的 From/IntoTryFrom/TryInto ,而 AsRef/AsMut也是作為配對出現(xiàn)在這里,說明該trait 是和類型轉(zhuǎn)化有關(guān)。再根據(jù) Rust API Guidelines 里的命名規(guī)范可以推理,以 as_ 開頭的方法,代表從 borrowed -> borrowed ,即 reference -> reference的一種轉(zhuǎn)換,并且是無開銷的。并且這種轉(zhuǎn)換不能失敗。
  3. std::borrow::Borrow: Borrow 被歸類到 borrow 模塊中。而該模塊的文檔則相對比較簡陋:這是用于使用借來的數(shù)據(jù)。所以該 trait 多多少少和表達借用語義是相關(guān)的。提供了三個 trait : Borrow / BorrowMut/ ToOwned ,可以說是和所有權(quán)語義完全對應(yīng)了。
  4. std::borrow::Cow: Cow 也被歸類為 borrow 模塊中。根據(jù)描述,Cow 是 一種 clone-on-write 的智能指針。被放到 borrow 模塊,主要還是為了盡可能的使用 借用 而避免 拷貝,是一種優(yōu)化。

二、trait詳解

接下來逐個深入了解

std::ops::Deref

1、定義:

pub trait Deref {
    type Target: ?Sized;
    #[must_use]
    pub fn deref(&self) -> &Self::Target;
}

其實定義不復(fù)雜,Deref 只包含一個 deref 方法簽名。該 trait 妙就妙在,它會被編譯器 「隱式」調(diào)用,官方的說法叫 deref. 強轉(zhuǎn)(deref coercion) 。標(biāo)準(zhǔn)庫示例:

use std::ops::Deref;

struct DerefExample<T> {
    value: T,    // Value類型為T(泛型)
}
// 實現(xiàn)Deref特型trait
// 內(nèi)部可操作類型:T;與結(jié)構(gòu)體DerefExample中字段value相同
impl<T> Deref for DerefExample<T> {
    type Target = T;

   // 接收一個DerefExample的引用; 輸出DerefExample的內(nèi)部字段的引用
    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

// 驗證
// x類型:DerefExample<char>
// 此時DerefExample中的T=char
let x = DerefExample { value: 'a' };

// 此時*x解引用會調(diào)用deref方法,進行引用轉(zhuǎn)換輸出field: value的內(nèi)容
// *x等同于*Deref::deref(&x)
assert_eq!('a', *x);

代碼中,DerefExample 結(jié)構(gòu)體實現(xiàn)了 Deref trait,那么它就能被使用 解引用操作符* 來執(zhí)行了。示例中,直接返回字段 value 的值。

可以看得出來:DerefExample 實現(xiàn)了 Deref 而擁有了一種類似于 指針的行為,因為它可以被解引用了。所以為了方便理解這種行為,我們稱之為「指針語義」。DerefExample 也就變成了一種智能指針。這也是識別一個類型是否為智能指針的方法之一,看它是否實現(xiàn) Deref。但并不是所有智能指針都要實現(xiàn) Deref ,也有的是實現(xiàn) Drop ,或同時實現(xiàn)。

現(xiàn)在讓我們來總結(jié) Deref。
如果 T實現(xiàn)了 Deref<Target = U>,并且 x是 類型 T的一個實例,那么:

  1. 在不可變的上下文中,*x (此時 T 既不是引用也不是原始指針)操作等價于 *Deref::deref(&x)。
  2. &T 的值會強制轉(zhuǎn)換為 &U 的值。
  3. 相當(dāng)于 T 實現(xiàn)了 U 的所有(不可變)方法。

Deref 的妙用在于提升了 Rust 的開發(fā)體驗。標(biāo)準(zhǔn)庫里典型的示例就是 Vec<T> 通過實現(xiàn) Deref 而共享了 slice的所有方法。

impl<T, A: Allocator> ops::Deref for Vec<T, A> {
    type Target = [T];

    fn deref(&self) -> &[T] {
        unsafe { slice::from_raw_parts(self.as_ptr(), self.len) } // 此處開銷并不大
    }
}

比如, len() 方法實際上是在 slice 模塊被定義的。但因為 在 Rust 里,當(dāng)執(zhí)行 .調(diào)用,或在函數(shù)參數(shù)位置,都會被編譯器自動執(zhí)行 deref 強轉(zhuǎn)這種隱式行為,所以,就相當(dāng)于 Vec<T> 也擁有了 slice的方法。
如下樣例:

fn main() {
 let a = vec![1, 2, 3];
 assert_eq!(a.len(), 3); // 當(dāng) a 調(diào)用 len() 的時候,發(fā)生 deref 強轉(zhuǎn)
}

在Rust 中的隱式行為并不多見,但是 Deref 這種隱式強轉(zhuǎn)的行為,為我們方便使用智能指針提供了便利。

fn main() {
    let h = Box::new("hello");
    assert_eq!(h.to_uppercase(), "HELLO");
}

比如我們操作 Box<T>,我們就不需要手動解引用取出里面T來操作,而是當(dāng) Box<T> 外面這一層是透明的,直接來操作 T 就可以了。

再比如:

fn uppercase(s: &str) -> String {
    s.to_uppercase()
}

fn main() {
    let s = String::from("hello");
    assert_eq!(uppercase(&s), "HELLO");
}

上面 uppercase 方法的參數(shù)類型 明明是 &str,但現(xiàn)在main函數(shù)中實際傳的類型是 &String,為什么編譯可以成功呢?就是因為 String 實現(xiàn)了 Deref :

impl ops::Deref for String {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}

這就是 Deref 的妙用。但是有些人可能會“恍然大悟”,這不就是繼承嗎?大誤。
這種行為好像有點像繼承,但請不要隨便用 Deref 來模擬繼承。

std::convert::AsRef

來看一下 AsRef 的定義:

pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

我們已經(jīng)知道 AsRef 可以用于轉(zhuǎn)換。相比較于擁有隱式行為的 Deref ,AsRef 屬于顯式的轉(zhuǎn)換

fn is_hello<T: AsRef<str>>(s: T) {
   assert_eq!("hello", s.as_ref());
}

fn main() {
    let s = "hello";
    is_hello(s);

    let s = "hello".to_string();
    is_hello(s);
}

上面示例中,is_hello 的函數(shù)是泛型函數(shù)。通過 T: AsRef<str>的限定,并且在函數(shù)內(nèi)使用 s.as_ref()這樣的顯式調(diào)用來達到轉(zhuǎn)換的效果。不管是 String 還是 str其實都實現(xiàn)了 AsRef trait。

那現(xiàn)在問題來了,什么時候使用 AsRef 呢?為啥不直接用 &T ?

舉一個示例:

// 定義struct
pub struct Thing {
    name: String,
}
// 實現(xiàn)
impl Thing {
    pub fn new(name: WhatTypeHere) -> Self {
        Thing { name: name.some_conversion() }
}

上面示例中,new函數(shù) name的類型參數(shù)有以下幾種情況選擇:

  1. &str。此時, 調(diào)用方(caller)需要傳入一個引用。但是為了轉(zhuǎn)換為 String ,則被調(diào)方(callee)則需要自己控制內(nèi)存分配,并且會有拷貝。
  2. String。此時,調(diào)用方傳 String 還好,如果是傳引用,則和情況 1 相似。
  3. T: Into<String>。此時,調(diào)用方可以傳 &strString,但是在類型轉(zhuǎn)換的時候同樣會有內(nèi)存分配和拷貝的情況。
  4. T: AsRef<str>。同 情況 3 。
  5. T: Into<Cow<'a, str>>,此時,可以避免一些分配。后面會介紹 Cow。

到底何時使用哪種類型,這個其實沒有一個標(biāo)準(zhǔn)答案。有的人就是喜歡 &str ,不管在什么地方都會使用它。這里面其實是需要權(quán)衡的:

  1. 有些分配和拷貝是無關(guān)緊要的,所以就沒有必要讓類型簽名過度復(fù)雜化,直接使用 &str就可以了。
  2. 有些是需要看方法定義,是否需要消耗所有權(quán),或者返回所有權(quán)還是借用。
  3. 有些則是需要盡量減少分配和拷貝,那就必須使用比較復(fù)雜的類型簽名,比如情況5。

通過顯式調(diào)用 .as_ref(),就可以得到父類結(jié)構(gòu)的引用。
Deref 注重隱式透明地使用 父類結(jié)構(gòu),而 AsRef 則注重顯式地獲取父類結(jié)構(gòu)的引用。這是結(jié)合具體的 API 設(shè)計所作的權(quán)衡,而不是無腦模擬 OOP 繼承。

std::borrow::Borrow

來看一下 Borrow 的定義:

pub trait Borrow<Borrowed: ?Sized> {
    fn borrow(&self) -> &Borrowed;
}

對比一下 AsRef:

pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

是不是非常相似?所以,有人提出,這倆 trait 完全可以去掉一個。但實際上,Borrow 和 AsRef 是有區(qū)別的,它們都有存在的意義。

Borrow trait是用來表示 借用數(shù)據(jù)。而 AsRef 則是用來表示類型轉(zhuǎn)換。在Rust中,為不同的語義不同的使用情況提供不同的類型表示是很常見的。

一個類型通過實現(xiàn) Borrow,在 borrow()方法中提供對 T 的引用/借用,表達的語義是可以作為某個類型 T被借用,而非轉(zhuǎn)換。一個類型可以自由地借用為幾個不同的類型,也可以用可變的方式借用。

所以 Borrow 和 AsRef 如何選呢?

  • 當(dāng)你想把不同類型的借用進行統(tǒng)一抽象,或者當(dāng)你要建立一個數(shù)據(jù)結(jié)構(gòu),以同等方式處理自擁有值(ownered)和借用值(borrowed)時,例如散列(hash)和比較(compare)時,選擇Borrow。
  • 當(dāng)你想把某個類型直接轉(zhuǎn)換為引用,并且你正在編寫通用代碼時,選擇AsRef。比較簡單的情況。

其實在標(biāo)準(zhǔn)庫文檔中給出的 HashMap 示例已經(jīng)說明的很好了:
HashMap<K, V> 存儲鍵值對,對于 API 來說,無論使用 Key 的自有值,還是其引用,應(yīng)該都可以正常地在 HashMap 中檢索到對應(yīng)的值。因為 HashMap 要對 key 進行 hash計算 和 比較,所以必須要求 不管是 Key 的自有值,還是引用,在進行 hash計算和比較的時候,行為應(yīng)該是一致的。

use std::borrow::Borrow;
use std::hash::Hash;

pub struct HashMap<K, V> {
    // fields omitted
}

impl<K, V> HashMap<K, V> {
    // insert 方法使用 Key 的自有值,擁有所有權(quán)
    pub fn insert(&self, key: K, value: V) -> Option<V>
    where K: Hash + Eq
    {
        // ...
    }

    // 使用 get 方法通過 key 來獲取對應(yīng)的值,則可以使用 key的引用,這里用 &Q 表示
    // 并且要求 Q 要滿足 `Q: Hash + Eq + ?Sized `
    // 而 K 呢 ,通過 `K: Borrow<Q>` 來表達 K 是 Q 的一個借用數(shù)據(jù)。
    // 所以,這里要求 Q 的 hash 實現(xiàn) 和 K 是一樣的,否則編譯就會出錯
    pub fn get<Q>(&self, k: &Q) -> Option<&V>
    where
        K: Borrow<Q>,
        Q: Hash + Eq + ?Sized
    {
        // ...
    }
}

代碼的注釋基本已經(jīng)說明了問題。Borrow 是對借用數(shù)據(jù)的一種限制,并且配合額外的trait來使用,比如示例中的 HashEq 等。

再看一個示例:

// 這個結(jié)構(gòu)體能不能作為 HashMap 的 key?
pub struct CaseInsensitiveString(String);

// 它實現(xiàn) Eq 沒有問題
impl  PartialEq for CaseInsensitiveString {
    fn eq(&self, other: &Self) -> bool {
       // 但這里比較是要求忽略了 ascii 大小寫
        self.0.eq_ignore_ascii_case(&other.0)
    }
}

impl Eq for CaseInsensitiveString { }

// 實現(xiàn) Hash 沒有問題
// 但因為 eq 忽略大小寫,那么 hash 計算也必須忽略大小寫
impl Hash for CaseInsensitiveString {
    fn hash<H: Hasher>(&self, state: &mut H) {
        for c in self.0.as_bytes() {
            c.to_ascii_lowercase().hash(state)
        }
    }
}

但是 CaseInsensitiveString 可以實現(xiàn) Borrow<str>嗎?

很顯然,CaseInsensitiveString 和 str 對 Hash 的實現(xiàn)不同,str 是不會忽略大小寫的。因此,CaseInsensitiveString 不能實現(xiàn) Borrow<str>,所以 CaseInsensitiveString 不能作為 HashMap 的 key,但編譯器無法通過 Borrow trait 來識別這種情況。

但是 CaseInsensitiveString 完全可以實現(xiàn) AsRef 。

這就是 Borrow 和 AsRef 的區(qū)別,Borrow 更加嚴(yán)格一些,并且表示的語義和 AsRef 完全不同。

std::borrow::Cow

看一下 Cow 的定義:

pub enum Cow<'a, B> 
where  B: 'a + ToOwned + ?Sized, 
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

看得出來, Cow 是一個枚舉。有點類似于 Option,表示兩種情況中的某一種。Cow 在這里就是表示 借用的 和 自有的,但只能出現(xiàn)其中的一種情況。

Cow 主要功能:

  1. 作為智能指針,提供對此類型實例的透明的不可變訪問(比如可直接調(diào)用此類型原有的不可變方法,實現(xiàn)了Deref ,但沒實現(xiàn) DerefMut);
  2. 如果遇到需要修改此類型實例,或者需要獲得此類型實例的所有權(quán)的情況,Cow 提供方法做克?。–lone)處理,并避免多次重復(fù)克隆。

Cow 的設(shè)計目的是提高性能(減少復(fù)制)同時增加靈活性,因為大部分情況下,業(yè)務(wù)場景都是讀多寫少。利用 Cow,可以用統(tǒng)一,規(guī)范的形式實現(xiàn),需要寫的時候才做一次對象復(fù)制。這樣就可能會大大減少復(fù)制的次數(shù)。

它有以下幾個要點需要掌握:

  1. Cow<T> 能直接調(diào)用 T 的不可變方法,因為 Cow 這個枚舉,實現(xiàn)了 Deref;
  2. 在需要修改T的時候,可以使用.to_mut()方法得到一個具有所有權(quán)的值的可變借用;
      1. 注意,調(diào)用 .to_mut() 不一定會產(chǎn)生Clone;
      1. 在已經(jīng)具有所有權(quán)的情況下,調(diào)用 .to_mut() 有效,但是不會產(chǎn)生新的Clone;
      1. 多次調(diào)用 .to_mut() 只會產(chǎn)生一次Clone。
  3. 在需要修改T的時候,可以使用.into_owned()創(chuàng)建新的擁有所有權(quán)的對象,這個過程往往意味著內(nèi)存拷貝并創(chuàng)建新對象;
      1. 如果之前 Cow 中的值是借用狀態(tài),調(diào)用此操作將執(zhí)行Clone;
      1. 本方法,參數(shù)是self類型,它會“消費”原先的那個類型實例,調(diào)用之后原先的類型實例的生命周期就截止了,在 Cow 上不能調(diào)用多次;

Cow 在 API 設(shè)計上用的比較多:

use std::borrow::Cow;

// 返回值使用 Cow ,避免多次拷貝
fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> {
    if input.contains(' ') {
        let mut buf = String::with_capacity(input.len());
        for c in input.chars() {
            if c != ' ' {
                buf.push(c);
            }
        }
        return Cow::Owned(buf);
    }
    return Cow::Borrowed(input);
}

當(dāng)然,什么時候使用 Cow ,又回到了我們前文中那個「什么時候使用 AsRef 」的討論,一切都要權(quán)衡,并沒有放之四海皆準(zhǔn)的標(biāo)準(zhǔn)答案。

三、引用

關(guān)于Deref
deref. 強轉(zhuǎn)(deref coercion)
關(guān)于AsRef
關(guān)于Borrow
關(guān)于Cow

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容