范型和替换
给定泛型类型MyType<A, B, ...>
,我们可能希望将泛型参数A, B, ...
替换为其他一些类型(可能是其他泛型参数或具体类型)。
我们在进行类型推断,类型检查和Trait求解时会做很多这类事情。
从概念上讲,在这些过程中,我们可能会发现一种类型等于另一种类型,并希望将一种类型换成另一种类型,依此类推,直到最终得到一些具体的类型(或错误) 。
在rustc中,这是使用我们上面提到的SubstsRef
完成的(“substs” = “substitutions”)。
从概念上讲,您可以认为SubstsRef
是一个替换ADT泛型类型参数的类型列表。
SubstsRef
是List<GenericArg<'tcx>>
的类型别名(请参阅rust文档中的List
)。
GenericArg
本质上是GenericArgKind
周围的节省空间的包装器,这是一个枚举,指示类型参数是哪种泛型(类型,生存期或const)。
因此,SubstsRef
在概念上类似于&tcx [GenericArgKind <'tcx>]
切片(但它实际上是一个List
)。
那么为什么我们使用这种List
类型而不是真正的slice呢?
它的长度是“内连”的,因此&List
仅为32位。
结果,它不能被切片(仅在长度超出范围时才起作用)。
这也意味着您可以通过==
来检查两个List
的相等性(对于普通切片是不可能的)。
正是因为它们从不代表“子列表”,而仅代表完整的“列表”,该列表已被散列和interned。
综上所述,让我们回到上面的示例:
struct MyStruct<T>
MyStruct
会有一个AdtDef
(和相应的DefId
)。T
会有一个TyKind::Param
(以及相应的DefId
)(稍后再介绍)。- 将有一个包含列表
[GenericArgKind::Type(Ty(T))]
的SubstsRef
。- 这里的
Ty(T)
是对ty::Ty
的简写,其中有TyKind::Param
,我们在之前提到过这一点。
- 这里的
- 这是一个
TyKind::Adt
,其中包含MyStruct
的AdtDef
和上面的SubstsRef
。
最后,我们将快速提到Generics
类型。 它用于提供某个类型的类型参数的信息。
替换前的范型
因此,回想一下,在我们的示例中,MyStruct
结构具有范型T
。
例如,当我们对使用MyStruct
的函数进行类型检查时,我们将需要能够在不真正知道T
是什么的情况下引用该类型T
。
总的来说,在所有泛型定义中都是如此:我们需要能够处理未知类型。 这是通过TyKind::Param
(我们在上面的示例中提到的)完成的。
每个TyKind::Param
都包含两个字段:名称和索引。
通常,索引完全定义了参数,并且大多数代码都使用该索引。
名称则包含在调试打印输出中。
这么做有两个原因。
首先,索引很方便,它使您可以在替换时将其包含在通用参数列表中。
其次,索引鲁棒性更强。 例如,原则上可以有两个使用相同名称的不同类型参数,例如 impl<A> Foo<A> { fn bar<A>() { .. } }
,尽管禁止阴影的规则使此操作变得困难(但是将来这些语言规则可能会更改)。
类型参数的索引是一个整数,指示其在类型参数列表中的顺序。 此外,我们认为该列表包括来自外部作用域的所有类型参数。 考虑以下示例:
struct Foo<A, B> {
// A would have index 0
// B would have index 1
.. // some fields
}
impl<X, Y> Foo<X, Y> {
fn method<Z>() {
// inside here, X, Y and Z are all in scope
// X has index 0
// Y has index 1
// Z has index 2
}
}
当我们在泛型定义中工作时,我们将像其他TyKind
一样使用TyKind::Param
。
毕竟这只是一种类型。
但是,如果我们想在某个地方使用范型,那么我们将需要进行替换。
例如,假设前面示例中的Foo <A, B>
类型的字段为Vec<A>
。
请注意,Vec
也是通用类型。
我们要告诉编译器,应将Vec
的类型参数替换为Foo<A,B>
的A
类型参数。我们通过替换来做到这一点:
struct Foo<A, B> { // Adt(Foo, &[Param(0), Param(1)])
x: Vec<A>, // Adt(Vec, &[Param(0)])
..
}
fn bar(foo: Foo<u32, f32>) { // Adt(Foo, &[u32, f32])
let y = foo.x; // Vec<Param(0)> => Vec<u32>
}
这个例子有一些不同的替代:
- 在
Foo
的定义中,在字段x
的类型中,将Vec
的类型参数替换为Param(0)
,即Foo<A, B>
的第一个参数,因此x
的类型是Vec <A>
。 - 在函数
bar
上,我们指定要使用Foo<u32, f32>
。这意味着我们将用u32
和f32
替换Param(0)
和Param(1)
。 - 在
bar
的函数体中,我们访问foo.x
,其类型为Vec<Param(0)>
,但Param(0)
已经被替换为u32
,因此,foo.x
的类型为Vec<u32>
。
让我们更仔细地看看最后的替换方法,以了解为什么使用索引。如果要查找foo.x
的类型,则可以获取x的范型,即Vec<Param(0)>
。
现在我们可以使用索引0
,并使用它来查找正确的类型替换:查看Foo
的SubstsRef
,我们有列表[u32, f32]
,
因为我们要替换索引0
,我们采用此列表的第0个索引,即u32
。然后就好了!
您可能有几个后续问题……
type_of
我们如何获得x
的范型?您可以通过 tcx.type_of(def_id)
查询获得几乎所有类型的东西,在这种情况下,我们将传递字段x
的DefId
。
type_of
查询总是返回带有定义范围内的泛型的定义。
例如,tcx.type_of(def_id_of_my_struct)
将返回MyStruct
的“自视图”:Adt(Foo, &[Param(0), Param(1)])
。
subst
我们如何实际地进行替换?也有一个用来这么做的函数!您可以使用subst
将SubstRef
替换为其他类型的列表。
这里是在编译器中实际使用subst
的示例。
确切的细节并不是太重要,但是在这段代码中,我们碰巧将其从rustc_hir::Ty
转换为真实的ty::Ty
。
您可以看到我们首先得到了一些替换(substs
)。然后我们调用type_of
来获取类型,并调用ty.subst(substs)
来获得新的ty
类型,并进行替换。
关于索引的注释:Param
中的索引可能与我们期望的不匹配。
例如,索引可能超出范围,或者可能是我们期望类型时却得到了一个生命周期的索引。
从rustc_hir::Ty
转换为ty::Ty
时或者更早,编译器会捕获这些错误。
如果它们在那以后发生,那就是编译器错误。