在Rust中默認(rèn)定義了一些比較常用的trait,主要是為了滿足不同的場景下使用。但其中一些trait(Deref/AsRef/Borrow/Cow)的概念理解起來有點“晦澀”;
對于初學(xué) Rust 的新手,對這幾個概念會十分迷惑。所以,現(xiàn)在就讓我們一起來探索一下。
一、按模塊分類理解
其實按標(biāo)準(zhǔn)庫的分類,首先就可以略知一二它們的作用。
-
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)的。 -
std::convert::AsRef :AsRef 被歸類到 convert 模塊。這個模塊下放的都是擁有類型轉(zhuǎn)換的 trait 。比如熟悉的
From/Into、TryFrom/TryInto,而AsRef/AsMut也是作為配對出現(xiàn)在這里,說明該trait 是和類型轉(zhuǎn)化有關(guān)。再根據(jù) Rust API Guidelines 里的命名規(guī)范可以推理,以as_開頭的方法,代表從borrowed -> borrowed,即reference -> reference的一種轉(zhuǎn)換,并且是無開銷的。并且這種轉(zhuǎn)換不能失敗。 - std::borrow::Borrow: Borrow 被歸類到 borrow 模塊中。而該模塊的文檔則相對比較簡陋:這是用于使用借來的數(shù)據(jù)。所以該 trait 多多少少和表達借用語義是相關(guān)的。提供了三個 trait : Borrow / BorrowMut/ ToOwned ,可以說是和所有權(quán)語義完全對應(yīng)了。
- 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的一個實例,那么:
- 在不可變的上下文中,
*x(此時T既不是引用也不是原始指針)操作等價于*Deref::deref(&x)。 -
&T的值會強制轉(zhuǎn)換為&U的值。 - 相當(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ù)有以下幾種情況選擇:
-
&str。此時, 調(diào)用方(caller)需要傳入一個引用。但是為了轉(zhuǎn)換為 String ,則被調(diào)方(callee)則需要自己控制內(nèi)存分配,并且會有拷貝。 -
String。此時,調(diào)用方傳 String 還好,如果是傳引用,則和情況 1 相似。 -
T: Into<String>。此時,調(diào)用方可以傳&str和String,但是在類型轉(zhuǎn)換的時候同樣會有內(nèi)存分配和拷貝的情況。 -
T: AsRef<str>。同 情況 3 。 -
T: Into<Cow<'a, str>>,此時,可以避免一些分配。后面會介紹Cow。
到底何時使用哪種類型,這個其實沒有一個標(biāo)準(zhǔn)答案。有的人就是喜歡 &str ,不管在什么地方都會使用它。這里面其實是需要權(quán)衡的:
- 有些分配和拷貝是無關(guān)緊要的,所以就沒有必要讓類型簽名過度復(fù)雜化,直接使用
&str就可以了。 - 有些是需要看方法定義,是否需要消耗所有權(quán),或者返回所有權(quán)還是借用。
- 有些則是需要盡量減少分配和拷貝,那就必須使用比較復(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來使用,比如示例中的 Hash 和 Eq 等。
再看一個示例:
// 這個結(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 主要功能:
- 作為智能指針,提供對此類型實例的透明的不可變訪問(比如可直接調(diào)用此類型原有的不可變方法,實現(xiàn)了Deref ,但沒實現(xiàn) DerefMut);
- 如果遇到需要修改此類型實例,或者需要獲得此類型實例的所有權(quán)的情況,
Cow提供方法做克?。–lone)處理,并避免多次重復(fù)克隆。
Cow 的設(shè)計目的是提高性能(減少復(fù)制)同時增加靈活性,因為大部分情況下,業(yè)務(wù)場景都是讀多寫少。利用 Cow,可以用統(tǒng)一,規(guī)范的形式實現(xiàn),需要寫的時候才做一次對象復(fù)制。這樣就可能會大大減少復(fù)制的次數(shù)。
它有以下幾個要點需要掌握:
-
Cow<T>能直接調(diào)用T的不可變方法,因為Cow這個枚舉,實現(xiàn)了Deref; - 在需要修改
T的時候,可以使用.to_mut()方法得到一個具有所有權(quán)的值的可變借用;- 注意,調(diào)用
.to_mut()不一定會產(chǎn)生Clone;
- 注意,調(diào)用
- 在已經(jīng)具有所有權(quán)的情況下,調(diào)用
.to_mut()有效,但是不會產(chǎn)生新的Clone;
- 在已經(jīng)具有所有權(quán)的情況下,調(diào)用
- 多次調(diào)用
.to_mut()只會產(chǎn)生一次Clone。
- 多次調(diào)用
- 在需要修改
T的時候,可以使用.into_owned()創(chuàng)建新的擁有所有權(quán)的對象,這個過程往往意味著內(nèi)存拷貝并創(chuàng)建新對象;- 如果之前
Cow中的值是借用狀態(tài),調(diào)用此操作將執(zhí)行Clone;
- 如果之前
- 本方法,參數(shù)是
self類型,它會“消費”原先的那個類型實例,調(diào)用之后原先的類型實例的生命周期就截止了,在Cow上不能調(diào)用多次;
- 本方法,參數(shù)是
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