Rust常用特型之Borrow和BorrowMut特型

2024-04-21 09:36

本文主要是介绍Rust常用特型之Borrow和BorrowMut特型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在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>
whereQ: ?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;
}

这篇关于Rust常用特型之Borrow和BorrowMut特型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/922748

相关文章

Python常用命令提示符使用方法详解

《Python常用命令提示符使用方法详解》在学习python的过程中,我们需要用到命令提示符(CMD)进行环境的配置,:本文主要介绍Python常用命令提示符使用方法的相关资料,文中通过代码介绍的... 目录一、python环境基础命令【Windows】1、检查Python是否安装2、 查看Python的安

python判断文件是否存在常用的几种方式

《python判断文件是否存在常用的几种方式》在Python中我们在读写文件之前,首先要做的事情就是判断文件是否存在,否则很容易发生错误的情况,:本文主要介绍python判断文件是否存在常用的几种... 目录1. 使用 os.path.exists()2. 使用 os.path.isfile()3. 使用

Java实现本地缓存的常用方案介绍

《Java实现本地缓存的常用方案介绍》本地缓存的代表技术主要有HashMap,GuavaCache,Caffeine和Encahche,这篇文章主要来和大家聊聊java利用这些技术分别实现本地缓存的方... 目录本地缓存实现方式HashMapConcurrentHashMapGuava CacheCaffe

Python将字符串转换为小写字母的几种常用方法

《Python将字符串转换为小写字母的几种常用方法》:本文主要介绍Python中将字符串大写字母转小写的四种方法:lower()方法简洁高效,手动ASCII转换灵活可控,str.translate... 目录一、使用内置方法 lower()(最简单)二、手动遍历 + ASCII 码转换三、使用 str.tr

Spring Boot 常用注解整理(最全收藏版)

《SpringBoot常用注解整理(最全收藏版)》本文系统整理了常用的Spring/SpringBoot注解,按照功能分类进行介绍,每个注解都会涵盖其含义、提供来源、应用场景以及代码示例,帮助开发... 目录Spring & Spring Boot 常用注解整理一、Spring Boot 核心注解二、Spr

rust 中的 EBNF简介举例

《rust中的EBNF简介举例》:本文主要介绍rust中的EBNF简介举例,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1. 什么是 EBNF?2. 核心概念3. EBNF 语法符号详解4. 如何阅读 EBNF 规则5. 示例示例 1:简单的电子邮件地址

Java中的内部类和常用类用法解读

《Java中的内部类和常用类用法解读》:本文主要介绍Java中的内部类和常用类用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录内部类和常用类内部类成员内部类静态内部类局部内部类匿名内部类常用类Object类包装类String类StringBuffer和Stri

MySQL连接池(Pool)常用方法详解

《MySQL连接池(Pool)常用方法详解》本文详细介绍了MySQL连接池的常用方法,包括创建连接池、核心方法连接对象的方法、连接池管理方法以及事务处理,同时,还提供了最佳实践和性能提示,帮助开发者构... 目录mysql 连接池 (Pool) 常用方法详解1. 创建连接池2. 核心方法2.1 pool.q

Spring Boot 常用注解详解与使用最佳实践建议

《SpringBoot常用注解详解与使用最佳实践建议》:本文主要介绍SpringBoot常用注解详解与使用最佳实践建议,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要... 目录一、核心启动注解1. @SpringBootApplication2. @EnableAutoConfi

SQL常用操作精华之复制表、跨库查询、删除重复数据

《SQL常用操作精华之复制表、跨库查询、删除重复数据》:本文主要介绍SQL常用操作精华之复制表、跨库查询、删除重复数据,这些SQL操作涵盖了数据库开发中最常用的技术点,包括表操作、数据查询、数据管... 目录SQL常用操作精华总结表结构与数据操作高级查询技巧SQL常用操作精华总结表结构与数据操作复制表结