early bound 和 late bound

 

本文简介

本文简介

早期和晚期限定的变量

在Rust中,项定义(如fn)通常具有泛型参数,这些参数总是被普遍量化。也就是说,如果你有一个函数

fn foo<T>(x: T) { }

这个函数被定义为“针对所有T”(而不是“针对某些特定的T”,这将是存在量化的)。

虽然Rust项可以根据类型、生命期和常量进行量化,但Rust中的值类型只能按生命期进行量化。所以你可以有一个类型,比如for<'a> fn(&'a u32),它代表一个函数指针,它接受一个具有任何生命周期的引用,或者for<'a> dyn Trait<'a>,它是一个dyn Trait,用于实现任何生命周期的Trait;但是我们没有像for<T> fn(T)那样的类型,它是一个接受任何类型的值作为参数的函数。这是单态化的结果——为了支持for<T> fn(T)的类型值,我们需要一个可以用于任何类型的形参的函数指针,但是在Rust中,我们为每个形参类型生成定制的代码。

这种不对称性的一个后果是,我们在重新表示一些通用类型时出现了一个奇怪的分裂:早期绑定参数和后期绑定参数。基本上,如果我们不能表示一个类型(例如一个普遍量化的类型),我们就必须提前绑定它,这样无法表示的类型就永远不会出现。

考虑下面的例子:

fn foo<'a, 'b, T>(x: &'a u32, y: &'b T) where T: 'b {...}

我们不能以同样的方式对待'a'bT。Rust中的类型不能有for<T> { .. },只能有for<'a> {...},所以每当你引用foo时,你得到的类型不能是for<'a,'b,T> fn(&'a u32,y: &'b T)。相反,T必须提前替换代入。特别是,你有:

let x = foo; // T, 'b 必须在这里替换
x(...);     // 'a 在调用点这里被替换
x(...);     // 'a 在这哪用不同的值替换

早期限定的参数

rustc中的早期绑定参数由一个索引来标识,对于类型来说,这个索引存储在ParamTy结构中;对于生命期来说,这个索引存储在EarlyBoundRegion结构中。索引从作用域中最外层的声明开始计算。这意味着,当你在里面添加更多的绑定者时,索引不会改变。 比如:

trait Foo<T> {
  type Bar<U> = (Self, T, U);
}

这里,类型(Self, T, U)会是($0, $1, $2)$N意味着一个有索引NParamTy

rustc中,泛型结构携带这一信息。因此,Bar上面的泛型就像U一样,会表示Foo的“父”泛型,它声明了SelfT。你可以在这一章中阅读更多内容。

后期限定参数

rustc 中,后期限定参数的处理方式完全不同(它们也是专门针对生命期参数的,因为现在只支持后期限定的生命期参数,不过有了 GATs,这一点必须改变)。我们通过一个Binder类型来表示它们的潜在存在。Binder不知道该绑定级别有多少个变量。这只能通过走类型本身并收集它们来确定。所以像for<'a,'b>('a,'b)这样的类型就是for(^0.a,^0.b)。这里,我们只写for,因为我们不知道里面绑定的东西的名字。

而且,对后期绑定生存期的引用写成^0.a

  • 0是索引;它标识这个生命周期被绑定在最里面的绑定器Binder(for)中。
  • a为“名称”;rustc中的后期绑定生存期由一个“名称”(BoundRegionKind enum)标识。该enum可以包含DefId,也可以包含各种“匿名”编号名称。后者来自于fn(&u32, &u32)这样的类型,它相当于for<'a,'b> fn(&'a u32, &'b u32)这样的类型,但必须生成这些生命周期的名称。 这种在绑定级别不知道全部变量集的设置有一些优点,也有一些缺点。缺点是您必须遍历该类型,以找出在给定级别上绑定的内容,等等。其主要优点是,当从Rust语法构造类型时,如果遇到像fn(&u32)这样的匿名区域,我们只需创建一个新的索引,而不必更新绑定器。

混合参数列表

在一般情况下,生存期参数的工作方式与类型参数非常相似。例如,考虑struct,它有一个生存期参数和一个类型参数:VecIndex<'l, T>

struct VecIndex<l, T> {
  vec: &'l [T],
  index: unit
}

现在,如果我写一个类似于VecIndex<'foo, unit>的类型,基本上相当于用'foounit和来搜索和替换'lT,所以最后类似于:

struct VecIndex {
  vec: &'foo [unit],
  index: unit
}

然而,对于函数来说,生存期参数比类型参数更灵活。特别地,我们通常可以等到函数被调用时才指定生存期形参的值。要了解我的意思,请考虑函数get_index,它同样由生命周期'l和类型T参数化:

fn get_index<'l, T>(v: &'l [T], index: unit) -> &'l T {
  &v[index]
}

现在假设我要调用get_index两次,并提供两种不同类型的向量作为输入:

