深入再談智能指針、AsRef
引用與Borrow
借用
這是一個具有深度的技術主題。每次重溫其理論知識,都會有新的領悟。大約 2 年前,我曾就這一技術方向撰寫過另一篇短文《從類型轉換視角,淺談Deref<Target = T>
,?AsRef<T>
,?Borrow<T>
和From<T> trait
差異》。在那篇文章中,我依據當時的經驗知識,歸納了自定義智能指針、引用和借用的代碼實現方式,并羅列了一些原則。但那種“快餐式”的知識分享存在不少遺憾。兩年后的今天,我憑借更多的知識積累,利用春節長假的時間,重拾此話題,力求通過一篇長文將(廣義的)Rust
“引用”闡述清楚。
名詞解釋
為了統一對非具名抽象概念的理解,我們先對幾個Rust
常見項的中文表述進行約定。
@Rustacean
一般發音為/?r?st??si??n/
。它是由單詞Rust
和詞根-acean
(表示“……的人”的后綴)構成。它被用作Rust
技術棧程序員的自稱。
自定義引用
代表std::convert::AsRef<F>
或std::convert::AsMut<F>
的特征實現類。例如,泛型類型參數<T: AsRef<F>>
表示類型T
是F
的自定義引用。普通引用、自定義引用與智能指針之間的對比關系可以被歸納為

自定義借用
代表std::borrow::Borrow<F>
或std::borrow::BorrowMut<F>
特征的實現類。比如,泛型類型參數<T: Borrow<F>>
表示類型T
是F
的自定義借用,也可讀作“T
被借入作為F
”。
胖引用
代表動態分派特征對象trait Object
的普通引用。例如,&dyn AsRef<std::path::Path>
。
胖智能指針
代表動態分派特征對象trait Object
的智能指針。例如,Box<dyn AsRef<std::path::Path>>
。
胖指針
是對胖引用與胖智能指針的統稱。
泛型覆蓋實現
英文全稱是Blanket Implementation
,它是作用于泛型類型參數的特征實現塊。簡單來說,泛型覆蓋實現允許 @Rustacean 為一批滿足特定泛型條件的類型統一實現某個特征,而無需為每個具體類型單獨實現,這樣可以減少代碼重復。例如,impl<T: ?Sized> Borrow<T> for T { /* 特征成員方法的實現 */ }
。該定義的關鍵在于特征實現塊的“受體”不是一個具體類型,而是滿足泛型參數限定條件的一批具體類型。
特征成員方法
對應英文詞條是trait method
,指在特征定義內聲明的成員方法以及在特征實現塊內實現的成員方法。
智能指針內部值的數據類型
以<T: Deref<Target = F>>
為例,在文章中,
要么,將內部值數據類型記作
F
,出于文字簡潔的目的。要么,將其記作
<T as Deref>::Target
關聯類型,以強調當前是在智能指針討論上下文內。
<T: ?Copy>
特征限定條件
首先,這個“泛型(類型)參數限定條件”記法從語法規則上肯定是錯的,因為即便最新版rustc
也僅只認識?Sized
一個“不確定”限定條件。
其次,<T: ?Sized>
代表“直至編譯時,還不能確定被限定的泛型(類型)參數T
是否有可度量的字節大小”。即,類型T
是DST
的。
模仿?Sized
,我想用<T: ?Copy>
表示“直至編譯時,還不能確定被限定的泛型(類型)參數T
是否可復制”。注:【可克隆】不等同于【可復制】 — 這是兩碼事。
概述
接下來,文章正文將從下圖《自定義引用、自定義借用與智能指針的解引用方式分類》開始逐層深入展開論述。

