【社區投稿】深入再談智能指針、AsRef引用與Borrow借用

深入再談智能指針、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>>表示類型TF的自定義引用。普通引用、自定義引用與智能指針之間的對比關系可以被歸納為

輸入圖片說明
輸入圖片說明

自定義借用

代表std::borrow::Borrow<F>std::borrow::BorrowMut<F>特征的實現類。比如,泛型類型參數<T: Borrow<F>>表示類型TF的自定義借用,也可讀作“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是否有可度量的字節大小”。即,類型TDST的。

模仿?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自動完成的。具體地講,編譯器會在語義分析過程中

  1. 先識別出

  • 在成員方法調用語句中,對應&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已不再是智能指針,且不能進一步解引用了

  1. 若是,則繞回第二步將F當作T接著遞歸解引用F。遞歸處理會持續進行,直至

    若遞歸解引用被持續執行多次,那么在原來&T實參位置上會出現一條對T.deref()成員方法調用鏈條,或在原來&mut T實參位置上會出現一條對T.deref_mut()成員方法調用鏈條。

  2. 否則,則直接進行下一步。

最后,判斷&F是否匹配于函數形參的數據類型

  1. 若匹配,則此段編譯成功,并執行后續編譯處理。

  2. 否則,整體編譯失敗

上述文字描述較為抽象,一圖勝千言,請參考下圖。注意:執行圖中操作的行為主體是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>

    此處"自動解引用"的本質就是rustcMIR中間代碼生成前,將智能指針成員方法調用語句中對應self形參的智能指針實參T替換為對T的解引用表達式*<T as Deref>::deref(&T)

下面是對【手動解引用】過程的詳細圖解

輸入圖片說明
輸入圖片說明

此外,使用一元解引用操作符*替換(可變)智能指針內部值的代碼套路可概括為:

  1. 聲明和初始化一個可變智能指針變量。

  2. 在解引用該智能指針變量的同時,對其做賦值處理。這對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;
}

編譯時,觸發解引用的時間窗口不同

輸入圖片說明
輸入圖片說明

由上圖可總結出

  1. 解引用類型轉換“自定義引用”和“自定義借用”的代碼是由人工手動添加于程序文件編寫階段;而

  2. 解引用“智能指針”的處理邏輯則是由(前端)編譯器rustc條件地注入于編譯過程中的“語義分析”之后和“MIR中間代碼生成”之前。

我甚至猜測:“rustc對智能指針追加的解引用表達式不是人類可讀的Rust代碼,而是面向語義分析器的AST子節點樹”。

解引用的技術原理不同

自定義引用

對“自定義引用”的解引用處理是建立在Rust泛型類型系統的“通用底盤”基礎之上。憑借FST靜態分派機制,Rust能模擬出OOP的同一函數形參兼容于不同類型實參的“重載”效果。舉個例子,正是因為【標準庫】預置了類型&strStringstd::path::PathBuftrait 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的“自定義引用”。以下是摘錄于【標準庫】源碼的泛型覆蓋實現塊簽名:

  1. impl<T: ?Sized, F: ?Sized> AsRef<F> for &T where T: AsRef<F> { .. }

    讀作:若類型T是類型F只讀自定義引用,那么T只讀引用&T也同樣是類型F只讀自定義引用。

  2. 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 DerefMuttrait 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>>為例)

  1. 強制【借用T】與【被借用的值F】都對外呈現相同的:

  • 哈希值 --- 意味著處理邏輯一致的trait std::hash::Hash實現

  • 等價關系 --- 意味著處理邏輯一致的trait std::cmp::Eq實現

  • 排序關系 --- 意味著處理邏輯一致的trait std::cmp::Ord實現

督促 @Rustacean 對【借用T】與【被借用的值F】編寫處理邏輯一致的特征實現塊,當需要對它們實現除std::borrow::Borrowstd::borrow::BorrowMut之外的特征時。比如,我們一般預期【借用T】與【被借用的值F】都能被print!宏打印輸出相同的內容,通過給它們編寫處理邏輯一致的trait std::fmt::Display特征實現塊。