let vec1 = [1, 2, 3];
let addr1 = get_index(vec1, 1);

let vec2 = ['1', '2', '3'];
let addr2 = get_index(vec2, 1);

虽然它们看起来像是在调用同一个函数,但实际上这两个对 get_index 的调用在运行时执行的是完全不同的代码。这是因为Rust使用了一个单态化方案来处理类型参数(类似于C++),这意味着我们必须为每一个类型集创建一个重复的get_index副本。换句话说,在幕后,每一个对get_index的引用都必须指定一组具体的类型参数(尽管编译器通常会为我们推断它们的值)。因此,我们可以用一种更明确的方式重写上面的代码示例(现在,忽略生命期参数,我后面将会提到它):

let vec1 = [1, 2, 3];
let addr1 = get_index::<int>(vec1, 1);

let vec2 = ['1', '2', '3'];
let addr2 = get_index::<char>(vec2, 1);

单态化通常是不可见的,但如果你试图获得get_index的函数指针,它就会变得可见:

let func = get_index::<?>; // 这里必须选择int或char

let vec1 = [1, 2, 3];
let addr1 = get_index::<int>(vec1, 1);

let vec2 = ['1', '2', '3'];
let addr2 = get_index::<char>(vec2, 1);

可以看到,当我们将get_index存储到一个变量中时,我们必须指定它将操作的类型。所以我们可以传递给func一个整型intchar型的切片,但不能两者都传递。

换句话说,当你指定像|uint| -> float这样的闭包类型时,我们不允许该类型对于类型来说是泛型的。也就是说,您不能拥有像<T> fn(T) -> uint这样的类型,这是一个将任何类型转换为uint的函数(我使用的是最近提出的新语法)。即使你定义一个泛型函数(比如fn foo<T>(t: T) ->unit),在任何时间点,对该函数的引用的类型都会有一些具体的类型来替代T. 我将类型参数称为早绑定,这意味着我们将尽早用具体的值代替它们。

一直以来,我都忽略了生命周期参数。因为生命周期在运行时被删除,这意味着它们不会影响我们生成的代码,所以它们不必共享相同的限制。事实上,我们确实允许绑定生命期名称的闭包,也就是说,我可以写一个类似<'a> fn(&'a [uint], uint) -> &'a T这样的类型(也就是说,一个闭包接受一个生命期为'a的切片,并返回一个生命为'a的指针–但每次调用该闭包时,生命期'a可以不同)。我将把这个称为后期绑定的生命期参数,因为我们不需要马上替换一个特定的生命期,而是可以等到函数被调用时再替换。

让我们根据这一区别重新讨论get_index

fn get_index<'l, T>(v: &'l [T], index: unit) -> &'l T {
  &v[index]
}

如前所述,类型参数T(像所有类型参数一样)必须是早期绑定的。例如,如果不知道T,我们就无法实际生成v[index]的代码–我们不知道T有多大,从而不知道要跳过多少字节。然而,对于生命期'l,就没有问题了。当我们生成get_index的代码时,'l代表什么生命期将不重要,我们只需生成一串代码,它获取一个切片,并索引–类型系统保证我们这个解引用不会崩溃,但并不影响我们生成实际做解引用的代码。因此,'l可以是晚期绑定。例如,我们可以说,get_index::<int>这样的表达式的类型是<'l> fn(&'l [uint]) -> &'l uint

并不是所有的生命期参数都可以后期绑定。例如,类型上的生命期参数是早期绑定的。为了理解我的意思,考虑一个像Foo这样的结构体:

struct Foo<'a> {x: &'a int}

你不能在不指定'a是什么的情况下引用一个类型Foo(好吧,在某些情况下你可以写Foo,但编译器会插入默认值或利用推理将其支出到Foo<'z>的某个生命期'z)。

我曾经以为这就是当时的鸿沟:类型上的生命期参数是早期约束,fns上的生命参数是后期约束。但我意识到,这不一定是真的。考虑一个类似下面的函数:

trait Allocator<'arena> {
  fn new_box(&mut self) -> &'arena mut Box;
}

fn with_alloc<'arena, A: Allocator<'arena>> (
  alloc: &mut A,
  ...)
{
  ...
}

这里重要的是类型参数A有一个引用生存期参数'arena的绑定。这意味着,除非我们知道'arena,否则我们无法知道A。由于A是类型形参,因此是早期绑定,这意味着“'arena也必须是早期绑定”。

因此,我们可以看到,fns的生命期参数可能是早期或晚期绑定。不幸的是,我们当前的语法没有提供区分早期和晚期绑定生存期的方法。更糟糕的是,我们目前要求所有的生命周期都位于参数列表的前面(讽刺的是,这是因为我希望它们能够出现在trait绑定内,如最后一个例子所示,但我没有考虑到这一要求的全部含义)。

我建议的解决方案