解引用的觸發方式不同
首先,對“自定義引用”與“自定義借用”的解引用操作都要求 @Rustacean 必須手動調用對應的特征成員方法(
解引用自定義引用
<T as AsMut>::as_mut(&mut T) ? &mut F
?— 來自【標準庫】對trait AsMut
的直接實現<&mut T as AsMut>::as_mut(&mut &mut T) ? &mut F
?— 來自【標準庫】對trait AsMut
的泛型覆蓋實現
<T as AsRef>::as_ref(&T) ? &F
?— 來自【標準庫】對trait AsRef
的直接實現<&T as AsRef>::as_ref(&&T) ? &F
?— 來自【標準庫】對trait AsRef
的泛型覆蓋實現<&mut T as AsRef>::as_ref(&&mut T) ? &F
?— 來自【標準庫】對trait AsRef
的泛型覆蓋實現
只讀
可變
解引用自定義借用
[只讀]
<T as Borrow>::borrow(&T) ? &F
[可變]
<T as BorrowMut>::borrow_mut(&mut T) ? &mut F
)才能完成從T
/&T
/&mut T
至&F
/&mut F
的(純手動)解引用類型轉換。舉個[例程 1]
use?::std::path::{ Path, PathBuf };
fn?print<K:?AsRef<Path>>(file_path: K) {// 注意:由于編譯器不會自動生成對 file_path.as_ref() 成員方法的調用語句,// ? ? ?因此開發者必須顯式地手動調用 <K as AsRef>::as_ref(&K)。let?file_path: &Path = file_path.as_ref();println!("文件路徑= {}", file_path.display());
}
fn?main() {let?string_2_path =?String::from("/etc/<String>");let?path_buf_2_path = PathBuf::from("/etc/<PathBuf>");// &T ? &F 在 print() 函數調用之后,保留變量所有權不被消費掉。print(&string_2_path); ??// &String ? &Pathprint(&path_buf_2_path);?// &PathBuf ? &Path// &&T ? &Fprint(&&string_2_path);?// &&String ? &Pathprint(&&path_buf_2_path);?// &&PathBuf ? &Path// T ? &F 在 print() 函數調用之后,消耗掉了變量所有權。print(string_2_path);?// String ? &Pathprint(path_buf_2_path);?// PathBuf ? &Path
}
前面我們介紹了解引用自定義引用和自定義借用的方式,接下來看看對“智能指針”引用的解引用處理有什么不同。
對“智能指針”引用的解引用處理(&T ? &F
或&mut T ? &mut F
)則是由(前端)編譯器rustc
自動完成的。具體地講,編譯器會在語義分析過程中
先識別出
在成員方法調用語句中,對應
&self
或&mut self
形參的智能指針引用實參&T
或&mut T
,以及在函數、閉包、成員方法調用語句中,對應其它普通引用形參的智能指針引用實參
&T
或&mut T
再將&T
或&mut T
原地替換為對它們的解引用特征成員方法調用表達式
<T as Deref>::deref(&T) ? &F
或<T as DerefMut>::deref_mut(&mut T) ? &mut F
接著,判斷上一步返回值中的F
是否又是智能指針?
要么,解引用后的
&F
已匹配于函數形參的數據類型要么,
F
已不再是智能指針,且不能進一步解引用了
若是,則繞回第二步將
F
當作T
接著遞歸解引用F
。遞歸處理會持續進行,直至若遞歸解引用被持續執行多次,那么在原來
&T
實參位置上會出現一條對T
的.deref()
成員方法調用鏈條,或在原來&mut T
實參位置上會出現一條對T
的.deref_mut()
成員方法調用鏈條。否則,則直接進行下一步。
最后,判斷&F
是否匹配于函數形參的數據類型
若匹配,則此段編譯成功,并執行后續編譯處理。
否則,整體編譯失敗。
上述文字描述較為抽象,一圖勝千言,請參考下圖。注意:執行圖中操作的行為主體是rustc
,而不是 @Rustacean。

再舉個[例程2],佐證上述理論總結。
use?::std::path::{ Path, PathBuf };
fn?print(file_path: &Path) {println!("文件路徑= {}", file_path.display());
}
fn?main() {let?mut?path_buf = PathBuf::from("/etc/<&PathBuf>");// 場景一:在成員方法調用中,對 &self 與 &mut self 的自動解引用// (1) &mut T ? &mut F 修改智能指針內部值的內部狀態信息path_buf.push("usr");// (2) &T ? &F 讀取智能指針內部值的內部狀態信息println!("文件路徑= {}", path_buf.display());// 場景二:在普通函數調用中,對智能指針引用的自動解引用// 模擬了 OOP 編程中的函數重載。print(&path_buf);?// &T ? &Flet?path: &Path = path_buf.as_path();print(path);// 不存在 T ? &F,所以下面會編譯失敗// print(PathBuf::from("/etc/<PathBuf>"));
}
最后,對“智能指針”所有權變量的解引用(T ? F
)就得具體問題具體分析了。@Rustacean 需分三步漸進推導:
第一步:確認如何處理智能指針的解引用結果F
?
是要【替換】智能指針的內部值。例如,賦值語句
*t = new_value
?— 千萬別忘了以let mut
聲明智能指針變量t
。還是【拾出】內部數據的所有權值。例如,
let value = *t
。
第二步:若是后者(拾出所有權值),再接著判斷智能指針內部值的類型F
是否滿足trait Copy
限定條件?
若
where F: Copy
成立,那么被拾取出的所有權值其實是F
值的復本。若
F
是?Copy
,那么rustc
就會判定本次編譯操作整體失敗。知識點回顧,Rust
變量的所有權規則禁止從外層數據結構搬移出它內部字段的所有權值,因為禁止“掏空”數據結構留空坑位null
。
第三步,確認拾出智能指針內部值F
的手段方式
【手動解引用】使用一元解引用操作符
*
拾出所有權值。例如,let f_copy = *t;
。【自動解引用】調用
F
數據結構上“消費”self
所有權的成員方法。例如,PathBuf::into_boxed_path(self) ? Box<Path>
。“
此處"自動解引用"的本質就是
rustc
在MIR
中間代碼生成前,將智能指針成員方法調用語句中對應self
形參的智能指針實參T
替換為對T
的解引用表達式*<T as Deref>::deref(&T)
。
下面是對【手動解引用】過程的詳細圖解