換句話說,只要某個類型T實現了trait Borrow<F>trait BorrowMut<F>,那么類型TF

  • 【必有】相同的“哈希值”和“判等+排序”偏好。

  • 【可選但有理由期望】對其它trait的實現,也有處理邏輯一致的特征實現塊。

這是一套非常重要的約束規則。

同時從概念冠名上,

  • TF的自定義借用,和

  • 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自動解引用機制就能保證:

  1. 智能指針不僅能點出其內部值的全部pub成員方法,更會保持與內部值的trait method完全一致的處理行為。但,很可惜,

  2. 當面對泛型類型匹配時,智能指針卻不能“透明”呈現出其內部值的trait“形狀”。還是講,辦法總比問題多。采用ambassador crate,借助 crate 作者預定義的過程宏,便可:

    1. 給智能指針數據結構增補實現其內部值才實現的trait

    2. 將智能指針的trait method實現委托給內部值實現的trait method

      于是,從宏觀效果來看,智能指針與它的內部值既對外呈現相同的泛型trait“形狀”,還保持了一致的trait method處理邏輯,這簡直是完美致極!可是,這一切就已經與rustc自動解引用沒關系了。

故事依舊未結束。甚至,一個驚艷接著另一個驚艷。【自定義借用】的約束規則還大幅提升了MapSet類“可檢索”數據結構的查詢效率。簡單地講,【自定義借用】允許 @Rustacean

  • 既能,將所有權值作為數據保存于MapSet數據結構中,以滿足容器占有子元素的要求。

  • 又可,使用更輕量級的自定義借用(算是廣義引用的一種)作為對鍵數據匹配查詢的搜索條件。

進而避免,為每次檢索操作,都重新構造一個所有權值作為【鍵】的查詢條件 — 內存效率極低。舉個[例程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作為形參類型。但

  1. 第11行既能傳遞引用&path_buf作為實參 — 自定義借用的泛型覆蓋實現。同時,

  2. 第14行又能傳所有權變量path_buf作為實參 — 自定義借用的反身性

因為它們都是原始變量path_buf的【自定義借用】。

智能指針的條件化AsRef特征實現塊

不同于普通引用,智能指針被允許定義它自己的(特征)實現塊和實現它自己的(特征)成員方法。于是,智能指針內部值(Deref::Target)的同名成員方法就會被智能指針自身的成員方法給遮蔽掉和失效,因為rustc在對self/&self/&mut self的實參執行解引用處理前,就檢索到了“目標”成員方法和提前進入函數調用處理流程 — 只匹配名,而不匹配參數列表。這不僅造成程序設計上的難點,更導致【普通引用】與【智能指針】對被引用的【自定義引用】處理邏輯的不一致。以自定義引用<T: AsRef<F>>例,

  • &Tas_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()}
}

這段增補程序塊所完成的任務可概括為:

  1. 若智能指針關聯類型Deref::Target同時不滿足AsRef<F>AsMut<F>特征限定條件,那就什么也不做,也不添加新特征實現塊。否則,繼續。

  2. 若智能指針關聯類型Deref::Target滿足AsRef<F>特征限定條件,那就給智能指針數據結構增補trait AsRef<F>特征實現塊,和實現特征成員方法<SmartPointer as AsRef<F>>::as_ref(&SmartPointer)返回&F

  3. 若智能指針關聯類型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()特征成員方法時,有一點兒麻煩,因為需要分辨兩種情況:

  1. 智能指針數據結構上未定義過面向其它類型的trait AsRef<_>trait AsMut<_>特征實現塊,那么從智能指針實例直接點出新增補的as_ref()as_mut()特征成員方法即可,不會有任何的歧義。

  2. 否則,

  • 要么,采用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泛化“引用”項的最新冥悟。文章不僅長,還著實有點兒生澀。感謝耐心的讀者能堅持閱讀至文章結束。創作不易,請讀者們點個贊唄!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/71993.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/71993.shtml
英文地址,請注明出處:http://en.pswp.cn/web/71993.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

外層元素旋轉,其包括在內的子元素一并旋轉(不改變旋轉中心),單元測試

