范型和替换

给定泛型类型MyType<A, B, ...>,我们可能希望将泛型参数A, B, ...替换为其他一些类型(可能是其他泛型参数或具体类型)。 我们在进行类型推断,类型检查和Trait求解时会做很多这类事情。 从概念上讲,在这些过程中,我们可能会发现一种类型等于另一种类型,并希望将一种类型换成另一种类型,依此类推,直到最终得到一些具体的类型(或错误) 。

在rustc中,这是使用我们上面提到的SubstsRef完成的(“substs” = “substitutions”)。 从概念上讲,您可以认为SubstsRef 是一个替换ADT泛型类型参数的类型列表。

SubstsRefList<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,其中包含MyStructAdtDef和上面的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>。这意味着我们将用u32f32替换Param(0)Param(1)
  • bar的函数体中,我们访问foo.x,其类型为Vec<Param(0)>,但Param(0)已经被替换为u32,因此,foo.x 的类型为Vec<u32>

让我们更仔细地看看最后的替换方法,以了解为什么使用索引。如果要查找foo.x的类型,则可以获取x的范型,即Vec<Param(0)>。 现在我们可以使用索引0,并使用它来查找正确的类型替换:查看FooSubstsRef,我们有列表[u32, f32], 因为我们要替换索引0 ,我们采用此列表的第0个索引,即u32。然后就好了!

您可能有几个后续问题……

type_of 我们如何获得x的范型?您可以通过 tcx.type_of(def_id) 查询获得几乎所有类型的东西,在这种情况下,我们将传递字段xDefIdtype_of查询总是返回带有定义范围内的泛型的定义。 例如,tcx.type_of(def_id_of_my_struct)将返回MyStruct的“自视图”:Adt(Foo, &[Param(0), Param(1)])

subst 我们如何实际地进行替换?也有一个用来这么做的函数!您可以使用substSubstRef替换为其他类型的列表。

这里是在编译器中实际使用subst的示例。 确切的细节并不是太重要,但是在这段代码中,我们碰巧将其从rustc_hir::Ty转换为真实的ty::Ty。 您可以看到我们首先得到了一些替换(substs)。然后我们调用type_of来获取类型,并调用ty.subst(substs)来获得新的ty类型,并进行替换。

关于索引的注释:Param中的索引可能与我们期望的不匹配。 例如,索引可能超出范围,或者可能是我们期望类型时却得到了一个生命周期的索引。 从rustc_hir::Ty转换为ty::Ty时或者更早,编译器会捕获这些错误。 如果它们在那以后发生,那就是编译器错误。