原文链接:https://lucumr.pocoo.org/2022/1/6/rust-extension-map/

翻译:trdthg

选题:trdthg

本文由 Rustt 翻译,StudyRust 荣誉推出

拓展 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_refdowncast_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>,以获得更明显的输出。