思路&#xff1a;外層旋轉后坐標&#xff0c;元素旋轉后坐標&#xff0c;計算偏移坐標 <template><div class"outbox"><label>角度: <input v-model.number"rotate" type"number" /></label><br><div c…

如何在虛擬機上安裝hadoop

與前面java的方式相同安裝好hadoop后進入hadoop的環境變量my_env.sh 輸入#?HADOOP_export HADOOP_HOME /opt/module/hadoop-3.1.3 export PATH$PATH:$HADOOP_HOME/bin export PATH$PATH:$HADOOP_HOME/sbin 再輸入hadoop測試是否安裝成功

WPF-DataGrid的增刪查改

背景&#xff1a;該功能為幾乎所有系統開發都需要使用的功能&#xff0c;現提供簡單的案例。 1、MyCommand using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Input;namespace Wp…

Oracle數據庫存儲結構--物理存儲結構

數據庫存儲結構&#xff1a;分為物理存儲結構和邏輯存儲結構。 物理存儲結構&#xff1a;操作系統層面如何組織和管理數據 邏輯存儲結構&#xff1a;Oracle數據庫內部數據組織和管理數據&#xff0c;數據庫管理系統層面如何組織和管理數據 存儲結構 在Oracle數據庫的存儲結構…

歌詞相關實現

歌詞相關 歌詞數據模型&#xff1a; // Lyric.swift class Lyric: BaseModel {/// 是否是精確到字的歌詞var isAccurate:Bool false/// 所有的歌詞var datum:Array<LyricLine>! }// LyricLine.swift class LyricLine: BaseModel {/// 整行歌詞var data:String!/// 開始…

紡織服裝制造行業現狀 內檢實驗室系統在紡織服裝制造行業的應用

在紡織服裝制造行業&#xff0c;內檢實驗室LIMS系統&#xff08;實驗室信息管理系統&#xff09;已成為提升檢測效率、優化質量控制和滿足行業合規性要求的關鍵工具。隨著行業競爭的加劇和消費者對產品質量要求的提高&#xff0c;紡織服裝制造企業需要更加高效、準確的檢測流程…

K8s 1.27.1 實戰系列(十一)ConfigMap

ConfigMap 是 Kubernetes 中管理非敏感配置的核心資源,通過解耦應用與配置實現靈活性和可維護性。 一、ConfigMap 的核心功能及優勢 ?1、配置解耦 將配置文件(如數據庫地址、日志級別)與容器鏡像分離,支持動態更新而無需重建鏡像。 ?2、多形式注入 ?環境變量:將鍵值…

3分鐘復現 Manus 超強開源項目 OpenManus

文章目錄 前言什么是 OpenManus構建方式環境準備克隆代碼倉庫安裝依賴配置 LLM API運行 OpenManus 效果演示總結個人簡介 前言 近期人工智能領域迎來了一位備受矚目的新星——Manus。Manus 能夠獨立執行復雜的現實任務&#xff0c;無需人工干預。由于限制原因大部分人無法體驗…

從零開始學機器學習——構建一個推薦web應用

首先給大家介紹一個很好用的學習地址:https://cloudstudio.net/columns 今天,我們終于將分類器這一章節學習完活了,和回歸一樣,最后一章節用來構建web應用程序,我們會回顧之前所學的知識點,并新增一個web應用用來讓模型和用戶交互。所以今天的主題是美食推薦。 美食推薦…

【最后203篇系列】014 AI機器人-1

說明 終于開張了&#xff0c;我覺得AI機器人是一件真正正確&#xff0c;具有商業價值的事。 把AI機器人當成一筆生意&#xff0c;我如何做好這筆生意&#xff1f;一端是業務價值&#xff0c;另一端是技術支撐。如何構造高質量的內容和服務&#xff0c;如何確保技術的廣度和深度…

【大模型統一集成項目】如何封裝多個大模型 API 調用

