[英]Generic struct over a generic type without type parameter
有可能在 Rust 中做这样的事情吗?
trait Foo<T> {}
struct A;
struct B;
struct Bar<T: Foo> {
a: T<A>,
b: T<B>
}
我知道我可以只为Bar
使用两个参数,但我认为必须有更好的方法来做到这一点。
我想实现一个Graph
结构。 因为我不能只将节点和边绑定到他们的父母生命周期,所以我想要像Rc
这样的东西。 但是,有时可能需要一个可以从多个线程访问的Graph
。 所以我必须同时实现Rc
和Arc
。
这就是Foo
的优点:我为Rc
和Arc
实现了Foo
( Foo
需要Deref
)并且我使用绑定到Foo
的参数T
。 这就是我想要为单线程和多线程使用一个结构的方式。
⇒ 这目前无法在 Rust 的类型系统中表达 ☹
幸运的是,由于本 RFC 中提出的“通用关联类型”,未来将成为可能。 您可以在相应的跟踪问题中跟踪实施和稳定状态。
这里最重要的词是“HKT”(H igherķinded牛逼YPES)。 这是尚未在 Rust 中实现的类型系统的一个特性。 Haskell 提供 HKT。 在 C++ 世界中,HKT 被称为“模板模板”。 上面提到的泛型关联类型也是 HKT 的一种形式。
让我们慢慢开始:我们所知道的简单类型是什么? 让我们列出一些类型: i32
、 bool
、 String
。 这些都是类型……您可以拥有这些类型的值(变量)。 Vec<i32>
呢? 这也是一种简单的类型! 你可以有一个Vec<i32>
类型的变量,没问题!
我们想将这些类型组合在一起; 我们称这种分类为“某种类型”。 如果我们想以一种非常抽象的方式(关于类型的类型)进行讨论,我们会选择其他词,在这种情况下是kind 。 甚至还有种类型的符号。 对于上面的简单类型,我们说:这些类型的种类是
*
是的,只是一颗星星,很容易。 符号在以后更有意义!
让我们搜索与简单类型不同的类型。 Mutex<HashMap<Vec<i32>, String>>
? 不,它可能相当复杂,但它仍然是那种*
并且我们仍然可以拥有这种类型的变量。
Vec
呢? 是的,我们省略了尖括号。 是的,这确实是另一种类型! 我们可以有一个Vec
类型的变量吗? 不! 什么的向量?!
这种捐赠方式为:
* -> *
这只是说:给我一个普通类型( *
),我将返回一个普通类型! 给这个东西( Vec
)一个普通类型i32
,它会返回一个普通类型Vec<i32>
! 它也称为类型构造函数,因为它用于构造类型。 我们甚至可以更进一步:
* -> * -> *
这有点奇怪,因为它与柯里化有关,对于非 Haskell 程序员来说读起来很奇怪。 但这意味着:给我两种类型,我将返回一种类型。 让我们考虑一个例子...... Result
! 在您提供两个具体类型A
和B
之后Result<A, B>
Result
类型构造函数将返回一个具体类型Result<A, B>
。
术语更高级的类型只是指所有不是*
的类型,它们是类型构造函数。
当您编写struct Bar<T: Foo>
您希望T
是* -> *
,这意味着:您可以给T
一个类型并接收一个简单类型。 但正如我所说,这在 Rust 中还不能表达。 要使用类似的语法,人们可能会想象这在未来可以工作:
// This does NOT WORK!
struct Bar<for<U> T> where T<U>: Foo {
a: T<A>,
b: T<B>,
}
for<>
语法是从"higher-ranked trait bounds" (HRTB)借来的,它现在可用于对生命周期进行抽象(最常用于闭包)。
如果您想阅读有关此主题的更多信息,请访问以下链接:
奖励:万一将实现关联类型构造函数,您的问题的解决方案(我认为,因为没有办法测试)!
我们必须在我们的实现中绕道而行,因为 RFC 不允许将Rc
作为类型参数直接传递。 可以这么说,它没有直接介绍HKT。 但是正如 Niko 在他的博客文章中所说的那样,我们可以通过使用所谓的“家族特征”,获得与具有关联类型构造函数的 HKT 相同的灵活性和功能。
/// This trait will be implemented for marker types, which serve as
/// kind of a proxy to get the real type.
trait RefCountedFamily {
/// An associated type constructor. `Ptr` is a type constructor, because
/// it is generic over another type (kind * -> *).
type Ptr<T>;
}
struct RcFamily;
impl RefCountedFamily for RcFamily {
/// In this implementation we say that the type constructor to construct
/// the pointer type is `Rc`.
type Ptr<T> = Rc<T>;
}
struct ArcFamily;
impl RefCountedFamily for ArcFamily {
type Ptr<T> = Arc<T>;
}
struct Graph<P: RefCountedFamily> {
// Here we use the type constructor to build our types
nodes: P::Ptr<Node>,
edges: P::Ptr<Edge>,
}
// Using the type is a bit awkward though:
type MultiThreadedGraph = Graph<ArcFamily>;
有关更多信息,您应该真正阅读 Niko 的博客文章。 困难的话题解释得足够好,甚至我都能或多或少地理解它们!
编辑:我刚刚注意到 Niko 在他的博客文章中实际上使用了Arc
/ Rc
示例! 我完全忘记了这一点,我自己想到了上面的代码……但也许我的潜意识还记得,因为我和 Niko 一样选择了几个名字。 无论如何,这是他(可能更好)对这个问题的看法。
在某种程度上,Rust确实看起来很像 HKT(请参阅 Lukas 的回答以了解它们是什么),尽管有一些可以说是笨拙的语法。
首先,您需要为所需的指针类型定义接口,这可以使用泛型特征来完成。 例如:
trait SharedPointer<T>: Clone {
fn new(v: T) -> Self;
// more, eg: fn get(&self) -> &T;
}
加上一个泛型特性,它定义了一个关联类型,它是你真正想要的类型,它必须实现你的接口:
trait Param<T> {
type Pointer: SharedPointer<T>;
}
接下来,我们为我们感兴趣的类型实现该接口:
impl<T> SharedPointer<T> for Rc<T> {
fn new(v: T) -> Self {
Rc::new(v)
}
}
impl<T> SharedPointer<T> for Arc<T> {
fn new(v: T) -> Self {
Arc::new(v)
}
}
并定义一些实现上述Param
特征的虚拟类型。 这是关键部分; 我们可以有一个类型 ( RcParam
) 为任何T
实现Param<T>
,包括能够提供一个类型,这意味着我们正在模拟一个更高级的类型。
struct RcParam;
struct ArcParam;
impl<T> Param<T> for RcParam {
type Pointer = Rc<T>;
}
impl<T> Param<T> for ArcParam {
type Pointer = Arc<T>;
}
最后我们可以使用它:
struct A;
struct B;
struct Foo<P: Param<A> + Param<B>> {
a: <P as Param<A>>::Pointer,
b: <P as Param<B>>::Pointer,
}
impl<P: Param<A> + Param<B>> Foo<P> {
fn new(a: A, b: B) -> Foo<P> {
Foo {
a: <P as Param<A>>::Pointer::new(a),
b: <P as Param<B>>::Pointer::new(b),
}
}
}
fn main() {
// Look ma, we're using a generic smart pointer type!
let foo = Foo::<RcParam>::new(A, B);
let afoo = Foo::<ArcParam>::new(A, B);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.