此外,使用一元解引用操作符*
替換(可變)智能指針內部值的代碼套路可概括為:
聲明和初始化一個可變智能指針變量。
在解引用該智能指針變量的同時,對其做賦值處理。這對
Cpp
語法的一比一復刻真讓人看著就親切!
用偽碼表示大約是如下的樣子:
// 1. 假設結構體 SmartPointer實現了 Deref<Target = PathBuf> 特征。
let?mut?path_buf_sp = SmartPointer::new(PathBuf::from("/etc/abc"));
// 2. 該[解引用+賦值]語句總是能編譯成功,無論 <SmartPointer as Deref>::Target 是否滿足 trait Copy 限定條件
*path_buf_sp = PathBuf::from("/etc/abc1");
將上述“解引用智能指針所有權變量(T ? F
)”的知識點都捏合至[例程3],可向讀者呈現:
// 注釋內容同樣很精彩和更重要。
mod?generic_deref {use?::std::ops::{ Deref, DerefMut };pub?struct?GenericDeref<T> {value: T}impl<T> GenericDeref<T> {pub?fn?new(value: T) ->?Self?{GenericDeref { value }}}impl<T> Deref?for?GenericDeref<T> {type?Target?= T;fn?deref(&self) -> &Self::Target {&self.value}}impl<T> DerefMut?for?GenericDeref<T> {fn?deref_mut(&mut?self) -> &mut?Self::Target {&mut?self.value}}
}
use?::std::path::PathBuf;
use?generic_deref::GenericDeref;type?Deref1?= GenericDeref<PathBuf>;
type?Deref2?= GenericDeref<i32>;fn?main() {let?mut?i32_wrapper = Deref2::new(1);// 用法1:替換智能指針的內部字段值。這總是能夠被成功地完成*i32_wrapper = -1;// 用法2:拾取出智能指針內部字段的所有權值。// 因為 <T as Deref2>::Target 是滿足 trait Copy 限定條件的 i32 類型,// 所以如下解引用操作才能成功通過編譯:// (1) 調用"消耗所有權的"成員方法,和自動解引用。assert_eq!(0, i32_wrapper.leading_zeros());// (2) 使用解引用操作符 * 手動解引用。解引用結果是智能指針內部值的復本。assert_eq!(-1, *i32_wrapper);let?mut?path_buf_wrapper = Deref1::new(PathBuf::from("/etc/abc"));// 用法1:替換智能指針內部值。這總是能夠被成功地完成*path_buf_wrapper = PathBuf::from("/etc/abc1");// 用法2:拾取出智能指針內部字段的所有權值。// 因為 <T as Deref2>::Target 未滿足 trait Copy 限定條件,// 所以如下解引用操作未能通過編譯:// (1) 調用消耗所有權的成員方法,自動解引用。// let path_buf = path_buf_wrapper.into_boxed_path();// (2) 使用解引用操作符,手動解引用。// let path_buf = *path_buf_wrapper;
}
編譯時,觸發解引用的時間窗口不同

由上圖可總結出
解引用類型轉換“自定義引用”和“自定義借用”的代碼是由人工手動添加于程序文件編寫階段;而
解引用“智能指針”的處理邏輯則是由(前端)編譯器
rustc
有條件地注入于編譯過程中的“語義分析”之后和“MIR
中間代碼生成”之前。
我甚至猜測:“rustc
對智能指針追加的解引用表達式不是人類可讀的Rust
代碼,而是面向語義分析器的AST
子節點樹”。
解引用的技術原理不同
自定義引用
對“自定義引用”的解引用處理是建立在Rust
泛型類型系統的“通用底盤”基礎之上。憑借FST
的靜態分派機制,Rust
能模擬出OOP
的同一函數形參兼容于不同類型實參的“重載”效果。舉個例子,正是因為【標準庫】預置了類型&str
、String
與std::path::PathBuf
對trait AsRef<std::path::Path>
的特征實現和定義這些類型可作為std::path::Path
的自定義引用,所以[例程4]中模仿多態的函數調用才有能通過編譯檢查
use?::std::{ convert::AsRef, path::{ Path, PathBuf } };
//
// 因為該函數的唯一【形參】兼容于任何“可解引用為 &Path 的”自定義引用【實參】。
//
fn?print_fst<T:?AsRef<Path>>(file_path: T) {let?file_path: &Path = file_path.as_ref();?// 手動解引用,而不是自動解引用println!("文件路徑fst= {}", file_path.display());
}
fn?main() {let?str_file_path =?"/etc/<str>";let?string_file_path =?"/etc/<string>".to_string();let?path_buf = PathBuf::from("/etc/<PathBuf>");//// 所以,形似 OOP 函數重載的多態調用語句才有機會出現在 Rust 程序內。//print_fst(str_file_path); ? ?// &str ? &Path// 美中不足,這都是按所有僅傳值。print_fst(string_file_path);?// String ? &Pathprint_fst(path_buf); ? ? ? ??// PathBuf ? &Path// 因消費掉了變量所有權,所以 string_file_path 與 path_buf 都將不可再被訪問
}
又因為“一味地按所有權傳值”是件非常“內耗的”程序設計選擇,所以故事并沒有止步于此。【標準庫】還為“自定義引用”的引用(甚至,引用的引用遞歸)提供了泛型覆蓋實現,以使自定義引用的引用們(比如,&T
、&&T
、&&mut T
、&mut T
、&mut &mut T
)繼續是初始被引用值F
的“自定義引用”。以下是摘錄于【標準庫】源碼的泛型覆蓋實現塊簽名:
impl<T: ?Sized, F: ?Sized> AsRef<F> for &T where T: AsRef<F> { .. }
讀作:若類型
T
是類型F
的只讀自定義引用,那么T
的只讀引用&T
也同樣是類型F
的只讀自定義引用。impl<T: ?Sized, F: ?Sized> AsMut<F> for &mut T where T: AsMut<F> { .. }
讀作:若類型
T
是類型F
的可變自定義引用,那么T
的可變引用&mut T
也同樣是類型F
的可變自定義引用。
依舊感覺文字描述蒼白無力,我還是接著畫張圖吧!