&#x1f31f; 在這系列文章中&#xff0c;我們將一起探索如何搭建一個支持大模型集成項目 NexLM 的開發過程&#xff0c;從 架構設計 到 代碼實戰&#xff0c;逐步搭建一個支持 多種大模型&#xff08;GPT-4、DeepSeek 等&#xff09; 的 一站式大模型集成與管理平臺&#xff…

AI4CODE】3 Trae 錘一個貪吃蛇的小游戲

【AI4CODE】目錄 【AI4CODE】1 Trae CN 錐安裝配置與遷移 【AI4CODE】2 Trae 錘一個 To-Do-List 這次還是采用 HTML/CSS/JAVASCRIPT 技術棧 Trae 錘一個貪吃蛇的小游戲。 1 環境準備 創建一個 Snake 的子文件夾&#xff0c;清除以前的會話記錄。 2 開始構建 2.1 輸入會…

【簡答題002】Java變量簡答題

博主會經常補充完善這里面問題的答案。希望可以得到大家的一鍵三連支持&#xff0c;你的鼓勵是我堅持下去的最大動力&#xff01;謝謝&#xff01; 001 什么是Java變量&#xff1f; Java變量是用來存儲數據并在程序中引用的命名空間。 002 Java變量有哪些類型&#xff1f; J…

從零開發Chrome廣告攔截插件:開發、打包到發布全攻略

從零開發Chrome廣告攔截插件&#xff1a;開發、打包到發布全攻略 想打造一個屬于自己的Chrome插件&#xff0c;既能攔截煩人的廣告&#xff0c;又能優雅地發布到Chrome Web Store&#xff1f;別擔心&#xff0c;這篇教程將帶你從零開始&#xff0c;動手開發一個功能強大且美觀…

基于騰訊云高性能HAI-CPU的跨境電商客服助手全鏈路解析

跨境電商的背景以及痛點 根據Statista數據&#xff0c;2025年全球跨境電商市場規模預計達6.57萬億美元&#xff0c;年增長率保持在12.5% 。隨著平臺規則趨嚴&#xff08;如亞馬遜封店潮&#xff09;&#xff0c;更多賣家選擇自建獨立站&#xff0c;2024年獨立站占比已達35%。A…

maven的項目構建

常用構建命令 命令說明mvn clean清理編譯結果&#xff08;刪掉target目錄&#xff09;mvn compile編譯核心代碼&#xff0c;生成target目錄mvn test-compile編譯測試代碼&#xff0c;生成target目錄mvn test執行測試方法mvn package打包&#xff0c;生成jar或war文件mvn insta…

定時任務和分布式任務框架

文章目錄 一 Spring Task1.@Scheduled注解介紹2 基本用法(1)使用@EnableScheduling修飾啟動類(2)創建定時任務的類(3)fixedDelay(4)fixedRate(5)cron3 執行多個任務4 設置異步執行5 @Async使用自定義線程池6 缺點二 xxl-job介紹架構圖與其他任務調度平臺的比較運行調…

git安裝,配置SSH公鑰(查看版本、安裝路徑,更新版本)git常用指令

目錄 一、git下載安裝 1、下載git 2、安裝Git?&#xff1a; 二、配置SSH公鑰 三、查看安裝路徑、查看版本、更新版本 四、git常用指令 1、倉庫初始化與管理 2、配置 3、工作區與暫存區管理 4、提交 5、分支管理 6、遠程倉庫管理 7、版本控制 8、其他高級操作 一…

[Web]ServletContext域(Application)

簡介 Web應用的Application域的實現是通過ServletContext對象實現的。整個Web應用程序的所有資源共享這個域。生命周期與Web應用程序相同&#xff0c;即當前Web應用程序啟動時&#xff08;以服務器視角而非訪客視角&#xff09;出生&#xff0c;Web應用服務程序關閉時停止。 通…

qt c++ 進程和線程

在Qt C開發中&#xff0c;進程&#xff08;Process&#xff09;和線程&#xff08;Thread&#xff09;是兩種不同的并發模型&#xff0c;各有適用場景和實現方式。以下是詳細對比和實際開發中的用法總結&#xff1a; 一、進程&#xff08;Process&#xff09; 進程是操作系統資…