Rust标准库里提供了若干个与引用相关的trait,通过这些特征可以获取需要的引用类型,通常用于扩展方法签名的灵活性,可以接受更多的输入类型。
具体来说,常见的特征有6种:AsRef、AsMut、Deref、DerefMut、Borrow、BorrowMut,它们按可变/不可变可以分为三组,下面只分析不可变版本的,也就是AsRef、Deref、Borrow,可变版本可依此类推。
让我们先来看一下这三个特征的定义(从标准库中拷出,去掉宏,只保留必要注释):
pub trait AsRef<T: ?Sized> {
/// Converts this type into a shared reference of the (usually inferred) input type.
fn as_ref(&self) -> &T;
}
pub trait Deref {
/// The resulting type after dereferencing.
type Target: ?Sized;
/// Dereferences the value.
fn deref(&self) -> &Self::Target;
}
pub trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
它们看起来很类似,都是将一个引用类型转化为另一个引用类型,那么它们有什么区别吗,或者它们分别是针对什么场景而设计的呢?
AsRef与Deref
我们先来看AsRef和Deref,二者的定义其实很接近,都是实现&self->&T的转换,可以将一个不可变引用转为另一个不可变引用,而且它们其实上也很常见。Deref就不用多说了,了解过Rust智能指针的都清楚这个特征,Box和Arc都是基于Deref特征实现,实现了Deref特征的类,在遇到*操作符(也就是解引用)时会调用它的deref方法,并递归地处理,这样实现了Deref特征的类在一定程度上就可以当作一个通常的指针使用,也就是所谓的智能指针。AsRef其实也很常见,可能你没有注意到,它频繁出现在Rust的文件读写接口,与路径相关的接口基本都能看到它,以打开文件的接口为例,Rust是这么定义的:
pub fn open<P: AsRef<Path>>(&self, path: P) -> io::Result<File> {
self._open(path.as_ref())
}
那么AsRef和Deref有什么区别吗,或者说既然已经有了Deref特征,为什么还需要再设计一个AsRef特征?仔细分析它们的定义就能发现,AsRef的目标类型是通过泛型定义的,而Deref的目标类型则是通过关联类型定义。因此它们的区别实质上就是泛型与关联类型的区别。
通常情况下,关联类型与泛型非常接近,关联类型除了能提高泛型的可读性,它更重要的是增加一个限制:只能有一个实现版本,我们在实现这个特征时只能选择性地实现一个版本:
struct SmartPointer {
s: String
}
/// 无法编译,这两个版本只能选择一个
impl std::ops::Deref for SmartPointer {
type Target = String;
fn deref(&self) -> &Self::Target {
return &self.s;
}
}
/// 无法编译,这两个版本只能选择一个
impl std::ops::Deref for SmartPointer {
type Target = str;
fn deref(&self) -> &Self::Target {
return &self.s;
}
}
与之相反,AsRef的实现可以有任意多的版本:
struct SmartPointer {
s: String,
}
impl AsRef<String> for SmartPointer{
fn as_ref(&self) -> &String {
return &self.s;
}
}
impl AsRef<str> for SmartPointer{
fn as_ref(&self) -> &str {
return &self.s;
}
}
仔细一想很容易理解为什么Rust这么设计:Deref作为智能指针不能有二义性,否则遇到*p时Rust怎么知道应该采用哪个版本的实现,你可能会说编译器不是会自动推导类型吗:
fn take(s: &String){}
fn main() {
let p = &SmartPointer{s: String::from("test")};
take(p);
}
问题是解引用的过程可以是递归的,假设Rust支持实现多个版本的Deref,那它在遇到这种情况下应该怎么解引用:
SmartPointer应该先解引用为PointerA再解引用为PointerC,还是先解引用为PointerB再解引用为PointerC?为了消除这个歧义,定义
Deref特征时使用关联类型而不是泛型,这样保证了只会有一种实现,不会有歧义。而AsRef则相反,它作为Deref的补充,当用户需要实现多个版本的指针转换时,AsRef就派上用场了。与Deref不同,它需要通过as_ref()方法显式调用,而不是编译器自行处理,因此即使有歧义也可以通过显式的类型声明来消除歧义:
struct SmartPointer {
a: PointerA,
b: PointerB,
}
struct PointerA{
c: PointerC,
}
struct PointerB{
c: PointerC,
}
struct PointerC{}
impl AsRef<PointerA> for SmartPointer{
fn as_ref(&self) -> &PointerA {
return &self.a;
}
}
impl AsRef<PointerB> for SmartPointer{
fn as_ref(&self) -> &PointerB {
return &self.b;
}
}
impl AsRef<PointerC> for PointerA{
fn as_ref(&self) -> &PointerC {
return &self.c;
}
}
impl AsRef<PointerC> for PointerB{
fn as_ref(&self) -> &PointerC {
return &self.c;
}
}
fn take(c: &PointerC){}
fn main() {
let a = SmartPointer {
a: PointerA { c: PointerC {} },
b: PointerB { c: PointerC {} },
};
let b: &PointerA = a.as_ref();
take(<SmartPointer as AsRef<PointerA>>::as_ref(&a).as_ref());
take(<SmartPointer as AsRef<PointerB>>::as_ref(&a).as_ref());
}
AsRef和Borrow
接下来我们再看AsRef和Borrow,我们上面已经分析了AsRef和Deref最大的区别就是泛型和关联类型的区别,而Borrow与AsRef一样,均是使用泛型定义,事实上,你仔细看会发现它们的定义其实完全一致(Borrow特征中的Borrowed只是为了增强可读性,将它改成T就与AsRef的定义完全一样),那么它们的区别又是什么呢?
首先来看Borrow的应用场景,它被广泛应用在标准库的数据结构中,尤其是HashMap和BTreeMap,如果你研究过HashMap的api,你会发现它的get方法是这么定义的:
impl<K, V, S> HashMap<K, V, S>
where
K: Eq + Hash,
S: BuildHasher,
{
pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq,
{
self.base.get(k)
}
}
哈希表的键值类型分别是K和V,但get方法的参数k的类型很奇怪,既不是K,也不是&K,而是&Q,而Q则通过Borrow特征与K关联起来,用rust的专业说法称为:K可被借用为Q。
让我们简单回顾一下哈希表的实现原理,查询时首先会计算key的哈希值,通过哈希值定位哈希桶,假设使用链表法解决哈希冲突,在这个桶下面的链表中逐一比对key的内容是否一致即可。由于K可被借用为Q,因此对比的时候只需要将链表中各元素的键“借用”为&Q,这样就可与参数k直接对比了(注意Q需要实现Eq)。
听起来用AsRef也可以实现这一效果,为什么需要再增加一个Borrow特征呢?如果HashMap中的get方法是用AsRef定义的会怎么样呢?
pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
K: AsRef<Q>,
Q: Hash + Eq,
{
}
假设K和V类型都是String,那调用的时候就不能指定:
let m: HashMap<String, String>;
m.get(&String("hello")); // 传递的参数类型是&String,那么Q是String,不成立,因为String没有实现AsRef<String>
只能指定:
let m: HashMap<String, String>;
m.get("hello"); // 传递的参数类型是&str,那么Q是str,成立,因为String实现了AsRef<str>
设计标准库的时候自然希望API越灵活越好,这样用户可以根据他们的场景随意传递参数,不需要自己再做适配转换。对于HashMap<String, String>,我们希望它的get方法既能接受:&String也能接受&str,而Borrow刚好能满足这一要求,因此rust会为所有T和&T都默认实现Borrow<T>:
impl<T: ?Sized> Borrow<T> for T {
fn borrow(&self) -> &T {
self
}
}
impl<T: ?Sized> Borrow<T> for &T {
fn borrow(&self) -> &T {
&**self
}
}
同时类似AsRef,rust为String实现了Borrow<str>,这样通过Borrow特征,rust扩大了标准库中API的应用范围,简化了用户的调用过程。
这里也能窥见rust设计的简洁之美,其它语言可能会通过重载函数来实现,例如java;rust并不支持函数重载,当需要接受多种类型的参数时,rust倾向于使用特征来抽象,这样既能减少API的维护成本,也能降低使用方的学习成本。
总结
AsRef与Deref的区别在于泛型和关联类型,泛型可以实现任意数量版本,而关联类型只能实现一种版本,由于解引用是编译器自动调用的,为了消除歧义,Deref通过关联类型定义;AsRef与Borrow都是通过泛型定义的,区别在于rust会自动为T和&T实现Borrow<T>,通过Borrow特征可以进一步扩大API的应用范围,例如HashMap<String, String>的get方法可以同时接受&String和&str