“虽然这些概念在我的书中和视频课程中都出现过,但我没有把它们放在一起比较过。而且初学 Rust 的新手,对这几个概念会十分迷惑。所以,现在就让我们一起来探索一下。
其实按标准库的分类,首先就可以略知一二它们的作用。
Add trait
对应的就是 +
,而 Deref trait
则对应 共享(不可变)借用 的解引用操作,比如 *v
。相应的,也有 DerefMut trait
,对应独占(可变)借用的解引用操作。因为 Rust 所有权语义是贯穿整个语言特性,所以 拥有(Owner)
/不可变借用(&T)
/可变借用(&mut T)
的语义 都是配套出现的。From/Into
、TryFrom/TryInto
,而 AsRef/AsMut
也是作为配对出现在这里,就说明,该trait 是和类型转化有关。再根据 Rust API Guidelines[9] 里的命名规范可以推理,以 as_
开头的方法,代表从 borrowed -> borrowed
,即 reference -> reference
的一种转换,并且是无开销的。并且这种转换不能失败。分类我们清楚了,接下来逐个深入了解。
先来看该 trait 的定义:
pub trait Deref {
type Target: ?Sized;
#[must_use]
pub fn deref(&self) -> &Self::Target;
}
定义不复杂,Deref 只包含一个 deref
方法签名。该 trait 妙就妙在,它会被编译器 「隐式」调用,官方的说法叫 deref. 强转(deref coercion)[23] 。标准库示例:
use std::ops::Deref;
struct DerefExample<T> {
value: T
}
impl<T> Deref for DerefExample<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
let x = DerefExample { value: 'a' };
assert_eq!('a', *x);
代码中,DerefExample 结构体实现了 Deref trait,那么它就能被使用 解引用操作符*
来执行了。示例中,直接返回字段 value 的值。
可以看得出来,DerefExample 因为实现了 Deref 而拥有了一种类似于 指针的行为,因为它可以被解引用了。所以为了方便理解这种行为,我们称之为「指针语义」。DerefExample 也就变成了一种智能指针。这也是识别一个类型是否为智能指针的方法之一,看它是否实现 Deref 。但并不是所有智能指针都要实现 Deref ,也有的是实现 Drop ,或同时实现。
现在让我们来总结 Deref。
如果 T
实现了 Deref<Target = U>
,并且 x
是 类型 T
的一个实例,那么:
*x
(此时 T
既不是引用也不是原始指针)操作等价于 *Deref::deref(&x)
。&T
的值会强制转换为 &U
的值。T
实现了 U
的所有(不可变)方法。Deref 的妙用在于提升了 Rust 的开发体验。标准库里典型的示例就是 Vec<T>
通过实现 Deref
而共享了 slice
的所有方法。
impl<T, A: Allocator> ops::Deref for Vec<T, A> {
type Target = [T];
fn deref(&self) -> &[T] {
unsafe { slice::from_raw_parts(self.as_ptr(), self.len) }
}
}
比如,最简单的 len()
方法,实际上是在 `slice` 模块[24]被定义的。但因为 在 Rust 里,当执行 .
调用,或在函数参数位置,都会被编译器自动执行 deref 强转这种隐式行为,所以,就相当于 Vec<T>
也拥有了 slice
的方法。
fn main() {
let a = vec![1, 2, 3];
assert_eq!(a.len(), 3); // 当 a 调用 len() 的时候,发生 deref 强转
}
Rust 中的隐式行为并不多见,但是 Deref 这种隐式强转的行为,为我们方便使用智能指针提供了便利。
fn main() {
let h = Box::new("hello");
assert_eq!(h.to_uppercase(), "HELLO");
}
比如我们操作 Box<T>
,我们就不需要手动解引用取出里面T
来操作,而是当 Box<T>
外面这一层是透明的,直接来操作 T 就可以了。
再比如:
fn uppercase(s: &str) -> String {
s.to_uppercase()
}
fn main() {
let s = String::from("hello");
assert_eq!(uppercase(&s), "HELLO");
}
上面 uppercase 方法的参数类型 明明是 &str
,但现在main函数中实际传的类型是 &String
,为什么编译可以成功呢?就是因为 String 实现了 Deref :
impl ops::Deref for String {
type Target = str;
#[inline]
fn deref(&self) -> &str {
unsafe { str::from_utf8_unchecked(&self.vec) }
}
}
这就是 Deref 的妙用。但是有些人可能会“恍然大悟”,这不就是继承吗?大误。
这种行为好像有点像继承,但请不要随便用 Deref 来模拟继承。
来看一下 AsRef 的定义:
pub trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
我们已经知道 AsRef 可以用于转换。相比较于拥有隐式行为的 Deref ,AsRef 属于显式的转换。
fn is_hello<T: AsRef<str>>(s: T) {
assert_eq!("hello", s.as_ref());
}
fn main() {
let s = "hello";
is_hello(s);
let s = "hello".to_string();
is_hello(s);
}
上面示例中,is_hello 的函数是泛型函数。通过 T: AsRef<str>
的限定,并且在函数内使用 s.as_ref()
这样的显式调用来达到转换的效果。不管是 String
还是 str
其实都实现了 AsRef
trait。
那现在问题来了,什么时候使用 AsRef
呢?为啥不直接用 &T
?
考察这样一个示例:
pub struct Thing {
name: String,
}
impl Thing {
pub fn new(name: WhatTypeHere) -> Self {
Thing { name: name.some_conversion() }
}
上面示例中,new函数 name的类型参数有以下几种选择:
&str
。此时, 调用方(caller)需要传入一个引用。但是为了转换为 String ,则被调方(callee)则需要自己控制内存分配,并且会有拷贝。String
。此时,调用方传 String 还好,如果是传引用,则和情况 1 相似。T: Into<String>
。此时,调用方可以传 &str
和String
,但是在类型转换的时候同样会有内存分配和拷贝的情况。T: AsRef<str>
。同 情况 3 。T: Into<Cow<'a, str>>
,此时,可以避免一些分配。后面会介绍 Cow
。到底何时使用哪种类型,这个其实没有一个放之四海皆准的标准答案。有的人就是喜欢 &str
,不管在什么地方都会使用它。这里面其实是需要权衡的:
&str
就可以了。在 wasm-bindgen[28] 库中有一个 **web-sys**[29] 组件。该组件是对 浏览器 Web API 的 Rust 绑定。所以,通过 web-sys 可以使得通过 rust 代码就可以操作浏览器 DOM、获取服务器数据、绘制图形图像、处理音视频、处理客户端存储等。
但是要用 Rust 来绑定 Web API 并没有那么简单,比如操作 DOM ,依赖 JavaScript 的类继承,所以 web-sys 就必须提供对此继承层次结构的访问。在 web-sys 中,就利用 Deref
和 AsRef
来提供这种继承结构的访问功能。
使用 deref
let element: &Element = ...;
element.append_child(..); // call a method on `Node`
method_expecting_a_node(&element); // coerce to `&Node` implicitly
let node: &Node = &element; // explicitly coerce to `&Node`
你如果有 web_sys::Element
,那么就可以通过 Deref 来隐式得到 web_sys::Node
。
使用 deref 主要是从 API 的人体工程学来考虑,让开发者方便使用 .
操作来透明使用父类。
使用 AsRef
在 web-sys 中也为各种类型实现了大量的 AsRef 转换。
impl AsRef<HtmlElement> for HtmlAnchorElement
impl AsRef<Element> for HtmlAnchorElement
impl AsRef<Node> for HtmlAnchorElement
impl AsRef<EventTarget> for HtmlAnchorElement
impl AsRef<Object> for HtmlAnchorElement
impl AsRef<JsValue> for HtmlAnchorElement
通过显式调用 .as_ref()
,就可以得到父类结构的引用。
Deref 注重隐式透明地使用 父类结构,而 AsRef 则注重显式地获取父类结构的引用。这是结合具体的 API 设计所作的权衡,而不是无脑模拟 OOP 继承。
另外一个使用 AsRef 的案例是 http-types[30] 库,使用 AsRef和AsMut来转换各种类型。
例如,Request是Stream / headers/ URL
的组合,所以它实现了AsRef<Url>
, AsRef<Headers>
, 和AsyncRead
。同样地,Response 是Stream / headers/ Status Code
的组合。所以它实现了AsRef<StatusCode>
, AsRef<Headers>
, 和AsyncRead
。
fn forwarded_for(headers: impl AsRef<http_types::Headers>) {
// get the X-forwarded-for header
}
// 所以,forwarded_for 可以方便处理 Request/ Response / Trailers
let fwd1 = forwarded_for(&req);
let fwd2 = forwarded_for(&res);
let fwd3 = forwarded_for(&trailers);
来看一下 Borrow 的定义:
pub trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
对比一下 AsRef:
pub trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
是不是非常相似?所以,有人提出,这俩 trait 完全可以去掉一个。但实际上,Borrow 和 AsRef 是有区别的,它们都有存在的意义。
Borrow trait是用来表示 借用数据。而 AsRef 则是用来表示类型转换。在Rust中,为不同的语义不同的使用情况提供不同的类型表示是很常见的。
一个类型通过实现 Borrow,在 borrow()
方法中提供对 T
的引用/借用,表达的语义是可以作为某个类型 T
被借用,而非转换。一个类型可以自由地借用为几个不同的类型,也可以用可变的方式借用。
所以 Borrow 和 AsRef 如何选呢?
其实在标准库文档中给出的 HashMap 示例已经说明的很好了。我来给大家翻译一下。
HashMap<K, V>
存储键值对,对于 API 来说,无论使用 Key 的自有值,还是其引用,应该都可以正常地在 HashMap 中检索到对应的值。因为 HashMap 要对 key 进行 hash计算 和 比较,所以必须要求 不管是 Key 的自有值,还是引用,在进行 hash计算和比较的时候,行为应该是一致的。
use std::borrow::Borrow;
use std::hash::Hash;
pub struct HashMap<K, V> {
// fields omitted
}
impl<K, V> HashMap<K, V> {
// insert 方法使用 Key 的自有值,拥有所有权
pub fn insert(&self, key: K, value: V) -> Option<V>
where K: Hash + Eq
{
// ...
}
// 使用 get 方法通过 key 来获取对应的值,则可以使用 key的引用,这里用 &Q 表示
// 并且要求 Q 要满足 `Q: Hash + Eq + ?Sized `
// 而 K 呢 ,通过 `K: Borrow<Q>` 来表达 K 是 Q 的一个借用数据。
// 所以,这里要求 Q 的 hash 实现 和 K 是一样的,否则编译就会出错
pub fn get<Q>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized
{
// ...
}
}
代码的注释基本已经说明了问题。Borrow 是对借用数据的一种限制,并且配合额外的trait来使用,比如示例中的 Hash
和 Eq
等。
再看一个示例:
// 这个结构体能不能作为 HashMap 的 key?
pub struct CaseInsensitiveString(String);
// 它实现 Eq 没有问题
impl PartialEq for CaseInsensitiveString {
fn eq(&self, other: &Self) -> bool {
// 但这里比较是要求忽略了 ascii 大小写
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Eq for CaseInsensitiveString { }
// 实现 Hash 没有问题
// 但因为 eq 忽略大小写,那么 hash 计算也必须忽略大小写
impl Hash for CaseInsensitiveString {
fn hash<H: Hasher>(&self, state: &mut H) {
for c in self.0.as_bytes() {
c.to_ascii_lowercase().hash(state)
}
}
}
但是 CaseInsensitiveString 可以实现 Borrow<str>
吗?
很显然,CaseInsensitiveString 和 str 对 Hash 的实现不同,str 是不会忽略大小写的。因此,CaseInsensitiveString 不能实现 Borrow<str>
,所以 CaseInsensitiveString 不能作为 HashMap 的 key,但编译器无法通过 Borrow trait 来识别这种情况。
但是 CaseInsensitiveString 完全可以实现 AsRef 。
这就是 Borrow 和 AsRef 的区别,Borrow 更加严格一些,并且表示的语义和 AsRef 完全不同。
看一下 Cow 的定义:
pub enum Cow<'a, B>
where
B: 'a + ToOwned + ?Sized,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
看得出来, Cow 是一个枚举。有点类似于 Option,表示两种情况中的某一种。Cow 在这里就是表示 借用的 和 自有的,但只能出现其中的一种情况。
Cow 主要功能:
Cow
提供方法做克隆(Clone)处理,并避免多次重复克隆。Cow
的设计目的是提高性能(减少复制)同时增加灵活性,因为大部分情况下,业务场景都是读多写少。利用 Cow
,可以用统一,规范的形式实现,需要写的时候才做一次对象复制。这样就可能会大大减少复制的次数。
它有以下几个要点需要掌握:
Cow<T>
能直接调用 T
的不可变方法,因为 Cow
这个枚举,实现了 Deref
;T
的时候,可以使用.to_mut()
方法得到一个具有所有权的值的可变借用;.to_mut()
不一定会产生Clone;.to_mut()
有效,但是不会产生新的Clone;.to_mut()
只会产生一次Clone。T
的时候,可以使用.into_owned()
创建新的拥有所有权的对象,这个过程往往意味着内存拷贝并创建新对象;Cow
中的值是借用状态,调用此操作将执行Clone;self
类型,它会“消费”原先的那个类型实例,调用之后原先的类型实例的生命周期就截止了,在 Cow
上不能调用多次;Cow 在 API 设计上用的比较多:
use std::borrow::Cow;
// 返回值使用 Cow ,避免多次拷贝
fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> {
if input.contains(' ') {
let mut buf = String::with_capacity(input.len());
for c in input.chars() {
if c != ' ' {
buf.push(c);
}
}
return Cow::Owned(buf);
}
return Cow::Borrowed(input);
}
当然,什么时候使用 Cow ,又回到了我们前文中那个「什么时候使用 AsRef
」的讨论,一切都要权衡,并没有放之四海皆准的标准答案。
要理解 Rust 里的各种 类型 和 trait,需要结合所有权语义,好好琢磨它的文档和示例,应该不难去理解。不知道看过本文,解开你的疑惑了吗?欢迎交流反馈。
[1]
std: https://doc.rust-lang.org/std/index.html
[2]
ops: https://doc.rust-lang.org/std/ops/index.html
[3]
Deref: https://doc.rust-lang.org/std/ops/trait.Deref.html
[4]
trait: https://doc.rust-lang.org/std/ops/index.html#traits
[5]
std: https://doc.rust-lang.org/std/index.html
[6]
convert: https://doc.rust-lang.org/std/convert/index.html
[7]
AsRef: https://doc.rust-lang.org/std/convert/trait.AsRef.html
[8]
trait: https://doc.rust-lang.org/std/convert/index.html#traits
[9]
Rust API Guidelines: https://rust-lang.github.io/api-guidelines/
[10]
std: https://doc.rust-lang.org/std/index.html
[11]
borrow: https://doc.rust-lang.org/std/borrow/index.html
[12]
Borrow: https://doc.rust-lang.org/std/borrow/trait.Borrow.html
[13]
trait: https://doc.rust-lang.org/std/borrow/index.html#traits
[14]
Borrow: https://doc.rust-lang.org/std/borrow/trait.Borrow.html
[15]
BorrowMut: https://doc.rust-lang.org/std/borrow/trait.BorrowMut.html
[16]
ToOwned: https://doc.rust-lang.org/std/borrow/trait.ToOwned.html
[17]
std: https://doc.rust-lang.org/std/index.html
[18]
borrow: https://doc.rust-lang.org/std/borrow/index.html
[19]
Cow: https://doc.rust-lang.org/std/borrow/enum.Cow.html
[20]
std: https://doc.rust-lang.org/std/index.html
[21]
ops: https://doc.rust-lang.org/std/ops/index.html
[22]
Deref: https://doc.rust-lang.org/std/ops/trait.Deref.html
[23]
deref. 强转(deref coercion): https://doc.rust-lang.org/std/ops/trait.Deref.html#more-on-deref-coercion
[24]
slice
模块: https://doc.rust-lang.org/std/primitive.slice.html
[25]
std: https://doc.rust-lang.org/std/index.html
[26]
convert: https://doc.rust-lang.org/std/convert/index.html
[27]
AsRef: https://doc.rust-lang.org/std/convert/trait.AsRef.html
[28]
wasm-bindgen: https://github.com/rustwasm/wasm-bindgen
[29]
web-sys: https://github.com/rustwasm/wasm-bindgen/tree/master/crates/web-sys
[30]
http-types: https://github.com/http-rs/http-types
[31]
std: https://doc.rust-lang.org/std/index.html
[32]
borrow: https://doc.rust-lang.org/std/borrow/index.html
[33]
Borrow: https://doc.rust-lang.org/std/borrow/trait.Borrow.html
[34]
std: https://doc.rust-lang.org/std/index.html
[35]
borrow: https://doc.rust-lang.org/std/borrow/index.html
[36]
Cow: https://doc.rust-lang.org/std/borrow/enum.Cow.html