通过 Deref
trait 将智能指针当作常规引用处理
实现 Deref
trait 允许我们重载 解引用运算符(dereference operator)*
(与乘法运算符或通配符相区别)。通过这种方式实现 Deref
trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。
让我们首先看看解引用运算符如何处理常规引用,接着尝试定义我们自己的类似 Box<T>
的类型并看看为何解引用运算符不能像引用一样工作。我们会探索如何实现 Deref
trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 解引用强制转换(deref coercions)功能以及它是如何处理引用或智能指针的。
我们将要构建的
MyBox<T>
类型与真正的Box<T>
有一个很大的区别:我们的版本不会在堆上储存数据。这个例子重点关注Deref
,所以其数据实际存放在何处,相比其类似指针的行为来说不算重要。
通过解引用运算符追踪指针的值
常规引用是一种指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。在示例 15-6 中,创建了一个 i32
值的引用,接着使用解引用运算符解出所引用的值:
文件名: src/main.rs
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
变量 x
存放了一个 i32
值 5
。y
等于 x
的一个引用。可以断言 x
等于 5
。然而,如果希望对 y
的值做出断言,必须使用 *y
来解出引用所指向的值(也就是 解引用)。一旦解引用了 y
,就可以访问 y
所指向的整型值并可以与 5
做比较。
相反如果尝试编写 assert_eq!(5, y);
,则会得到如下编译错误:
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for
`{integer}`
不允许比较数字的引用与数字,因为它们是不同的类型。必须使用解引用运算符解出引用所指向的值。
像引用一样使用 Box<T>
可以使用 Box<T>
代替引用来重写示例 15-6 中的代码,解引用运算符也一样能工作,如示例 15-7 所示:
文件名: src/main.rs
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
示例 15-7 相比示例 15-6 唯一不同的地方就是将 y
设置为一个指向 x
值的 box 实例,而不是指向 x
值的引用。在最后的断言中,可以使用解引用运算符以 y
为引用时相同的方式追踪 box 的指针。接下来让我们通过实现自己的 box 类型来探索 Box<T>
能这么做有何特殊之处。
自定义智能指针
为了体会默认情况下智能指针与引用的不同,让我们创建一个类似于标准库提供的 Box<T>
类型的智能指针。接着学习如何增加使用解引用运算符的功能。
从根本上说,Box<T>
被定义为包含一个元素的元组结构体,所以示例 15-8 以相同的方式定义了 MyBox<T>
类型。我们还定义了 new
函数来对应定义于 Box<T>
的 new
函数:
文件名: src/main.rs
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
这里定义了一个结构体 MyBox
并声明了一个泛型参数 T
,因为我们希望其可以存放任何类型的值。MyBox
是一个包含 T
类型元素的元组结构体。MyBox::new
函数获取一个 T
类型的参数并返回一个存放传入值的 MyBox
实例。
尝试将示例 15-7 中的代码加入示例 15-8 中并修改 main
使用我们定义的 MyBox<T>
类型代替 Box<T>
。示例 15-9 中的代码不能编译,因为 Rust 不知道如何解引用 MyBox
:
文件名: src/main.rs
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
得到的编译错误是:
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
MyBox<T>
类型不能解引用,因为我们尚未在该类型实现这个功能。为了启用 *
运算符的解引用功能,需要实现 Deref
trait。
通过实现 Deref
trait 将某类型像引用一样处理
如第 10 章所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。Deref
trait,由标准库提供,要求实现名为 deref
的方法,其借用 self
并返回一个内部数据的引用。示例 15-10 包含定义于 MyBox
之上的 Deref
实现:
文件名: src/main.rs
use std::ops::Deref;
# struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
type Target = T;
语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多的担心它;第 19 章会详细介绍。
deref
方法体中写入了 &self.0
,这样 deref
返回了我希望通过 *
运算符访问的值的引用。示例 15-9 中的 main
函数中对 MyBox<T>
值的 *
调用现在可以编译并能通过断言了!
没有 Deref
trait 的话,编译器只会把 &
引用类型解引用。deref
方法向编译器提供了一种能力:能够获取任何实现了 Deref
trait 的类型的值,并且可以通过调用这个类型的 deref
方法来获取一个解引用方法已知的 &
引用。
当我们在示例 15-9 中输入 *y
时,Rust 事实上在底层运行了如下代码:
*(y.deref())
Rust 将 *
运算符替换为先调用 deref
方法再进行普通解引用的操作,如此我们便不用担心是否还需手动调用 deref
方法了。Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 Deref
的类型。
deref
方法返回了一个值的引用,而 *(y.deref())
括号外边的普通解引用仍然必须存在的原因是因为所有权。如果 deref
方法直接返回值而不是值的引用,其值(的所有权)将被移出 self
。在这里以及大部分使用解引用运算符的情况下,我们并不希望获取 MyBox<T>
内部值的所有权。
注意,每次当我们在代码中使用 *
时, *
运算符都被替换成了先调用 deref
方法再接着使用 *
解引用的操作,且只会发生一次,不会对 *
操作符无限递归替换,解引用出上面 i32
类型的值就停止了,这个值与示例 15-9 中 assert_eq!
的 5
相匹配。
函数和方法的隐式解引用强制转换
解引用强制转换(deref coercions)是 Rust 在函数或方法传参上的一种便利。解引用强制转换只能工作在实现了 Deref trait 的类型上。解引用强制转换将一种类型(A)隐式转换为另外一种类型(B)的引用,因为 A 类型实现了 Deref trait,并且其关联类型是 B 类型。比如,解引用强制转换可以将 &String 转换为 &str,因为类型 String 实现了 Deref trait 并且其关联类型是 str。代码如下:
#[stable(feature = "rust1", since = "1.0.0")]
impl ops::Deref for String {
type Target = str;
#[inline]
fn deref(&self) -> &str {
unsafe { str::from_utf8_unchecked(&self.vec) }
}
}
当我们将特定类型的值的引用作为参数传递给函数或方法,但是被传递的值的引用与函数或方法中定义的参数类型不匹配时,会发生解引用强制转换。这时会有一系列的 deref
方法被调用,把我们提供的参数类型转换成函数或方法需要的参数类型。
解引用强制转换的加入使得 Rust 开发者编写函数和方法调用时无需增加过多显式使用 &
和 *
的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。
作为展示解引用强制转换的实例,让我们使用示例 15-8 中定义的 MyBox<T>
,以及示例 15-10 中增加的 Deref
实现。示例 15-11 展示了一个有着字符串 slice 参数的函数定义:
文件名: src/main.rs
fn hello(name: &str) {
println!("Hello, {}!", name);
}
可以使用字符串 slice 作为参数调用 hello
函数,比如 hello("Rust");
。解引用强制转换使得用 MyBox<String>
类型值的引用调用 hello
成为可能,如示例 15-12 所示:
文件名: src/main.rs
# use std::ops::Deref;
#
# struct MyBox<T>(T);
#
# impl<T> MyBox<T> {
# fn new(x: T) -> MyBox<T> {
# MyBox(x)
# }
# }
#
# impl<T> Deref for MyBox<T> {
# type Target = T;
#
# fn deref(&self) -> &T {
# &self.0
# }
# }
#
# fn hello(name: &str) {
# println!("Hello, {}!", name);
# }
#
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
这里使用 &m
调用 hello
函数,其为 MyBox<String>
值的引用。因为示例 15-10 中在 MyBox<T>
上实现了 Deref
trait,Rust 可以通过 deref
调用将 &MyBox<String>
变为 &String
。标准库中提供了 String
上的 Deref
实现,其会返回字符串 slice,这可以在 Deref
的 API 文档中看到。Rust 再次调用 deref
将 &String
变为 &str
,这就符合 hello
函数的定义了。
如果 Rust 没有实现解引用强制转换,为了使用 &MyBox<String>
类型的值调用 hello
,则不得不编写示例 15-13 中的代码来代替示例 15-12:
文件名: src/main.rs
# use std::ops::Deref;
#
# struct MyBox<T>(T);
#
# impl<T> MyBox<T> {
# fn new(x: T) -> MyBox<T> {
# MyBox(x)
# }
# }
#
# impl<T> Deref for MyBox<T> {
# type Target = T;
#
# fn deref(&self) -> &T {
# &self.0
# }
# }
#
# fn hello(name: &str) {
# println!("Hello, {}!", name);
# }
#
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m)
将 MyBox<String>
解引用为 String
。接着 &
和 [..]
获取了整个 String
的字符串 slice 来匹配 hello
的签名。没有解引用强制转换所有这些符号混在一起将更难以读写和理解。解引用强制转换使得 Rust 自动的帮我们处理这些转换。
当所涉及到的类型定义了 Deref
trait,Rust 会分析这些类型并使用任意多次 Deref::deref
调用以获得匹配参数的类型。这些解析都发生在编译时,所以利用解引用强制转换并没有运行时损耗!
解引用强制转换如何与可变性交互
类似于使用 Deref
trait 重载不可变引用的 *
运算符,Rust 提供了 DerefMut
trait 用于重载可变引用的 *
运算符。
Rust 在发现类型和 trait 的实现满足以下三种情况时会进行解引用强制转换:
- 当
T: Deref<Target=U>
:从&T
到&U
。 - 当
T: DerefMut<Target=U>
:从&mut T
到&mut U
。 - 当
T: Deref<Target=U>
:从&mut T
到&U
。
前两种情况除了可变性之外是相同的:第一种情况表明如果有一个 &T
,而 T
实现了返回 U
类型的 Deref
,则可以直接得到 &U
。第二种情况表明对于可变引用也有着相同的行为。
第三种情况有些微妙:Rust 也会将可变引用强转为不可变引用,但是反之是 不可能 的,因为不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。
推荐几款学习编程的免费平台
免费在线开发平台(https://docs.ltpp.vip/LTPP/)
探索编程世界的新天地,为学生和开发者精心打造的编程平台,现已盛大开启!这个平台汇集了近4000道精心设计的编程题目,覆盖了C、C++、JavaScript、TypeScript、Go、Rust、PHP、Java、Ruby、Python3以及C#等众多编程语言,为您的编程学习之旅提供了一个全面而丰富的实践环境。
在这里,您不仅可以查看自己的代码记录,还能轻松地在云端保存和运行代码,让编程变得更加便捷。平台还提供了私聊和群聊功能,让您可以与同行们无障碍交流,分享文件,共同进步。不仅如此,您还可以通过阅读文章、参与问答板块和在线商店,进一步拓展您的知识边界。
为了提升您的编程技能,平台还设有每日一题、精选题单以及激动人心的编程竞赛,这些都是备考编程考试的绝佳资源。更令人兴奋的是,您还可以自定义系统UI,选择视频或图片作为背景,打造一个完全个性化的编码环境,让您的编程之旅既有趣又充满挑战。
免费公益服务器(https://docs.ltpp.vip/LTPP-SHARE/linux.html)
作为开发者或学生,您是否经常因为搭建和维护编程环境而感到头疼?现在,您不必再为此烦恼,因为一款全新的免费公共服务器已经为您解决了所有问题。这款服务器内置了多种编程语言的编程环境,并且配备了功能强大的在线版VS Code,让您可以随时随地在线编写代码,无需进行任何复杂的配置。
随时随地,云端编码
无论您身在何处,只要有网络连接,就可以通过浏览器访问这款公共服务器,开始您的编程之旅。这种云端编码的便利性,让您的学习或开发工作不再受限于特定的设备或环境。
丰富的编程语言支持
服务器支持包括C、C++、JavaScript、TypeScript、Go、Rust、PHP、Java、Ruby、Python3以及C#等在内的多种主流编程语言,满足不同开发者和学生的需求。无论您是初学者还是资深开发者,都能找到适合自己的编程环境。
在线版VS Code,高效开发
内置的在线版VS Code提供了与本地VS Code相似的编辑体验,包括代码高亮、智能提示、代码调试等功能,让您即使在云端也能享受到高效的开发体验。
数据隐私和安全提醒
虽然服务器是免费的,但为了保护您的数据隐私和安全,我们建议您不要上传任何敏感或重要的数据。这款服务器更适合用于学习和实验,而非存储重要信息。
免费公益MYSQL(https://docs.ltpp.vip/LTPP-SHARE/mysql.html)
作为一名开发者或学生,数据库环境的搭建和维护往往是一个复杂且耗时的过程。但不用担心,现在有一款免费的MySQL服务器,专为解决您的烦恼而设计,让数据库的使用变得简单而高效。
性能卓越,满足需求
虽然它是免费的,但性能绝不打折。服务器提供了稳定且高效的数据库服务,能够满足大多数开发和学习场景的需求。
在线phpMyAdmin,管理更便捷
内置的在线phpMyAdmin管理面板,提供了一个直观且功能强大的用户界面,让您可以轻松地查看、编辑和管理数据库。
数据隐私提醒,安全第一
正如您所知,这是一项公共资源,因此我们强烈建议不要上传任何敏感或重要的数据。请将此服务器仅用于学习和实验目的,以确保您的数据安全。
免费在线WEB代码编辑器(https://docs.ltpp.vip/LTPP-WEB-IDE/)
无论你是开发者还是学生,编程环境的搭建和管理可能会占用你宝贵的时间和精力。现在,有一款强大的免费在线代码编辑器,支持多种编程语言,让您可以随时随地编写和运行代码,提升编程效率,专注于创意和开发。
多语言支持,无缝切换
这款在线代码编辑器支持包括C、C++、JavaScript、TypeScript、Go、Rust、PHP、Java、Ruby、Python3以及C#在内的多种编程语言,无论您的项目需要哪种语言,都能在这里找到支持。
在线运行,快速定位问题
您可以在编写代码的同时,即时运行并查看结果,快速定位并解决问题,提高开发效率。
代码高亮与智能提示
编辑器提供代码高亮和智能提示功能,帮助您更快地编写代码,减少错误,提升编码质量。
免费二维码生成器(https://docs.ltpp.vip/LTPP-QRCODE/)
二维码(QR Code)是一种二维条码,能够存储更多信息,并且可以通过智能手机等设备快速扫描识别。它广泛应用于各种场景,如:
企业宣传
企业可以通过二维码分享公司网站、产品信息、服务介绍等。
活动推广
活动组织者可以创建二维码,参与者扫描后可以直接访问活动详情、报名链接或获取电子门票。
个人信息分享
个人可以生成包含联系方式、社交媒体链接、个人简历等信息的二维码。
电子商务
商家使用二维码进行商品追踪、促销活动、在线支付等。
教育
教师可以创建二维码,学生扫描后可以直接访问学习资料或在线课程。
交通出行
二维码用于公共交通的票务系统,乘客扫描二维码即可进出站或支付车费。 功能强大的二维码生成器通常具备用户界面友好,操作简单,即使是初学者也能快速上手和生成的二维码可以在各种设备和操作系统上扫描识别的特点。