在Rust标准库中,存在很多常用的工具类特型,它们能帮助我们写出更具有Rust风格的代码。

std::borrow::BorrowAsRef有点相似,如果一个类型实现了Borrow<T>,那么你可以从它的borrow函数里高效的借出一个&T。但是Borrow施加了一些限制,就是借出的&T必须和该类型拥有相同的哈希和比较算法。注意,Rust并不强制这一点,只是标准库注明了该特型的限制。这使得Borrow主要用于哈希表和结构树中的key和处理其它需要比较或者哈希值的场景。

举一个实际例子,对于String来说,它实现了AsRef<str>AsRef<[u8]>AsRef<Path>。但是这三种实现的目标类型都会产生不同的哈希值。只有&strString会产生相同的哈希值,所以String 只实现了Borrow<str>

Borrow特型的定义和AsRef是相等的,只有名称上的区别:

trait Borrow<Borrowed: ?Sized> {
	fn borrow(&self) -> &Borrowed;
}

Borrow特型被设计用来解决一个特定的问题,那就是哈希表和关联集合类型的通过不同类型的Key进行查询的问题。

例如,假定你有一个std::collections::HashMap<String,i32>用来做从字符串到i32的映射。这个HashMap的键是String类型,每个键值对都拥有自己的键。那么查询表中的键值对的函数定义是什么样子的呢?我们先从简单的看起:

impl<K, V> HashMap<K, V> where K: Eq + Hash
{
	fn get(&self, key: K) -> Option<&V> { ... }
}

这样的定义的确有意义,当你查询一个entry时,你必须提供一个key,当然这个key的类型必须是表中的K。但是此例中,K是String,这个函数定义会强制你在每次函数调用时传递一个String的值,这有些小题大作了。你所需要的只是对这个Key的一个引用而已,于是,代码可以稍微修改成下面这样:

impl<K, V> HashMap<K, V> where K: Eq + Hash
{
	fn get(&self, key: &K) -> Option<&V> { ... }
}

上最初的定义相比,仅是将key的类型改成了&K,但是对上例来说,你必须传递一个&String。假定你有一个&str,你必须先创建一个临时String,然后再将它的引用传递过去。你需要写出类似如下代码:hashtable.get(&"twenty-two".to_string()).

从语法上看,上面的写法问题不大,但是从性能上看,上面的写法并不可取,它首先在堆上会分配一个String的缓冲区并且将所有文本复制进去,然后你再借出一个引用,然后将引用 传递到get函数中去,在函数调用结束后,再销毁这个String。

更好的解决方案是函数参数可以传递一个和我们的key具有相同的hash和比较算法的类型,例如&str,它很好的符合了这个要求。于是,我们需要继续改进代码,下面是标准库中的版本:

impl<K, V> HashMap<K, V> where K: Eq + Hash
{
  fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
  	where K: Borrow<Q>,
  	Q: Eq + Hash
  { ... }
}

换句话说,如果你能从一个entry的key中借出一个&Q,并且它的哈希和比较算法实现和key的结果一样,那么&Q也应该是可以接受的key的类型。由于String实现了Borrow<str>Borrow<String>, 这个最终版本的get函数允许你传递&String或者&str来作为一个key.

这里的意思是如果哈希表的key的哈希与比较结果与另一种类型相同,并且Key的类型可以借出另一种类型的引用,则另一种类型的引用也是有效的key类型(对于get函数来讲,插入时肯定是严格的类型匹配)。这也侧面反映了哈希表需要进行key的哈希和比较,只要哈希和比较后的结果一样,就可以用作key。那是不是所有符合这个条件的类型都可以作为key呢?这里需要进一步缩小范围进行限定,因此约束了K: Borrow<Q>,也就是K可以借出Q,这也是有意义的。两个无关的类型可以相互替换在设计上是有瑕疵的,必须满足这个Borrow特型才可以。如果K实现了Borrow<Q>,那么根据Borrow的定义,K和Q的哈希及比较结果必须是相同的(虽然Rust并不强制这一点,但Borrow就是为了解决这个问题而引入的协议,你非要自己违反它别人是没有办法的)。

从这里也可以看出,部分转换特型其实就是添加了一个君子协定,约定一个公共的方法。

Vec<T>[T:N]实现了Borrow<[T]>,也就是向量和数据可以借出底层切片的引用(虽然AsRef也这么做了,但是两者应用的场景不同)。每个像字符串这样的类型都允许错出它的底层切片类型,例如String实现了Borrow<str>PathBuf实现了Borrow<Path>等。所有的标准库关联集合类型(例如HashSet)都使用了Borrow来决定某个类型是否能传递给他们的查询函数。

标准库同时包含了一个空实现,所有的类型都可以借出自己,这是显而易见的。这样,&K 传递给get函数也是没有问题的。

作为一个约定,每个&mut T类型同时也实现了Borrow<T>,用来返回一个平常使用的共享的引用。这允许你向集合查询函数传递一个可变引用而不需要重新把它借出为一个共享引用。模拟了Rust隐式从可变引用到共享引用的引用转换。

这里我们看一下源码:

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Borrow<T> for &mut T {
    fn borrow(&self) -> &T {
        &**self
    }
}

这里的self其实为 &&mut T,所以需要两次解引用 得到T,然后再前面加&得到&T,这是没有问题的。

结合上面的例子,如果传进去一个&mut String, 这里K为String ,那么key是什么呢? 按定义是K能借出Q,也就是String能借出什么类型?能借出自己和str,所以 key 可以是 &String和&str。但是涉及到mut到底是什么会事呢?

我们写了如下测试代码:

use std::borrow::Borrow;
use std::hash::Hash;

pub fn get<K,V,Q>(k: &Q) -> Option<&V>
where
    Q: ?Sized,
    K: Borrow<Q>,
    Q: Hash + Eq, 
{   
    println!("type is {}", std::any::type_name::<Q>());
    None
}

fn main() {
    let mut key = String::from("key");
    let v = get::<String,i32,String>(&mut key);
    println!("v:{v:?}");
}

这里我们打印出来Q的类型是String. 也就是说,我们传递进去一个&mut T,它自动变成了一个&T ,这个自动引用转换是通过borrow进行的还是Rust自动转换的,不得而知。猜想是Rust自动转换的。那个 &mut T的空实现估计是为了让Borrow更加完美。

但是看书中的意思是使用了Borrow特型的borrow函数进行转换的。

相应的,BorrowMut特型允许借出一个可变引用。除此外和Borrow用法没有区别

trait BorrowMut<Borrowed: ?Sized>: Borrow<Borrowed> {
  fn borrow_mut(&mut self) -> &mut Borrowed;
}
Logo

欢迎加入西安开发者社区!我们致力于为西安地区的开发者提供学习、合作和成长的机会。参与我们的活动,与专家分享最新技术趋势,解决挑战,探索创新。加入我们,共同打造技术社区!

更多推荐