尝试在 Rust 中实现
ResourceId
抽象:
在编译任何内容之前,我想出了:
pub struct ResourceId<T: ResourceKind>([u8; T::SIZE], PhantomData<T>);
trait ResourceKind {
const SIZE: usize;
const PREFIX: &'static str;
}
impl<T: ResourceKind> Display for ResourceId<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let encoded_bytes = /* encode self.0 to a string */
write!(f, "{}_{}", T::PREFIX, encoded_bytes)
}
}
现在,让我们实现资源的资源 ID
Foo
:
pub struct Foo;
impl ResourceKind for Foo {
const SIZE: usize = 8;
const PREFIX: &'static str = "foo";
}
现在,在我的代码中,无论我想要在
Foo
资源上进行操作,我都想接受 ResourceId<Foo>
,以确保除“foo”之外的其他前缀或除“8”之外的其他字节大小的资源 ID 不会编译。
generic_const_exprs
。
我收到的错误:
error: generic parameters may not be used in const operations
--> packages/core_resource/lib.rs:9:45
|
9 | pub struct ResourceId<T: ResourceKind>([u8; T::SIZE], PhantomData<T>);
| ^^^^^^^ cannot perform const operation using `T`
|
= note: type parameters may not be used in const expressions
error: constant expression depends on a generic parameter
--> packages/core_resource/lib.rs:18:33
|
18 | let mut id = Self([0u8; T::SIZE], PhantomData);
| ^^^^^^^
|
= note: this may fail depending on what value the parameter takes
error: aborting due to 2 previous errors
我想到的其他选择:
ResourceId
更改为 ResourceId<T: ResourceKind, const SIZE: usize>
但这将有效地允许人们为具有多个可接受的字节大小的资源 R 定义一个 ID,因为它们会变得独立#[resource_id(prefix = "foo", size = 8) pub struct FooId;
;似乎是一个不错的选择,所以我希望得到一些反馈。在不依赖夜间编译器和
generic_const_exprs
的情况下,可接受且(希望)惯用的代码设计是什么?
一般来说,不同类型不具有相同属性的要求可以通过从该属性投影回类型来在类型级别上得到维持。 在您的情况下,人们可能会定义一个新特征,例如
ResourceKindOf
,它具有关联的类型 Kind
,然后要求 Kind
与预期类型匹配:
pub trait ResourceKindOf {
type Kind: ResourceKind;
}
pub trait ResourceKind {
type Id: ResourceKindOf<Kind = Self>;
type Prefix: ResourceKindOf<Kind = Self>;
}
当然,这要求所涉及的属性是 types 而不是常量,这让事情变得有点复杂,尤其是缺少
adt_const_params
功能,该功能使我们能够轻松创建包装我们选择的常量的类型.
用于
Id
的类型相当明显:[u8; Z]
,其中 Z
是之前的 SIZE
常量。 我们可以通过添加 PermissableId
约束来强制只允许符合这种模式的类型,其中 PermissableId
是我们仅针对 [u8; Z]
实现的密封特征:
mod sealed {
pub PermissableId {}
}
use sealed::PermissableId;
impl<const Z: usize> PermissableId for [u8; Z] {}
pub trait ResourceKind {
type Id: PermissableId + ResourceKind<Kind = Self>;
// etc
}
前缀有点难,因为任意字符串没有如此明显的类型级版本。 我能想到的最好办法就是使用字符列表:
struct Nil;
struct Cons<T, const C: char>(PhantomData<T>);
然后,例如,我们可以将类型级别
"foo"
设为 Cons<Cons<Cons<Nil, 'o'>, 'o'>, 'f'>
。 然而,这写起来非常可怕,所以让我们用宏让它(稍微)更容易:
macro_rules! cons {
() => {Nil};
($c:literal $($rest:tt)*) => {$crate::Cons<cons!($($rest)*), $c>};
}
现在我们可以改为编写
cons!('f''o''o')
来获取表示 "foo"
的类型级字符串。 令人烦恼的是,我们需要单独引用每个字符,但我想不出更好的方法(无论如何,没有 proc 宏)。
然后我们可以添加与之前相同的限制,以确保
Prefix
始终是这样的类型级字符串。 然而,在这种情况下,我们还希望能够在运行时将 cons-list 转换回渲染的字符串,所以让我们向我们的密封特征添加一个 fmt
方法:
mod sealed {
use std::fmt;
pub trait PermissablePrefix {
fn fmt(f: &mut fmt::Formatter<'_>) -> fmt::Result;
}
}
use sealed::PermissablePrefix;
impl PermissablePrefix for Nil {
fn fmt(_: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(())
}
}
impl<T: PermissablePrefix, const C: char> PermissablePrefix for Cons<T, C> {
fn fmt(f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_char(C)?;
T::fmt(f)
}
}
pub trait ResourceKind {
type Prefix: PermissablePrefix + ResourceKind<Kind = Self>;
// etc
}
最后,让我们通过创建一个宏来定义资源类型及其相关的特征实现,从而更轻松地使用所有这些:
macro_rules! resource_kinds {
($($kind:ident{$size:literal, $($prefix:tt)*})+) => {$(
pub struct $kind;
impl $crate::ResourceKind for $kind {
type Id = [u8; $size];
type Prefix = $crate::cons!($($prefix)*);
}
impl $crate::ResourceKindOf for [u8; $size] {
type Kind = $kind;
}
impl $crate::ResourceKindOf for $crate::cons!($($prefix)*) {
type Kind = $kind;
}
)+}
}
在游乐场一起观看这一切。