我认为我们应该允许类型和生命周期参数自由混合。 此外,我们应该要求在为泛型参数指定值时,必须始终在正确的位置同时指定生存期和类型参数。 为了减轻繁琐的工作,我们将添加一个新的说明符_,该说明符_可以用来省略生命周期/类型参数,并让编译器填写默认值。 _可以用来为类型或区域参数提供一个值。

前面规则的一个例外:在fn项目上,任何尾随生命周期都将被视为后期绑定。后期绑定的生命周期可以(但不需要)在引用fn时指定。如果忽略了后期绑定生存期的值,则在结果fn类型中绑定生存期。这个约定保留了为类型添加后期绑定寿命的空间,一旦某种语义被定义出来,就可以这样做。

这是相当密集的。让我举几个例子。首先,我们之前看到的get_index函数:

fn get_index<'l, T>(v: &'l [T], index: unit) -> &'l T {
  &v[index]
}

这个函数的编写方式是,'l被认为是早期绑定,因为它位于类型参数T之前。这意味着下面我之前编写的代码现在是非法的。

let func = get_index::<int>

这段代码是非法的,因为有两个早期绑定的参数('l, T),代码只为其中一个提供一个值。用户可以提供一个命名的生存期,例如:

fn foo<'a>(v: &'a [int]) -> &'a int {
  let func = get_index::<'a, int>;
  func(v, 0)
}

或者,如果他们愿意允许编译器使用推断,他们可以直接编写_

fn foo<'a>(v: &'a [int]) -> &'a int {
  let func = get_index::<_, int>;
  func(v, 0)
}

事实上,可以为两个参数写_:

fn foo<'a>(v: &'a [int]) -> &'a int{
    let func = get_index::<_, _>;
    func(v, 0)
}

所有这些例子都是等价的。 对于这个get_index的定义,用户不能做的是将func应用于两个不同生存期的切片。例如:

fn bar<'a, 'b>(v: &'a [int], w: &'b [int]) {
  let fun = get_index::<_, _>;
  let x: 'a int = func(v, 0);  // 推断生命期 'l 为 'a
  let y: 'b int = func(w, 0);   // 错误: `l是'a, 不是'b
}

要实现这一点,形参'l必须是后期绑定的,这意味着它必须移动到列表的末尾:

fn get_index_late<T, 'l>(v: &'l [T], index: uint) -> &'l T {
    &v[index]
}

现在,由于'l是后期绑定,因此可以将其完全排除在参数列表之外,而只需引用get_index_late::<int>

fn bar<'a, 'b>(v: &'a [int], w: &'b [int]) {
  let func = get_index::<int>;
  let x: 'a int = func(v, 0);
  let y: 'b int = func(w, 0);
}

这里的func类型是<'l> fn(&'l [int])->&'l int –即'l保持绑定,因此当多次调用该函数时,可以赋予不同的值。 如果我们回到需要早期绑定的生命周期参数的例子,我们会看到一切都很顺利,因为生命周期参数“'arena”出现在列表的第一个,因此是早期绑定的

trait Allocator<'arena> {
    fn new_box(&mut self) -> &'arena mut Box;
}

fn with_alloc<'arena,A:Allocator<'arena>>(
    alloc: &mut A,
    ...)
{
    ...
}

作用域规则将防止像下面这样的函数定义,它试图引用类型绑定内的后期绑定的生存期参数:

fn with_alloc_bad<A: Allocator<'arena>, 'arena>(
  // 注意'arena出现在第二个参数上
  alloc: &mut A,
  ...)
{
  ...
}

请注意,_符号可以在其他类型的上下文中使用,也可以用作方便的速记。 所以我可以这样写一条语句:

let v: ~[_] = vec.iter().map(...).collect();

这个表达式指定v的类型是某种拥有的向量,但不指定内容(假定可以推断)。现在,人们可以完全省略一种类型,也可以详细说明它。此功能已被独立要求作为问题#9508。

这个表达式指定v的类型是某种自有向量,但没有指定内容(假定可以推断出内容)。现在,我们既可以省略一个类型的全部内容,或者将其指定到最详细的细节。这个功能已经作为#9508问题被独立要求。

用于类型的_参数仅在函数体内是合法的,用于生命期的_参数在函数主体或函数签名中都是合法的:在签名中,它的意思是“一个新的生命周期参数”。(作为扩展,我们可能使_对类型具有相同的含义)

通过这个更改,我建议将类型上的生存期参数设置为强制性的。今天,它们在类型签名中是强制性的,但在fn签名或fn体中不是。如果在这些上下文中省略,它们将默认为一个新的生命周期。使用这个建议,我们可以简单地写_代替。所以代码

struct MyMap<'a> { ... }
fn foo(x: &MyMap) { ... }

可以改为:

struct MyMap<'a> { ... }
fn foo(x: &MyMap<_>) { ... }

这样更连贯,更清晰。