爱国的张浩予 发表于 2025-02-04 18:37
Tags:asref,asmut,borrow,borrowmut,deref,derefmut,auto-deref,fst,dst,blanket-implementation,macro,conditional-implementation,orphan
深入再谈智能指针、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
关联类型,以强调当前是在智能指针讨论上下文内。
概述
接下来,文章正文将从下图《自定义引用、自定义借用与智能指针的解引用方式分类》开始逐层深入展开论述。
解引用的触发方式不同
首先,对“自定义引用”与“自定义借用”的解引用操作都要求 @Rustacean 必须手动调用对应的特征成员方法(
- 解引用自定义引用
- 只读
<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 AsMut>::as_mut(&mut T) ➜ &mut F
— 来自【标准库】对trait AsMut
的直接实现<&mut T as AsMut>::as_mut(&mut &mut T) ➜ &mut F
— 来自【标准库】对trait AsMut
的泛型覆盖实现
- 只读
- 解引用自定义借用
- [只读]
<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 ➜ &Path
print(&path_buf_2_path); // &PathBuf ➜ &Path
// &&T ➜ &F
print(&&string_2_path); // &&String ➜ &Path
print(&&path_buf_2_path); // &&PathBuf ➜ &Path
// T ➜ &F 在 print() 函数调用之后,消耗掉了变量所有权。
print(string_2_path); // String ➜ &Path
print(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
当作T
接着递归解引用F
。递归处理会持续进行,直至- 要么,解引用后的
&F
已匹配于函数形参的数据类型 - 要么,
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 ➜ &F
let 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
的(类似于?Sized
,表示不确定是否满足trait 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 ➜ &Path
print_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 ➜ &Path
print_fst(&path_buf); // &PathBuf ➜ &Path
// 2. 甚至,引用的引用也能传值。
print_fst(&&string_file_path); // &&String ➜ &Path
print_fst(&&path_buf); // &&PathBuf ➜ &Path
// 最后,再消费掉变量的所有权
print_fst(string_file_path); // String ➜ &Path
print_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 as AsRef>::as_ref(&Box)
)就会遮蔽掉其内部值上的同名成员方法<Deref::Target as AsRef>::as_ref(&Deref::Target)
,进而阻断模拟“自动解引用”的泛型匹配。再举个[例程7]
use ::std::{ convert::AsRef, ops::Deref, path::{ Path, PathBuf } };
//
// 该函数的唯一【形参】兼容于任何“既装箱于智能指针 Box<T> 又是 Path 自定义引用的【实参】”。
//
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_dst(&string_file_path);
// print_dst(&path_buf);
// 2. 即便是直接传智能指针的所有权值,对胖智能指针的拆箱也得一步变两步完成
// (1) 从 Box\<T\> 中拆箱出 自定义引用
// (2) 从 自定义引用 拆箱出 &Path
// (3) 最后,才能调用 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行。
- 否则,编译失败,因为取不出智能指针内部值的复本来。
- 要么,智能指针实例正作为函数、成员方法、甚至闭包调用语句中非对应
self
/&self
/&mut self
形参的引用类型实参。- 场景复现,请参考[例程2]的第15行。
- 要么,智能指针实例正作为该指针关联类型
Deref::Target
成员方法调用语句中对应&self
/&mut self
形参的引用类型实参。- 场景复现,请参考[例程2]的第10与12行。
- 要么,智能指针实例正作为该指针关联类型
Deref::Target
成员方法调用语句中对应self
形参的所有权实参,且Deref::Target
还得满足trait Copy
限定条件。- 场景复现,请参考[例程3]的第36行。
- 否则,编译失败。
智能指针的语义功能
虽然智能指针可作为模仿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
自动解引用机制就能保证:智能指针与其内部值必对任何trait
都会保持完全一致的处理行为。
故事依旧未结束。甚至,一个惊艳接着另一个惊艳。【自定义借用】的约束规则还大幅提升了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,
// 来断言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
作为形参类型。但
- 第10行既能传递引用
&path_buf
作为实参。同时, - 第13行又能传所有权变量
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
编译沙箱的孤儿原则导致“实现标准库的AsRef<F>
与AsMut<F>
特征给任何非当前crate
的数据结构都会被编译器拒绝”。
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
智能指针已实现过
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 $struct
where <$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 $struct
where <$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 $struct
where <$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 $struct
where <$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
泛化“引用”项的最新冥悟。文章不仅长,还着实有点儿生涩。感谢耐心的读者能坚持阅读至文章结束。创作不易,请读者们点个赞呗!
评论区
写评论还没有评论