本文简介
早期和晚期限定的变量
在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
、'b
和T
。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
意味着一个有索引N
的ParamTy
在rustc
中,泛型结构携带这一信息。因此,Bar上面的泛型就像U一样,会表示Foo
的“父”泛型,它声明了Self
和T
。你可以在这一章中阅读更多内容。
后期限定参数
在 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>
的类型,基本上相当于用'foo
和unit
和来搜索和替换'l
和T
,所以最后类似于:
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
一个整型int
或char
型的切片,但不能两者都传递。
换句话说,当你指定像|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<_>) { ... }
这样更连贯,更清晰。