目录

社区投稿深入再谈智能指针AsRef引用与Borrow借用

【社区投稿】深入再谈智能指针、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 的自定义引用。普通引用、自定义引用与智能指针之间的对比关系可以被归纳为

https://i-blog.csdnimg.cn/img_convert/45c9c287c4c9f1c000cdfaa3cb162f69.png
屏幕截图

输入图片说明

自定义借用

代表 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 是否可复制”。注:【可克隆】不等同于【可复制】 — 这是两码事。

概述

接下来,文章正文将从下图《自定义引用、自定义借用与智能指针的解引用方式分类》开始逐层深入展开论述。

https://i-blog.csdnimg.cn/img_convert/4711e6d184933cd333b7d2d3c83fa717.png
屏幕截图

输入图片说明

解引用的触发方式不同

首先,对“自定义引用”与“自定义借用”的解引用操作都要求 @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 ➜ &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 自动完成 的。具体地讲,编译器会在语义分析过程中

  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。

https://i-blog.csdnimg.cn/img_convert/8fb6d6b443a14f6368595afd3112edd5.png
屏幕截图

输入图片说明

再举个[例程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 ,那么 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)

下面是对【手动解引用】过程的详细图解

https://i-blog.csdnimg.cn/img_convert/0e7e5ce4dad7348acd37067357016dcd.png
屏幕截图

输入图片说明

此外,使用一元解引用操作符 * 替换(可变)智能指针内部值的代码套路可概括为:

  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;
}

编译时,触发解引用的时间窗口不同

https://i-blog.csdnimg.cn/img_convert/5df33dd2b8f160682acab9bb0f407f63.png
屏幕截图

输入图片说明

由上图可总结出

  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 ➜ &Path
    print_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 的可变自定义引用。

依旧感觉文字描述苍白无力,我还是接着画张图吧!

https://i-blog.csdnimg.cn/img_convert/915025acd6947b9bc23312cc06a06933.png
屏幕截图

输入图片说明

由此,我们就能将 按所有权传值 的[例程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<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 $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 泛化“引用”项的最新冥悟。文章不仅长,还着实有点儿生涩。感谢耐心的读者能坚持阅读至文章结束。创作不易,请读者们点个赞呗!