原文链接:https://lucumr.pocoo.org/2022/1/6/rust-extension-map/
翻译:trdthg
选题:trdthg
拓展 Rust 中的 Map
在 Rust 中,如果你想为用户提供一个灵活的 API,一般可以引入泛型参数。以一个 web 框架为例,它可能需要一个程序类型,并且需要传递给很多函数。这个程序类型需要能够以配置的形式被参数化。
引入 Any 特征
一个解决方法是使用 Any
特征。它需要一个 'static
的生命周期,当你之后使用它时,还需要用 Box
进行装箱。比如我们可能对它进行向下转型,即转换为原始的类型。这意味着你可以在某个地方(比如我们的 App)中存储和获取任意类型。
我们期望的 API 大致如下:
let app = App::new();
// place in extension map
app.extensions().insert(Config { ... });
app.extensions().insert(Database { ... });
// retrieve from extension map
let config = app.extensions().get::<Config>();
我们的 app 需要容纳其他拓展的类型,以便之后使用。
现在,让我们试试最简单的实现方式:准备一个 Extensions
对象,让它实现插入和获取的方法。如果一个拓展还不存在,我们就自动插入一个默认的(需要实现
Default
特征)。
use std::collections::HashMap;
use std::any::{Any, TypeId};
#[derive(Default)]
pub struct Extensions {
map: HashMap<TypeId, Box<dyn Any>>,
}
impl Extensions {
pub fn insert<T: 'static>(&mut self, value: T) {
self.map.insert(TypeId::of::<T>(), Box::new(value));
}
pub fn get<T: 'static>(&self) -> &T {
self.map.get(&TypeId::of::<T>())
.and_then(|b| b.downcast_ref())
.unwrap()
}
pub fn get_mut<T: Default + 'static>(&mut self) -> &mut T {
self.ensure::<T>();
self.map.get_mut(&TypeId::of::<T>())
.and_then(|b| b.downcast_mut())
.unwrap()
}
fn ensure<T: Default + 'static>(&mut self) {
if self.map.get(&TypeId::of::<T>()).is_none() {
self.insert(T::default());
}
}
}
上面的代码非常直接,但是存在两个问题:首先,只有 get_mut
能够调用 ensure
去插入默认值,如果有人直接调用 get
就会导致
panic。第二个问题是,借用检查器会让之后的编写非常困难。上面的 map 对于解决经典的问题(例如 app)是很有用的,你只需要配置一次,自那之后 map
就像是被冻结了一样,因为有太多的引用在飞来分飞去,以至于没有人能够得到 &mut
的引用。
how does it work?
上面的代码是如何做到的呢,Rust 中的每一种类型都会有一个 type ID,你可以使用 TypeId::of::<T>()
获取。他是唯一的,你可以用它进行比较,或者是作为 map 的键来使用。每种类型只允许有一个值。接着我们把 T 作为 dyn Any
存储在 map
里,Any
特征允许我们使用 downcast_ref
和 downcast_mut
方法拿到原始类型。由于我们使用了 ensure
方法确保这里的类型存在,因此可以安全的 unwrap。
内部可变性
让我们看一个 web 框架或者是模板引擎的常见案例。以
MiniJinja(模板引擎)为例,它里面有一个 State
对象,每次模板初始化时都会创建一次,State 没有实现 Send 和 Sync,MiniJinja 在评估时需要 State。如果你想让用户能够放入自定义的
State 呢?在这种情况下,我们可以通过在内部使用 RefCell
来调整上面的类型。
use std::collections::HashMap;
use std::any::{Any, TypeId};
use std::cell::{Ref, RefCell, RefMut};
#[derive(Default)]
pub struct Extensions {
map: RefCell<HashMap<TypeId, Box<dyn Any>>>,
}
impl Extensions {
pub fn insert<T: 'static>(&self, value: T) {
self.map.borrow_mut().insert(TypeId::of::<T>(), Box::new(value));
}
pub fn get<T: Default + 'static>(&self) -> Ref<'_, T> {
self.ensure::<T>();
Ref::map(self.map.borrow(), |m| {
m.get(&TypeId::of::<T>())
.and_then(|b| b.downcast_ref())
.unwrap()
})
}
pub fn get_mut<T: Default + 'static>(&self) -> RefMut<'_, T> {
self.ensure::<T>();
RefMut::map(self.map.borrow_mut(), |m| {
m.get_mut(&TypeId::of::<T>())
.and_then(|b| b.downcast_mut())
.unwrap()
})
}
fn ensure<T: Default + 'static>(&self) {
if self.map.borrow().get(&TypeId::of::<T>()).is_none() {
self.insert(T::default());
}
}
}
从用户的角度来看,几乎没有变化。主要的区别是你不需要一个可变引用就能调用 get_mut
,这一壮举是由 RefCell
实现的,Refcell
能够将检查移动到运行时。当一个 RefMut
被给出时,如果已经存在任何的可变或不可变引用,就会发生
panic。对于这里的用户来说,这并不是一个很大的问题,因为我们可以很容易地确保只有一个可变的引用在使用。特别棒的是,Ref 和 RefMut
类型提供了一个静态的 map 方法,让你可以轻松派生出另一个 Ref 或 RefMut,并保持原来的引用,但对值进行转换。
同步支持
如果我们想要用 Send 和 Sync 来实现和上面相同的效果呢?我们需要一个锁。可惜的是标准库提供的 Mutex 和 RwLock 不能让你在拿到锁的同时
map,你可以使用 parking_lot
替代,它实现了必要的一些方法。
use parking_lot::{
MappedRwLockReadGuard,
MappedRwLockWriteGuard,
RwLock,
RwLockReadGuard,
RwLockWriteGuard,
};
use std::any::{Any, TypeId};
use std::collections::HashMap;
#[derive(Default)]
pub struct Extensions {
map: RwLock<HashMap<TypeId, Box<dyn Any>>>,
}
impl Extensions {
pub fn insert<T: Send + Sync + 'static>(&self, value: T) {
self.map.write().insert(TypeId::of::<T>(), Box::new(value));
}
pub fn get<T: Send + Sync + Default + 'static>(&self) -> MappedRwLockReadGuard<'_, T> {
self.ensure::<T>();
RwLockReadGuard::map(self.map.read(), |m| {
m.get(&TypeId::of::<T>())
.and_then(|b| b.downcast_ref())
.unwrap()
})
}
pub fn get_mut<T: Send + Sync + Default + 'static>(&self) -> MappedRwLockWriteGuard<'_, T> {
self.ensure::<T>();
RwLockWriteGuard::map(self.map.write(), |m| {
m.get_mut(&TypeId::of::<T>())
.and_then(|b| b.downcast_mut())
.unwrap()
})
}
fn ensure<T: Default + Send + Sync + 'static>(&self) {
if self.map.read().get(&TypeId::of::<T>()).is_none() {
self.insert(T::default());
}
}
}
注意:由于 Any 并没有实现 Debug,所以我们很难为我们的 map 实现 Debug 特征,一些简单的改变并不能解决目前的问题。下半部分我们将介绍
as-any
模式
我们面临的挑战是,在 Rust 里,你不能使用 Box<Any + Debug>
,然而还是有一些方法解决这个问题。
为 map 实现 Debug
简化问题
我们的目标是对 Box<dyn Any>
做一个包装,并让 Wrapper 实现 Debug。
#[derive(Debug)]
struct AnyBox(Box<dyn Any + Debug>);
如果你尝试编译,编译器应该会很不高兴的抛出错误:
error[E0225]: only auto traits can be used as additional traits in a trait object
--> src/main.rs:9:29
|
9 | struct AnyBox(Box<dyn Any + Debug>);
| --- ^^^^^ additional non-auto trait
| |
| first non-auto trait
|
= help: consider creating a new trait with all of these as supertraits and
using that trait here instead: `trait NewTrait: Any + Debug {}`
超级特征
幸运的是,编译器再次为我们指明了解决之道,我们需要创建一个父特征,并利用特征约束。同时,我们为所有实现了 Any 和 Debug 的类型实现我们的超级特征。就像下面这样:
#[derive(Debug)]
struct AnyBox(Box<dyn DebugAny>);
trait DebugAny: Any + Debug {}
impl<T: Any + Debug + 'static> DebugAny for T {}
你可以想这样构建一个 Box,但是真正不能通过编译的是向下转型
fn main() {
let any_box = AnyBox(Box::new(42i32));
dbg!(any_box.0.downcast_ref::<i32>());
}
编译器会告诉我们,AnyBox 中的值并没有 downcast_ref
方法
error[E0599]: no method named `downcast_ref` found for struct
`Box<(dyn DebugAny + 'static)>` in the current scope
--> src/main.rs:15:20
|
15 | dbg!(any_box.0.downcast_ref::<i32>());
| ^^^^^^^^^^^^ method not found in `Box<(dyn DebugAny + 'static)>`
原因是 Box<dyn DebugAny>
并不是 Box<dyn Any>
,因此我们不能那里得到 Any
特征拥有的方法。那么我们如何解决这个问题呢?最简单的方法是 "as any" 模式,我们在我们的 DebugAny
特征上实现一个方法,将其向上转换为一个 Any。看起来像这样:
trait DebugAny: Any + Debug {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<T: Any + Debug + 'static> DebugAny for T {
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
}
现在虽然我们依然不能在 DebugAny 上调用 downcast_ref
,但是我们可以拿走它的值,并调用 as_any
得到一个
&dyn Any
:
fn main() {
let any_box = AnyBox(Box::new(42i32));
dbg!(any_box.0.as_any().downcast_ref::<i32>());
dbg!(&any_box);
}
但是当我们运行后,却得到了一个 None。发生什么事了???
[src/main.rs:23] any_box.0.as_any().downcast_ref::<i32>() = None
这个谜题的答案与方法解析的工作方式和空白实现有关。当我们在 Box<dyn DebugAny>
上调用 as_any
时,Box
并没有发生自动解引用,事实上调用的是 Box<dyn DebugAny>
的 as_any,因为 Box 现在也实现了我们的
DebugAny。那么,我们如何穿过这个 Box 呢?通过手动解引用。
fn main() {
let any_box = AnyBox(Box::new(42i32));
dbg!((*any_box.0).as_any().downcast_ref::<i32>());
dbg!(&any_box);
}
这样就是我们预期的值了
[src/main.rs:23] (*any_box.0).as_any().downcast_ref::<i32>() = Some(
42,
)
[src/main.rs:24] &any_box = AnyBox(
42,
)
可调试的 Extension Map
有了上面的经验,我们现在可以拿出之前的非同步 map,稍加改造就能为其实现 Debug。
use std::any::{Any, TypeId};
use std::cell::{Ref, RefCell, RefMut};
use std::collections::HashMap;
use std::fmt::Debug;
trait DebugAny: Any + Debug {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<T: Any + Debug + 'static> DebugAny for T {
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
}
#[derive(Default, Debug)]
pub struct Extensions {
map: RefCell<HashMap<TypeId, Box<dyn DebugAny>>>,
}
impl Extensions {
pub fn insert<T: Debug + 'static>(&self, value: T) {
self.map
.borrow_mut()
.insert(TypeId::of::<T>(), Box::new(value));
}
pub fn get<T: Default + Debug + 'static>(&self) -> Ref<'_, T> {
self.ensure::<T>();
Ref::map(self.map.borrow(), |m| {
m.get(&TypeId::of::<T>())
.and_then(|b| (**b).as_any().downcast_ref())
.unwrap()
})
}
pub fn get_mut<T: Default + Debug + 'static>(&self) -> RefMut<'_, T> {
self.ensure::<T>();
RefMut::map(self.map.borrow_mut(), |m| {
m.get_mut(&TypeId::of::<T>())
.and_then(|b| (**b).as_any_mut().downcast_mut())
.unwrap()
})
}
fn ensure<T: Default + Debug + 'static>(&self) {
if self.map.borrow().get(&TypeId::of::<T>()).is_none() {
self.insert(T::default());
}
}
}
向 map 里面添加点东西,打印一下:
[src/main.rs:63] &extensions = Extensions {
map: RefCell {
value: {
TypeId {
t: 13431306602944299956,
}: 42,
},
},
}
在这个例子中,我在 map 中放置了一个 32 位的整数 42,它打印出了作为键的 TypeId,和作为值的 42。
保留类型名称
如果你想保留原来的类型名称,而不仅仅是类型的 ID,我们可以使用一个自定义的类型作为 map 的键。通过对 TypeId 和 TypeName 做一次简单的包装就能轻松实现:
use std::any::{TypeId, type_name};
use std::hash::{Hash, Hasher};
use std::fmt::{self, Debug};
pub struct TypeKey(TypeId, &'static str);
impl TypeKey {
pub fn of<T: 'static>() -> TypeKey {
TypeKey(TypeId::of::<T>(), type_name::<T>())
}
}
impl Hash for TypeKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl PartialEq for TypeKey {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for TypeKey {}
impl Debug for TypeKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.1)
}
}
接着用它替换掉原来的键,调试一下:
[src/main.rs:90] &extensions = Extensions {
map: RefCell {
value: {
i32: 42,
alloc::vec::Vec<i32>: [
1,
2,
3,
],
},
},
}
注意,我在 map 中额外插入了一个 Vec<i32>
,以获得更明显的输出。