由此,我們就能將按所有權傳值的[例程4]升級改造為僅按引用傳值的[例程5]。
use?::std::{ convert::AsRef, path::{ Path, PathBuf } };
//
// 因為該函數的唯一【形參】兼容于任何“可解引用為 &Path 的”自定義引用【實參】。
//
fn?print_fst<T:?AsRef<Path>>(file_path: T) {let?file_path: &Path = file_path.as_ref();?// 手動解引用,而不是自動解引用println!("文件路徑fst= {}", file_path.display());
}
fn?main() {let?string_file_path =?"/etc/<string>".to_string();let?path_buf = PathBuf::from("/etc/<PathBuf>");//// 因為【標準庫】預置了對"自定義引用"的引用的泛型覆蓋實現,所以//// 1. AsRef<F> 實現類也就具備了部分“自動解引用”能力,和能夠按引用傳值。print_fst(&string_file_path);?// &String ? &Pathprint_fst(&path_buf); ? ? ? ??// &PathBuf ? &Path// 2. 甚至,引用的引用也能傳值。print_fst(&&string_file_path);?// &&String ? &Pathprint_fst(&&path_buf); ? ? ? ??// &&PathBuf ? &Path// 最后,再消費掉變量的所有權print_fst(string_file_path);?// String ? &Pathprint_fst(path_buf); ? ? ? ? ?// PathBuf ? &Path
}
閱讀至此,擅長發散思維的讀者一定已經開始掂量如何將泛型覆蓋實現的效用推廣至自定義引用的
DST
的動態分派胖指針,以及智能指針
,而不僅限于普通引用。棒!這個思路是十分正確的,而且它對胖引用(比如,&dyn AsRef<F>
)也確實有效。舉個[例程6]
use?::std::{ convert::AsRef, ops::Deref, path::{ Path, PathBuf } };
//
// 該函數的唯一【形參】兼容于任何“可解引用為 &Path 的”自定義引用【實參】。
//
// 靜態分派形參
fn?print_fst<T:?AsRef<Path>>(file_path: T) {let?file_path: &Path = file_path.as_ref();?// 手動解引用,而不是自動解引用println!("[靜態分派]文件路徑fst= {}", file_path.display());
}
// 動態分派形參
fn?print_dst(file_path: &dyn?AsRef<Path>) {let?file_path: &Path = file_path.as_ref();?// 手動解引用,而不是自動解引用println!("[動態分派][普通引用]文件路徑fst= {}", file_path.display());
}
fn?main() {let?string_file_path =?"/etc/<string>".to_string();let?path_buf = PathBuf::from("/etc/<PathBuf>");//// 因為【標準庫】預置了對"自定義引用"的引用的泛型覆蓋實現,所以//// 1. 對動態分派的函數形參,其實參也能兼容。print_dst(&string_file_path);print_dst(&path_buf);// 2. 對靜態分派的函數形參,其實參依舊能“自動解引用”。print_fst(&string_file_path);print_fst(&path_buf);
}
但這對智能指針就不一定成立了,無論它是動態分派的特征對象(例如,Box<dyn AsRef<F>>
),還是靜態分派的泛型(例如,Box<T> where T: AsRef<F>
)。至少由【標準庫】直供的智能指針數據結構都定義了自己專屬的trait AsRef<F>
,trait AsMut<F>
,trait Borrow<F>
與trait BorrowMut<F>
實現塊和為它們定義了獨立解釋的語義功能。于是,智能指針實例自身的特征成員方法(比如,<Box<T> as AsRef>::as_ref(&Box<T>)
)就會遮蔽掉其內部值上的同名成員方法<Deref::Target as AsRef>::as_ref(&Deref::Target)
,進而阻斷模擬“自動解引用”的泛型匹配。再舉個[例程7]
use?::std::{ convert::AsRef, ops::Deref, path::{ Path, PathBuf } };
//
// 該函數的唯一【形參】兼容于任何“裝箱于Box<T>智能指針且可解引用為 &Path 自定義引用的”實參。
//
// 靜態分派形參
#[allow(dead_code)]
fn?print_fst<T:?AsRef<Path>>(file_path: T) {let?file_path: &Path = file_path.as_ref();?// 手動解引用,而不是自動解引用println!("[靜態分派]文件路徑fst= {}", file_path.display());
}
// 動態分派形參
fn?print_dst(file_path:?Box<dyn?AsRef<Path>>) {let?file_path: &Path = file_path.deref().as_ref();?// 手動解引用,而不是自動解引用println!("[動態分派][智能指針]文件路徑fst= {}", file_path.display());
}
fn?main() {//// 定義“裝箱于Box<T>”智能指針的 &Path 自定義引用。//let?string_file_path =?Box::new("/etc/<string>".to_string());let?path_buf =?Box::new(PathBuf::from("/etc/<PathBuf>"));// 1. 沒有自動解引用,因為`<Box as AsRef>::as_ref(&Box)`的返回值是// ? ?&String 與 &PathBuf,而不是 &Path。所以,下面六條語句都會編譯失敗// print_fst(&string_file_path);// print_fst(&path_buf);// print_dst(&string_file_path);// print_dst(&path_buf);// print_fst(string_file_path);// print_fst(path_buf);// 2. 即便是直接傳智能指針的所有權值,對胖智能指針的拆箱也得一步變兩步完成// ? ?(1) 從 Box<T> 中拆箱出 自定義引用// ? ?(1) 從 自定義引用 ?拆箱出 &Path// ? ?最后,才能調用 Path 類型上的成員方法print_dst(string_file_path);print_dst(path_buf);
}
別慌張,辦法總比問題多!就 @Rustacean?本地定義的智能指針而言,我在文章正文最后一節分享了一段解決此痛點的《智能指針【條件化特征實現塊】補丁》,并對其從工作原理至可復用宏定義都做了講解。
智能指針
在*.rs
程序文件編譯過程中,(前端)編譯器rustc
會在AST
語義分析后、MIR
生成前,對滿足特定條件的智能指針實例“定點”注入解引用特征成員方法的調用表達式。
判斷某個實例是否是智能指針?
根據數據結構是否實現過trait Deref
特征,辨認智能指針實例。因為trait DerefMut
是trait Deref
的子特征,所以“粗線條地”識別智能指針,就不用專門對trait DerefMut
做限定條件檢查。
“定點”注入解引用表達式的代碼位置篩選條件
要么,智能指針實例正作為一元解引用操作符
*
的操作數,且該指針關聯類型Deref::Target
滿足trait Copy
限定條件。即,智能指針內部值是可復制的。場景復現,請參考[例程3]的第31與42行。
若
<Deref::Target: ?Copy>
,則編譯失敗,因為取不出智能指針內部值的復本來。
要么,智能指針實例正作為函數、成員方法、甚至閉包調用語句中非對應
self
/&self
/&mut self
形參的引用類型實參。場景復現,請參考[例程2]的第15行。
要么,智能指針實例正作為該指針關聯類型
Deref::Target
成員方法調用語句中對應&self
/&mut self
形參的引用類型實參。場景復現,請參考[例程2]的第10與12行。
要么,智能指針實例正作為該指針關聯類型
Deref::Target
成員方法調用語句中對應self
形參的所有權實參,且Deref::Target
還得滿足trait Copy
限定條件。場景復現,請參考[例程3]的第36行。
若
<Deref::Target: ?Copy>
,則編譯失敗。
智能指針的語義功能
雖然智能指針可作為模仿OOP
編程風格(比如,繼承)的反模式語法糖,但它的首要任務卻是從如下兩個維度(同時或之一地)增強其Deref::Target
類型內部值的語義功能:
所有權關系 ownership。例如,
Rc<T>
被當作其內部值T
的“引用”,和按所有權傳值,并擺脫普通引用規則的諸多限制。內存存儲位置 storage。還是以
Rc<T>
為例,它騰挪內部值T
從【棧】內存至【堆】內存。然后,構造多個指向相同【堆】數據的【棧】(所有權)“引用”變量。
自定義借用
對“自定義借用”的解引用處理也是建立在Rust
泛型類型系統的“通用底盤”基礎之上的。但它的首要用途已不再是模仿函數重載多態性的語法糖,而是(以<T: Borrow<F>>
為例)
強制【借用
T
】與【被借用的值F
】都對外呈現相同的:
哈希值 --- 意味著處理邏輯一致的
trait std::hash::Hash
實現等價關系 --- 意味著處理邏輯一致的
trait std::cmp::Eq
實現排序關系 --- 意味著處理邏輯一致的
trait std::cmp::Ord
實現
督促 @Rustacean 對【借用T
】與【被借用的值F
】編寫處理邏輯一致的特征實現塊,當需要對它們實現除std::borrow::Borrow
與std::borrow::BorrowMut
之外的特征時。比如,我們一般預期【借用T
】與【被借用的值F
】都能被print!
宏打印輸出相同的內容,通過給它們編寫處理邏輯一致的trait std::fmt::Display
特征實現塊。
換句話說,只要某個類型T
實現了trait Borrow<F>
或trait BorrowMut<F>
,那么類型T
與F
【必有】相同的“哈希值”和“判等+排序”偏好。
【可選但有理由期望】對其它
trait
的實現,也有處理邏輯一致的特征實現塊。
這是一套非常重要的約束規則。
同時從概念冠名上,
T
是F
的自定義借用,和T
被借用作為F
現實意義
令我恍然大悟的是,普通引用&T
/&mut T
與被引用值T
之間處理行為的高度一致性也是源于這套【自定義借用】約束規則,因為【標準庫】為任何普通引用都預置了如下對trait Borrow<F>
與trait BorrowMut<F>
的泛型覆蓋實現:
impl<T: ?Sized> Borrow<T> for &T { .. }
讀作:任何類型
T
的只讀引用?&T
同時也T
自身的只讀自定義借用。impl<T: ?Sized> BorrowMut<T> for &mut T { .. }
讀作:任何類型
T
的可變引用&mut T
同時也T
自身的可變自定義借用。
于是才有我憑經驗知識與死記硬背才掌握的經驗法則:
“比較兩個值的引用是否相等”就等效于“比較該引用背后的所有權值是否相等”,而不是匹配這兩個引用是否指向同一處內存地址。即,assert!(&1 == &1)
等效于assert!(1 == 1)
,而不是assert!(std::ptr::eq(&1, &1))
。
舉個[例程8]更形象。
use?::std::{ path::PathBuf, ptr };
fn?main() {let?path1 = PathBuf::from("/a/b/c");let?path2 = PathBuf::from("/a/b/c");// 根據自定義借用的限定規則,比較引用就相當于比較被引用的值,assert!(&path1 == &path2);?// 斷言成功assert!(path1 == path2);?// 斷言成功// 而不是匹配引用的內存地址是否是指向的同一處。assert!(ptr::eq(&path1, &path2));?// 斷言失敗
}
到這,發散思維的讀者必定又要發問:“這套約束規則對【智能指針】有啥影響呀?”。我快速回答:“沒影響,因為【標準庫】未提供面向Deref(Mut)
限定條件的Borrow(Mut)
泛型覆蓋實現”。另外,只要Deref(Mut)
實現類不定義與其關聯類型Deref::Target
重名的成員方法,那么rustc
自動解引用機制就能保證:
智能指針不僅能點出其內部值的全部
pub
成員方法,更會保持與內部值的trait method
完全一致的處理行為。但,很可惜,當面對泛型類型匹配時,智能指針卻不能“透明”呈現出其內部值的
trait
“形狀”。還是講,辦法總比問題多。采用ambassador crate,借助 crate 作者預定義的過程宏,便可:給智能指針數據結構增補實現其內部值才實現的
trait
。將智能指針的
trait method
實現委托給內部值實現的trait method
。于是,從宏觀效果來看,智能指針與它的內部值既對外呈現相同的泛型
trait
“形狀”,還保持了一致的trait method
處理邏輯,這簡直是完美致極!可是,這一切就已經與rustc
自動解引用沒關系了。
故事依舊未結束。甚至,一個驚艷接著另一個驚艷。【自定義借用】的約束規則還大幅提升了Map
和Set
類“可檢索”數據結構的查詢效率。簡單地講,【自定義借用】允許 @Rustacean
既能,將所有權值作為鍵數據保存于
Map
和Set
數據結構中,以滿足容器占有子元素的要求。又可,使用更輕量級的自定義借用(算是廣義引用的一種)作為對鍵數據匹配查詢的搜索條件。
進而避免,為每次檢索操作,都重新構造一個所有權值作為【鍵】的查詢條件 — 內存效率極低。舉個[例程10],讓讀者更形象地體會一下
use?::std::collections::HashMap;
fn?main() {let?mut?map: HashMap<String,?i32> = HashMap::new();// 向 Map 內保存字符串的所有權作為【鍵】let?key123 =?String::from("123");let?key124 =?String::from("124");map.insert(key123,?123);map.insert(key124,?124);// 在這一步涉及了 trait Borrow<F> 的兩個知識點:// 1. 因為 String 是 &str 的自定義借用,所以 String 與 &str// ? ?有相同的等價偏好與 hash 值。于是,由 String 為內容保存// ? ?的鍵,就能由 &str 為檢索條件給匹配出來。// 2. 因為 &i32 就是 i32 的自定義借用,&i32 與 i32 就具備相// ? ?同的等價偏好,所以就允許由 &i32 引用之間的判等來斷言其// ? ?背后 i32 值是否相等。assert_eq!(map.get("123"),?Some(&123));
}
寫到這里,我有感而發:“哪有什么天生的易用體質,只是有【標準庫】替我們負重前行”。
反身性Reflexivity
相比于自定義引用,自定義借用還具備“反身性Reflexivity”,因為【標準庫】為任何類型都預置了如下對trait Borrow<F>
與trait BorrowMut<F>
的泛型覆蓋實現:
impl<T: ?Sized> Borrow<T> for T { .. }
讀作:任何類型
T
就是它自身的只讀自定義借用。impl<T: ?Sized> BorrowMut<T> for T { .. }
讀作:任何類型
T
就是它自身的可變自定義借用。
為了證明反身性的存在,我再舉個[例程9]佐證一下
use?::std::{ borrow::Borrow, path::PathBuf };
// 注意下面函數形參的類型不是引用 &PathBuf 喲,而是像所有權值的類型!
fn?print_fst<T: Borrow<PathBuf>>(file_path: T) {let?file_path: &PathBuf = file_path.borrow();?// 手動解引用,而不是自動解引用println!("文件路徑fst= {}", file_path.display());
}
fn?main() {let?path_buf = PathBuf::from("/etc/<PathBuf>");// 1. 任何類型 T 的普通引用 &T 同時也是該類型 T 自身的自定義借用。// ? ?所以,即便函數的形參不是引用,我們也能將引用作為它的實參。這是兼容的不違和!print_fst(&path_buf);// 2. trait Borrow<F> 支持【反身性】。即,// ? ?任何類型 T 就是它自身的"自定義借用"print_fst(path_buf);
}
在上段代碼中,第3行的函數簽名未以引用&PathBuf
為形參類型,而是將所有權的泛型類型T
作為形參類型。但
第11行既能傳遞引用
&path_buf
作為實參 — 自定義借用的泛型覆蓋實現。同時,第14行又能傳所有權變量
path_buf
作為實參 — 自定義借用的反身性
因為它們都是原始變量path_buf
的【自定義借用】。
智能指針的條件化AsRef
特征實現塊
不同于普通引用,智能指針被允許定義它自己的(特征)實現塊和實現它自己的(特征)成員方法。于是,智能指針內部值(Deref::Target
)的同名成員方法就會被智能指針自身的成員方法給遮蔽掉和失效,因為rustc
在對self
/&self
/&mut self
的實參執行解引用處理前,就檢索到了“目標”成員方法和提前進入函數調用處理流程 — 只匹配名,而不匹配參數列表。這不僅造成程序設計上的難點,更導致【普通引用】與【智能指針】對被引用的【自定義引用】處理邏輯的不一致。以自定義引用<T: AsRef<F>>
例,
&T
的as_ref()
特征成員方法返回&F
,而Box<T>
的as_ref()
特征成員方法卻返回&T
它們雖同為T
的“引用”但同名成員方法卻返回不同類型的解引用值。對此,標準庫的技術選擇是放任此“不和諧的”存在。但,我忍不了。我要把【智能指針】對【自定義引用】內部值的處理邏輯“掰直”對齊于【普通引用】。具體做法也不難,
第一步,給每個自定義本地智能指針(數據結構),都增補如下一段對trait AsRef<F>
與trait AsMut<F>
的【條件化特征實現塊】
欲了解更多“條件化實現塊”的精彩內容,請請移步至我的另一篇主題文章《在 Rust 中令人印象深刻的三大【編譯時】條件處理》
這里突出強調“本地智能指針”是因為
Rust
編譯沙箱的孤兒原則導致“給任何非當前crate
的數據結構實現標準庫的AsRef<F>
或AsMut<F>
特征都會被編譯器拒絕”。
type?SmartPointer?=?/* 前文代碼定義的"智能指針"結構體類名 */;
impl<F>?AsRef<F>?for?SmartPointer
where?<SmartPointer?as?Deref>::Target:?AsRef<F> {fn?as_ref(&self) -> &F {self.deref().as_ref()}
}
impl<F>?AsMut<F>?for?SmartPointer
where?<SmartPointer?as?Deref>::Target:?AsMut<F> {fn?as_mut(&mut?self) -> &mut?F {self.deref_mut().as_mut()}
}
這段增補程序塊所完成的任務可概括為:
若智能指針關聯類型
Deref::Target
同時不滿足AsRef<F>
和AsMut<F>
特征限定條件,那就什么也不做,也不添加新特征實現塊。否則,繼續。若智能指針關聯類型
Deref::Target
滿足AsRef<F>
特征限定條件,那就給智能指針數據結構增補trait AsRef<F>
特征實現塊,和實現特征成員方法<SmartPointer as AsRef<F>>::as_ref(&SmartPointer)
返回&F
。若智能指針關聯類型
Deref::Target
滿足AsMut<F>
特征限定條件,那就給智能指針數據結構增補trait AsMut<F>
特征實現塊,和實現特征成員方法<SmartPointer as AsMut<F>>::as_mut(&mut SmartPointer)
返回&mut F
。
此外,即便上例中的SmartPointer
智能指針已實現過面向關聯類型<SmartPointer as Deref>::Target
的自定義引用特征
trait AsRef<<SmartPointer as Deref>::Target>
trait AsMut<<SmartPointer as Deref>::Target>
之一(或全部)也不會與此處新增補的trait AsRef<F>
與trait AsMut<F>
特征實現塊沖突,因為它們的泛型類型參數不一樣。
第二步,在調用智能指針實例上的as_ref()
與as_mut()
特征成員方法時,有一點兒麻煩,因為需要分辨兩種情況:
智能指針數據結構上未定義過面向其它類型的
trait AsRef<_>
和trait AsMut<_>
特征實現塊,那么從智能指針實例直接點出新增補的as_ref()
或as_mut()
特征成員方法即可,不會有任何的歧義。否則,
要么,采用
TurboFish
語法,從成員方法調用表達式,標注泛型參數值和關聯目標特征實現塊。例如,println!("{}", <SmartPointer as AsMut<i32>>::as_mut(&mut smartPointer));
。要么,定義獨立的賦值語句,從賦值變量的類型聲明,標注泛型參數值和關聯目標特征實現塊。例如,
let mut value: i32 = smartPointer.as_mut();
。
可復用的宏定義
因為上面那段增補代碼幾乎伴隨著我本地定義的每個智能指針類,所以它還被特意零成本抽象為如下一段宏定義,以方便復用。因為這個功能太小,所以我還未將其發包于crates.io
倉庫。
// 宏定義
macro_rules!?smart_pointer_patch_builder {[@ref?($struct:?ty)] => {impl<F> std::convert::AsRef<F>?for?$structwhere?<$struct?as?std::ops::Deref>::Target:?AsRef<F> {fn?as_ref(&self) -> &F {use?::std::ops::Deref;self.deref().as_ref()}}impl<F> std::convert::AsMut<F>?for?$structwhere?<$struct?as?std::ops::Deref>::Target:?AsMut<F> {fn?as_mut(&mut?self) -> &mut?F {use?::std::ops::DerefMut;self.deref_mut().as_mut()}}};
}
// 宏調用樣例
type?SmartPointer?=?/* 前文代碼定義的"智能指針"結構體類名 */;
// 裝配條件化的 AsRef 與 AsMut 特征實現塊
smart_pointer_patch_builder!{ @ref?(SmartPointer) }
為了向 @Rustacean 推銷我做的這個宏,我還特地做了一套用法展示[例程11]
macro_rules!?smart_pointer_patch_builder {[@ref?($struct:?ty)] => {impl<F> std::convert::AsRef<F>?for?$structwhere?<$struct?as?std::ops::Deref>::Target:?AsRef<F>,{fn?as_ref(&self) -> &F {use?::std::ops::Deref;self.deref().as_ref()}}impl<F> std::convert::AsMut<F>?for?$structwhere?<$struct?as?std::ops::Deref>::Target:?AsMut<F>,{fn?as_mut(&mut?self) -> &mut?F {use?::std::ops::DerefMut;self.deref_mut().as_mut()}}};
}
// 定義實現了`trait AsRef<F>`與`trait AsMut<F>`特征的智能指針內部值
mod?wrapping {use?::std::convert::{?AsRef,?AsMut?};#[derive(Debug)]pub?struct?Wrapping(i32);impl?AsRef<i32>?for?Wrapping {fn?as_ref(&self) -> &i32?{&self.0}}impl?AsMut<i32>?for?Wrapping {fn?as_mut(&mut?self) -> &mut?i32?{&mut?self.0}}impl?Wrapping {pub?fn?new(value:?i32) ->?Self?{Wrapping(value)}}
}
// 定義本地智能指針類型
mod?smart_pointer {use?::std::{ convert::{?AsRef,?AsMut?}, ops::{ Deref, DerefMut } };use?super::wrapping::Wrapping;#[derive(Debug)]pub?struct?SmartPointer?{value: Wrapping}impl?Deref?for?SmartPointer {type?Target?= Wrapping;fn?deref(&self) -> &Self::Target {&self.value}}impl?DerefMut?for?SmartPointer {fn?deref_mut(&mut?self) -> &mut?Self::Target {&mut?self.value}}impl?SmartPointer {pub?fn?new(value:?i32) ->?Self?{SmartPointer { value: Wrapping::new(value) }}}// 下面是【標準庫】對【智能指針】與`AsRef<F>`/`AsMut<F>`特征的慣例處理impl?AsRef<<SmartPointer?as?Deref>::Target>?for?SmartPointer {fn?as_ref(&self) -> &<SmartPointer?as?Deref>::Target {&self.value}}impl?AsMut<<SmartPointer?as?Deref>::Target>?for?SmartPointer {fn?as_mut(&mut?self) -> &mut?<SmartPointer?as?Deref>::Target {&mut?self.value}}
}
use?::std::{ convert::AsRef, ops::Deref };
use?smart_pointer::SmartPointer;
// 為本地智能指針數據結構增補`trait AsRef<F>`與`trait AsMut<F>`特征實現塊。
smart_pointer_patch_builder!{ @ref?(SmartPointer) }fn?main() {let?sp = SmartPointer::new(12);println!("sp = {:?}", sp);println!("sp.deref() = {:?}", sp.deref());let?ref_as: &i32?= sp.as_ref();println!("sp.as_ref() = {:?}", ref_as);
}
結束語
作為從春節前半個月我就開始著手籌備的傾心大作,這篇長文算是比較全面地匯總了我在生產實踐與理論知識沉淀過程中對&T
普通引用,<T: AsRef<F>>
自定義引用,<T: Borrow<F>>
自定義借用和<T: Deref<Target = F>>
智能指針四個Rust
泛化“引用”項的最新冥悟。文章不僅長,還著實有點兒生澀。感謝耐心的讀者能堅持閱讀至文章結束。創作不易,請讀者們點個贊唄!