高级 The Rust Project Developers 著,rustwiki.org 中译 2026-09-13 14:59:08 · 0 阅读

第11章 类型系统

类型

Rust 程序中的每个变量、程序项和值都有一个类型。类型定义了该如何解释用于保存它的内存数据,以及定义了可以对该值执行的操作。

内置的类型以非平凡的方式(in nontrivial ways)紧密地集成到语言中,这种方式是不可能在用户定义的类型中模拟的。用户定义的类型功能有限。

内置类型列表:

类型表达式

句法
Type :
      TypeNoBounds
   | ImplTraitType
   | TraitObjectType

TypeNoBounds :
      ParenthesizedType
   | ImplTraitTypeOneBound
   | TraitObjectTypeOneBound
   | TypePath
   | TupleType
   | NeverType
   | RawPointerType
   | ReferenceType
   | ArrayType
   | SliceType
   | InferredType
   | QualifiedPathInType
   | BareFunctionType
   | MacroInvocation

上表中的 Type 文法规则中定义的各种类型表达式都是某个具体类型的句法产生式。它们可以覆盖指代:

圆括号组合类型

ParenthesizedType :
   ( Type )

在某些情况下,类型组合在一起时可能会产生二义性。此时可以在类型周围使用元括号来避免歧义。例如,引用类型类型约束列表中的 +运算符搞不清楚其左值的边界位置在哪里,因此需要使用圆括号来明确其边界。这里需要的消歧文法就是使用 TypeNoBounds 句法规则替代 Type 句法规则。


#![allow(unused)]
fn main() {
use std::any::Any;
type T<'a> = &'a (dyn Any + Send);
}

递归类型

标称类型(nominal types) — 结构体(struct)枚举(enum)联合体(union) — 可以是递归的。也就是说,每个枚举(enum)变体或结构体(struct)或联合体(union)的字段可以直接或间接地指向此枚举(enum)或结构体(struct)类型本身。这种递归有一些限制:

  • 递归类型必须在递归中包含一个标称类型(不能仅是类型别名或其他结构化的类型,如数组元组)。因此不允许使用 type Rec = &'static [Rec]
  • 递归类型的尺寸必须是有限的;也就是说,类型的递归字段必须是指针类型
  • 递归类型的定义可以跨越模块边界,但不能跨越模块可见性边界或 crate 边界(为了简化模块系统和类型检查)。

递归类型及使用示例:


#![allow(unused)]
fn main() {
enum List<T> {
    Nil,
    Cons(T, Box<List<T>>)
}

let a: List<i32> = List::Cons(7, Box::new(List::Cons(13, Box::new(List::Nil))));
}

布尔型


#![allow(unused)]
fn main() {
let b: bool = true;
}

布尔型布尔数是一种可以为*真(true)假(false)*的原语数据类型。

这种类型的值可以使用字面量表达式创建,使用关键字 truefalse 来表达对应名称的值。

该类型是此语言的预导入包的一部分,使用名称 bool 来表示。

布尔型的对象尺寸和对齐量均为1。false 的位模式为 0x00, true 的位模式为 0x01。其他的任何其他位模式的布尔型的象都是未定义的行为

布尔型是多种表达式的操作数的类型:

注意:布尔型的行为类似于枚举类型,但它确实不是枚举类型。在实践中,这主要意味着构造函数不与类型相关联(例如没有 bool::true 这种写法)。

和其他所有的原语类型一样,布尔型实现CloneCopySizedSendSync 这些 traits

注意: 参见标准库文档中的相关操作运算。

布尔运算

当使用带有布尔型的操作数的特定操作符表达式时,它们使用[布尔逻辑规则][boolean logic]进行计算。

逻辑非

b!b
truefalse
falsetrue

逻辑或

aba | b
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

逻辑与

aba & b
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

逻辑异或

aba ^ b
truetruefalse
truefalsetrue
falsetruetrue
falsefalsefalse

比较

aba == b
truetruetrue
truefalsefalse
falsetruefalse
falsefalsetrue
aba > b
truetruefalse
truefalsetrue
falsetruefalse
falsefalsefalse
  • a != b 等同于 !(a == b)
  • a >= b 等同于 a == b | a > b
  • a < b 等同于 !(a >= b)
  • a <= b 等同于 a == b | a < b

数字型

整型/整数类型

无符号整数类型:

类型最小值最大值
u8028-1
u160216-1
u320232-1
u640264-1
u12802128-1

有符号二进制补码整型包括:

类型最小值最大值
i8-(27)27-1
i16-(215)215-1
i32-(231)231-1
i64-(263)263-1
i128-(2127)2127-1

浮点型

Rust 对应 IEEE 754-2008 的“binary32”和“binary64”浮点类型分别是 f32f64

和计算平台相关的整型

usize类型是一种无符号整型,其宽度与平台的指针类型的宽度相同。它可以表示进程中的每个内存地址。

isize类型是一种有符号整型,其宽度与平台的指针类型的宽度相同。(Rust 规定)对象的尺寸和数组的长度的理论上限是 isize 的最大值。这确保了 isize 可以用来计算指向对象或数组内部的指针之间的差异,并且可以寻址对象中的每个字节以及末尾之后的那个字节。

usizeisize 的宽度至少是 16-bits。

注意:许多 Rust 代码可能会假设指针、usizeisize 是 32-bit 或 64-bit 的。因此,16-bit 指针的支持是有限的,这部分支持可能需要来自库的明确关注和确认。

文本类类型

类型 charstr 用于保存文本数据。

字符型(char)的值是 Unicode 标量(scalar)值(即不是代理项(surrogate)的代码点),可以表示为 0x0000~0xD7FF 或 0xE000~0x10FFFF 范围内的 32-bit 无符号字符。创建超出此范围的字符直接触发未定义行为(Undefined Behavior)。一个 [char] 实际上是长度为1的 UCS-4 / UTF-32 字符串。

str类型的值的表示方法与 [u8] 相同,它是一个 8-bit 无符号字节类型的切片。但是,Rust 标准库对 str 做了额外的假定:str 上的方法会假定并确保其中的数据是有效的 UTF-8。调用 str 的方法来处理非UTF-8 缓冲区上的数据可能或早或晚地出现未定义行为

由于 str 是一个动态尺寸类型,所以它只能通过指针类型实例化,比如 &str

never 类型

句法
NeverType : !

never类型(!)是一个没有值的类型,表示永远不会完成计算的结果。! 的类型表达式可以强转为任何其他类型。

let x: ! = panic!();
// 可以强转为任何类型
let y: u32 = x;

注意: never类型原本预计在1.41中稳定下来,但由于最后一分钟检测到一些意想不到的回归,该类型的稳定进程临时暂停。目前 !类型只能出现在函数返回类型中。有关详细信息,请参阅议题跟踪

元组类型

句法
TupleType :
      ( )
   | ( ( Type , )+ Type? )

元组类型是由其他类型的异构列表组合成的一类结构化类型1

元组类型的语法规则为一对圆括号封闭的逗号分割的类型列表。 为和圆括号类型区分开来,一元元组的元素类型后面需要有一个逗号。

元组类型的字段数量等同于其封闭的异构类型列表的长度。 字段的数量决定元组的元数(arity)。 有 n 个字段的元组叫做 n元元组(n-ary tuple)。 例如,有两个字段的元组就是二元元组。

元组的字段用它在列表中的位置数字来索引。 第一个字段索引为 0。 第二个字段索引为 1。 然后以此类推。 每个字段的类型都是元组类型列表中相同位置的类型。

出于方便和历史原因,不带元素(())的元组类型通常被称为单元(unit)单元类型(unit type)。 它的值也被称为单元单元值

元组类型的示例:

  • () (单元)
  • (f64, f64)
  • (String, i32)
  • (i32, String) (跟前一个示例类型不一样)
  • (i32, f64, Vec<String>, Option<bool>)

这种类型的值是使用元组表达式来构造的。 此外,如果没有其他有意义的值可供求得/返回,很多种表达式都将生成单元值。 元组字段可以通过元组索引表达式模式匹配来访问。

1

结构化类型的特点就是其内部对等位置的类型如果是相等的,那么这些结构化类型就是相等的。有关元组的标称类型版本,请参见元组结构体

数组类型

句法
ArrayType :
   [ Type ; Expression ]

数组是 N 个类型为 T 的元素组成的固定长度(fixed-size)的序列,数组类型写为 [T; N]。长度(size)是一个计算结果为 usize常量表达式

示例:


#![allow(unused)]
fn main() {
// 一个栈分配的数组
let array: [i32; 3] = [1, 2, 3];

// 一个堆分配的数组,被自动强转成切片
let boxed_array: Box<[i32]> = Box::new([1, 2, 3]);
}

数组的所有元素总是初始化过的,使用 Rust 中的安全(safe)方法或操作符来访问数组时总是会先做越界检查。

注意:标准库类型 Vec<T> 提供了堆分配方案的可调整大小的数组类型。

切片类型

句法
SliceType :
   [ Type ]

切片是一种动态尺寸类型(dynamically sized type),它代表类型为 T 的元素组成的数据序列的一个“视图(view)”。切片类型写为 [T]

要使用切片类型,通常必须放在指针后面使用,例如:

  • &[T],共享切片('shared slice'),常被直接称为切片(slice),它不拥有它指向的数据,只是借用。
  • &mut [T],可变切片('mutable slice'),可变借用它指向的数据。
  • Box<[T]>, boxed切片('boxed slice')。

示例:


#![allow(unused)]
fn main() {
// 一个堆分配的数组,被自动强转成切片
let boxed_array: Box<[i32]> = Box::new([1, 2, 3]);

// 数组上的(共享)切片
let slice: &[i32] = &boxed_array[..];
}

切片的所有元素总是初始化过的,使用 Rust 中的安全(safe)方法或操作符来访问切片时总是会做越界检查。

结构体类型

结构体(struct)类型是其由他类型异构产生的类型,这些其他类型被称为结构体类型的字段。1

结构体(struct)的实例可以用结构体表达式来构造。

默认情况下,结构体(struct)的内存布局是未定义的(默认允许进行一些编译器优化,比如字段重排),但也可以使用repr属性来使其布局在定义时就固定下来。在这两种情况下,字段在相应的结构体表达式中都可以以任何顺序给出;(但在相同的编译目标中,在)同一布局规则下生成的结构体(struct)值将始终具有相同的内存布局。

结构体(struct)的字段可以由可见性修饰符(visibility modifiers)限定,以允许从模块之外来访问结构体中的数据。

*元组结构体(uple struct)*类型与结构体类型类似,只是字段是匿名的。

*单元结构体(unit-like struct)*类型类似于结构体类型,只是它没有字段。由初始结构体表达式构造的值是驻留在此类类型中惟一的值。

1

struct类型类似于 C 中的 struct 类型、ML家族的 record 类型或 Lisp 家族的 struct 类型。

枚举类型

枚举类型是一种标称型(nominal)的、异构的、不相交的类型联合起来组成的类型,它直接用枚举(enum)程序项的名称来表示。1

枚举(enum)程序项同时声明了类型和它的各种变体(variants),其中每个变体都独立命名,可使用定义结构体、元组结构体或单元结构体(unit-like struct)的句法来定义它们。

枚举(enum)的实例可以通过结构体体表达式来构造。

任何枚举值消耗的内存和其同类型的其他变体都是相同的,具体都为其枚举(enum)类型的最大变体所需的内存再加上存储其判别值(discriminant)所需的内存。

枚举类型不能在结构上表示为类型,必须通过对枚举程序项的具名引用(named reference)来表示。2

1

enum类型类似于 ML 中的数据(data)构造函数声明,或 Limbo 中的 pick ADT

2

译者理解这句话的意思是:枚举不同于普通结构化的类型,所有的枚举类型都是对枚举程序项的引用;这里引用分两种,一种是类C枚举,就是对程序项的直接具名引用;另一种是带字段的枚举变体,这种其实是类似于 BoxRc 这样的具名引用,它通过封装其他类型来指导数据的存储和限定其上可用的操作。

联合体类型

联合体类型是一种标称型(nominal)的、异构的、类似C语言里的 union 的类型,具体的类型名称由联合体(union)程序项的名称表示。

Since transmutes can cause unexpected or undefined behaviour, unsafe is required to read from a union field, or to write to a field that doesn't implement Copy or has a [ManuallyDrop] type. See the item documentation for further details. 联合体没有“活跃字段(active field)”的概念。相反,每次对联合体的访问都将联合体的部分存储内容转换为被访问字段的类型。由于转换可能会导致意外或未定义行为,所以读取联合体字段,或写入未实现 Copy 或 [ManuallyDrop] 的联合体字段的操作都需要放在 unsafe块内进行。有关详细信息,请参阅相应的程序项文档。

默认情况下,联合体(union)的内存布局是未定义的,但是可以使用 #[repr(...)]属性来固定为某一类型布局。

函数项类型

当被引用函数项、元组结构体的构造函数或枚举变体的构造函数时,会产生它们的*函数项类型(function item type)*的零尺寸的值。这种类型(也就是此值)显式地标识了该函数——标识出的内容包括程序项定义时的名字、类型参数,及其定义时的生存期参数(不是后期绑定的生存期参数,后期绑定的生存期参数只在函数被调用时才被赋与)——所以该值不需要包含一个实际的函数指针,当此函数被调用时也不需要一个间接的寻址操作去查找此函数。

没有直接引用函数项类型的句法,但是编译器会在错误消息中会显示类似于 fn(u32) -> i32 {fn_name} 这样的“类型”。

因为函数项类型显式地标识了其函数,所以如果它们所指的程序项不同(包括不同的程序项,或相同的程序项但泛型参数的实际类型不同),则此函数项类型也不同,混合使用它们将导致类型错误:


#![allow(unused)]
fn main() {
fn foo<T>() { }
let x = &mut foo::<i32>;
*x = foo::<u32>; //~ 错误:类型不匹配
}

但确实存在从函数项类型到具有相同签名的函数指针的自动强转(coercion),这种自动强转一般发生在使用函数项类型却直接预期函数指针时;另一种发生情况是 if表达式或匹配(match)表达式的不同分支返回使用了具有相同签名却使用了不同函数项类型的情况:


#![allow(unused)]
fn main() {
let want_i32 = false;
fn foo<T>() { }

// 这里 `foo_ptr_1` 标注使用了 `fn()` 这个函数指针类型。
let foo_ptr_1: fn() = foo::<i32>;

// ... `foo_ptr_2` 也行 - 这次是通过类型检查(type-checks)做到的。
let foo_ptr_2 = if want_i32 {
    foo::<i32>
} else {
    foo::<u32>
};
}

所有的函数项类型都实现了 FnFnMutFnOnceCopyCloneSendSync

闭包类型

闭包表达式生成的闭包值具有唯一性和无法写出的匿名性。闭包类型近似相当于包含捕获变量的结构体。比如以下闭包示例:


#![allow(unused)]
fn main() {
fn f<F : FnOnce() -> String> (g: F) {
    println!("{}", g());
}

let mut s = String::from("foo");
let t = String::from("bar");

f(|| {
    s += &t;
    s
});
// 打印 "foobar".
}

生成大致如下所示的闭包类型:

struct Closure<'a> {
    s : String,
    t : &'a String,
}

impl<'a> FnOnce<()> for Closure<'a> {
    type Output = String;
    fn call_once(self) -> String {
        self.s += &*self.t;
        self.s
    }
}

所以调用 f 相当于:

f(Closure{s: s, t: &t});

捕获方式

编译器倾向于优先通过不可变借用(immutable borrow)来捕获闭合变量(closed-over variable),其次是通过唯一不可变借用(unique immutable borrow)(见下文),再其次可变借用(mutable borrow),最后使用移动语义(move)来捕获。编译器将选择这些中的第一个能让此闭包编译通过的选项。这个选择只与闭包表达式的内容有关;编译器不考虑闭包表达式之外的代码,比如所涉及的变量的生存期。

如果使用了关键字 move ,那么所有捕获都是通过移动(move)语义进行的(当然对于 Copy类型,则是通过拷贝语义进行的),而不管借用是否可用。关键字 move 通常用于允许闭包比其捕获的值活得更久,例如返回闭包或用于生成新线程。

复合类型(如结构体、元组和枚举)始终是全部捕获的,而不是各个字段分开捕获的。如果真要捕获单个字段,那可能需要先借用该字段到本地局部变量中:


#![allow(unused)]
fn main() {
use std::collections::HashSet;

struct SetVec {
    set: HashSet<u32>,
    vec: Vec<u32>
}

impl SetVec {
    fn populate(&mut self) {
        let vec = &mut self.vec;
        self.set.iter().for_each(|&n| {
            vec.push(n);
        })
    }
}
}

相反,如果闭包直接使用了 self.vec,那么它将尝试通过可变引用捕获 self。但是因为 self.set 已经被借出用来迭代了,所以代码将无法编译。

捕获中的唯一不可变借用

捕获方式中有一种被称为唯一不可变借用的特殊类型的借用捕获,这种借用不能在语言的其他任何地方使用,也不能显式地写出。唯一不可变借用发生在修改可变引用的引用对象(referent)时,如下面的示例所示:


#![allow(unused)]
fn main() {
let mut b = false;
let x = &mut b;
{
    let mut c = || { *x = true; };
    // 下行代码不正确
    // let y = &x;
    c();
}
let z = &x;
}

在这种情况下,不能去可变借用 x,因为 x 没有标注 mut。但与此同时,如果不可变借用 x,那对其赋值又会非法,因为 & &mut 引用可能不是唯一的,因此此引用不能安全地用于修改值。所以这里闭包使用了唯一不可变借用:它采用了不可变的方式借用了 x,但是又像可变借用一样,当然前提是此借用必须是唯一的。在上面的例子中,解开 y 那行上的注释将产生错误,因为这将违反闭包对 x 的借用的唯一性;z 的声明是有效的,因为闭包的生存期在块结束时已过期,从而已经释放了对 x 的借用。

调用trait 和自动强转

闭包类型都实现了 FnOnce,这表明它们可以通过消耗掉闭包的所有权来调用执行它一次。此外,一些闭包实现了更具体的调用trait:

  • 对没采用移动语义来捕获任何变量(译者注:变量的原始所有者仍拥有该变量的所有权)的闭包都实现了 FnMut,这表明该闭包可以通过可变引用来调用。

  • 对捕获的变量没移出其值,也没去修改其值的闭包都实现了 Fn,这表明该闭包可以通过共享引用来调用。

注意:move闭包可能仍然实现 FnFnMut,即使它们通过移动(move)语义来捕获变量。这是因为闭包类型实现什么样的 trait 是由闭包对捕获的变量值做了什么来决定的,而不是由闭包如何捕获它们来决定的。1

*非捕获闭包(Non-capturing closures)*是指不捕获环境中的任何变量的闭包。它们可以通过匹配签名的方式被自动强转成函数指针(例如 fn())。


#![allow(unused)]
fn main() {
let add = |x, y| x + y;

let mut x = add(5,7);

type Binop = fn(i32, i32) -> i32;
let bo: Binop = add;
x = bo(5,7);
}

其他 trait

所有闭包类型都实现了 Sized。此外,闭包捕获的变量的类型如果实现了如下 trait,此闭包类型也会自动实现这些 trait:

闭包类型实现 SendSync 的规则与普通结构体类型实现这俩 trait 的规则一样,而 CloneCopy 就像它们在 derived属性中表现的一样。对于 Clone,捕获变量的克隆顺序目前还没正式的规范出来。

由于捕获通常是通过引用进行的,因此会出现以下一般规则:

  • 如果所有的捕获变量都实现了 Sync,则此闭包就也实现了 Sync
  • 如果所有非唯一不可变引用捕获的变量都实现了 Sync,并且所有由唯一不可变、可变引用、复制或移动语义捕获的值都实现了 Send,则此闭包就也实现了 Send
  • 如果一个闭包没有通过唯一不可变引用或可变引用捕获任何值,并且它通过复制或移动语义捕获的所有值都分别实现了 CloneCopy,则此闭包就也实现了 CloneCopy
1

译者的实验代码:


#![allow(unused)]
fn main() {
fn f1<F : Fn() -> i32> (g: F) { println!("{}", g());}
fn f2<F : FnMut() -> i32> (mut g: F) { println!("{}", g());}
let t = 8;    
f1(move || { t });
f2(move || { t });
}

指针类型

Rust 中所有的指针都是显式的头等(first-class)值。 它们可以被移动或复制,存储到其他数据结构中,或从函数中返回。

引用(&&mut)

句法
ReferenceType :
   & Lifetime? mut? TypeNoBounds

共享引用(&)

共享引用(&)指向由其他值拥有的内存。 创建了对值的共享引用可以防止对该值的直接更改。 但在某些特定情况下,内部可变性又提供了这种情况的一种例外。 顾名思义,对一个值的共享引用的次数没有限制。共享引用类型被写为 &type;当需要指定显式的生存期时可写为 &'a type。 拷贝一个引用是一个“浅拷贝(shallow)”操作:它只涉及复制指针本身,也就是指针实现了 Copy trait 的意义所在。 释放引用对共享引用所指向的值没有影响,但是对临时值的引用的存在将使此临时值在此引用的作用域内保持存活状态。

可变引用(&mut)

可变引用(&mut)也指向其他值所拥有的内存。 可变引用类型被写为 &mut type&'a mut type。 可变引用(其还未被借出1)是访问它所指向的值的唯一方法,所以可变引用没有实现 Copy trait。

裸指针(*const*mut)

句法
RawPointerType :
   * ( mut | const ) TypeNoBounds

裸指针是没有安全性或可用性(liveness)保证的指针。 裸指针写为 *const T*mut T,例如,*const i32 表示指向 32-bit 有符号整数的裸指针。 拷贝或销毁(dropping )裸指针对任何其他值的生命周期(lifecycle)都没有影响。对 裸指针的解引用是非安全(unsafe)操作,可以通过重新借用裸指针(&*&mut *)将其转换为引用。 在 Rust 代码中通常不鼓励使用裸指针;它们的存在是为了提升与外部代码的互操作性,以及编写对性能要求很高的函数或很底层的函数。

在比较裸指针时,比较的是它们的地址,而不是它们指向的数据。 当比较裸指针和动态尺寸类型时,还会比较它们指针上的附加/元数据。

可以直接使用 core::ptr::addr_of! 创建 *const 类型的裸指针,通过 core::ptr::addr_of_mut! 创建 *mut 类型的裸指针。

智能指针

标准库包含了一些额外的“智能指针”类型,它们提供了在引用和裸指针这类低级指针之外的更多的功能。

1

译者理解这里是指 &mut type 如果被借出,就成了 &&mut type,这样就又成了不可变借用了。

函数指针类型

句法
BareFunctionType :
   ForLifetimes? FunctionTypeQualifiers fn
      ( FunctionParametersMaybeNamedVariadic? ) BareFunctionReturnType?

FunctionTypeQualifiers:
   unsafe? (extern Abi?)?

BareFunctionReturnType:
   -> TypeNoBounds

FunctionParametersMaybeNamedVariadic :
   MaybeNamedFunctionParameters | MaybeNamedFunctionParametersVariadic

MaybeNamedFunctionParameters :
   MaybeNamedParam ( , MaybeNamedParam )* ,?

MaybeNamedParam :
   OuterAttribute* ( ( IDENTIFIER | _ ) : )? Type

MaybeNamedFunctionParametersVariadic :
   ( MaybeNamedParam , )* MaybeNamedParam , OuterAttribute* ...

函数指针类型(使用关键字 fn 写出)指向那些在编译时不必知道函数标识符的函数。它们也可以由函数项类型或非捕获(non-capturing)闭包经过一次自动强转(coercion)来创建。

非安全(unsafe)限定符表示类型的值是一个非安全函数,而外部(extern)限定符表示它是一个外部函数

可变参数只能通过使用 "C""cdecl" 的 ABI调用约定的 extern函数类型来指定。

下面示例中 Binop 被定义为函数指针类型:


#![allow(unused)]
fn main() {
fn add(x: i32, y: i32) -> i32 {
    x + y
}

let mut x = add(5,7);

type Binop = fn(i32, i32) -> i32;
let bo: Binop = add;
x = bo(5,7);
}

函数指针参数上的属性

函数指针参数上的属性遵循与常规函数参数相同的规则和限制。

trait对象

句法
TraitObjectType :
   dyn? TypeParamBounds

TraitObjectTypeOneBound :
   dyn? TraitBound

*trait对象1*是另一种类型的不透明值(opaque value),它实现了一组 trait。2 这组 trait 是由一个对象安全的基础trait(base trait) 加上任意数量的自动trait(auto traits)组成。

trait对象实现了基础trait、它的自动trait 以及其基础trait 的任何超类trait(supertraits)

trait对象被写为为可选的关键字 dyn 后跟一组 trait约束,这些 trait约束有如此限制:除了第一个 trait 外,其他所有 trait 都必须是自动trait;生存期不能超过一个;不允许选择退出约束(opt-out bounds)(例如 ?Sized)。此外,trait 的路径可以用圆括号括起来。

例如,给定一个trait Trait,下面所有的形式都是 trait对象:

  • Trait
  • dyn Trait
  • dyn Trait + Send
  • dyn Trait + Send + Sync
  • dyn Trait + 'static
  • dyn Trait + Send + 'static
  • dyn Trait +
  • dyn 'static + Trait.
  • dyn (Trait)

版本差异:在 2015 版里,如果 trait对象的第一个约束是以 :: 开头的路径,那么 dyn 会被视为路径的一部分。可以把第一条路径放在圆括号中来绕过这个问题。因此,如果希望 trait对象具有 ::your_module::Trait 路径,那么应该将其写为 dyn (::your_module::Trait)

从2018版本开始,dyn 是一个真正的关键字了,不允许在路径中使用,(不存在二义性了,)因此括号就没必要了。

注意:为了清晰起见,建议总是在 trait对象上使用关键字 dyn,除非你的代码库支持用Rust 1.26 或更低的版本来编译。

如果基础trait 互为别名,并且自动trait 相同,生存期约束也相同,则这两种 trait对象类型互为别名。例如,dyn Trait + Send + UnwindSafedyn Trait + UnwindSafe + Send 是等价的。

由于值的具体类型是不透明的,trait对象是动态尺寸类型。像所有的 DST 一样,trait对象常被用在某种类型的指针后面;例如 &dyn SomeTraitBox<dyn SomeTrait>。每个指向 trait对象的指针实例包括:

  • 一个指向实现 SomeTrait 的(那个真实的不透明的)类型 T 的实例的指针
  • 一个指向*虚拟方法表(virtual method table_)*的指针。虚拟方法表也通常被称为 虚函数表(vtable),它包含了 T 实现的 SomeTrait 的所有方法,T 实现的 SomeTrait超类trait 的每个方法,还有指向 T 的实现的指针(即函数指针)。

trait对象的目的是允许方法的“延迟绑定(late binding)”。在 trait对象上调用一个方法会导致运行时的虚拟分发(virtual dispatch):也就是说,一个函数指针从 trait对象的虚函数表(vtable)中被加载进来,并被间接调用。每个虚函数表实体的实际实现可能因对象的不同而不同。

一个 trait对象的例子:

trait Printable {
    fn stringify(&self) -> String;
}

impl Printable for i32 {
    fn stringify(&self) -> String { self.to_string() }
}

fn print(a: Box<dyn Printable>) {
    println!("{}", a.stringify());
}

fn main() {
    print(Box::new(10) as Box<dyn Printable>);
}

在本例中,trait Printable 作为 trait对象出现在 以 print 为类型签名的函数的参数中 和 main 中的类型转换表达式(cast expression)中。

trait对象的生存期约束

因为 trait对象可以包含引用,所以这些引用的生存期需要表示为 trait对象的一部分。这种生存期被写为 Trait + 'a默认情况下,可以通过合理的选择来推断此生存期。

1

本书行文中,使用“trait对象”这个词时,没区分 trait对象的值和类型本身,但一般都指类型。

2

译者认为这样翻译可能更容易理解:trait对象是丢失了/隐藏了具体真实类型的 trait实现的类型,此类型本身一般会实现了一组 trait。

实现trait

句法
ImplTraitType : impl TypeParamBounds

ImplTraitTypeOneBound : impl TraitBound

匿名类型参数

注意:此部分只是一个占位符,预备将来这里会有更全面的参考资料。

注意:匿名类型参数通常被称为“参数位置上的实现trait(impl Trait in argument position)”。

函数可以将其参数声明为匿名类型的参数,这种情况下函数内部只能使用匿名类型参数的 trait约束所提供的方法。之后的调用者(callee)在调用此函数时必须提供具体的类型,并且要求提供的具体类型必须拥有此匿名类型参数声明的约束。

它们被写成 impl 后跟一组 trait约束。

抽象返回类型

注意:此部分只是一个占位符,预备将来这里会有更全面的参考资料。

注意:抽象返回类型(abstract return types)通常被称为“函数返回位置上的实现trait(impl Trait in return position)”。

除了关联trait函数(associated trait function)外,其他函数都可以返回抽象返回类型。这些类型代表了某种具体类型,它要求在此返回类型的使用点(use-site)上只能使用由该类型的 trait约束声明的 trait方法。

它们被写成 impl 后跟一组 trait约束。

类型参数

在带有类型参数声明的程序项的代码体内,这些类型参数的名称可以直接当做类型使用:


#![allow(unused)]
fn main() {
fn to_vec<A: Clone>(xs: &[A]) -> Vec<A> {
    if xs.is_empty() {
        return vec![];
    }
    let first: A = xs[0].clone();
    let mut rest: Vec<A> = to_vec(&xs[1..]);
    rest.insert(0, first);
    rest
}
}

这里,first 的类型为 A,援引的是 to_vec 的类型参数 Arest 的类型为 Vec<A>,它是一个元素类型为 A 向量(vector)。

自动推断型类型

句法
InferredType : _

自动推断型类型要求编译器尽可能根据周围可用的信息推断出实际使用类型。它不能用于程序项的签名中。它经常用于泛型参数中:


#![allow(unused)]
fn main() {
let x: Vec<_> = (0..10).collect();
}

动态尺寸类型

大多数的类型都有一个在编译时就已知的固定尺寸,并实现了 trait Sized。只有在运行时才知道尺寸的类型称为动态尺寸类型(dynamically sized type)DST),或者非正式地称为非固定尺寸类型(unsized type)。切片trait对象DSTs 的两个例子。此类类型只能在某些情况下使用:

  • 指向 DST指针类型的尺寸是固定的(sized),但是是指向固定尺寸类型的指针的尺寸的两倍
    • 指向切片的指针也存储了切片的元素的数量。
    • 指向 trait对象的指针也存储了一个指向虚函数表(vtable)的指针地址
  • 当接受了 ?Sized约束时,DST 可以作为类型实参( type arguments)使用。默认情况下,任何类型形参(type parameter)都拥有 Sized约束。
  • 可以为 DST 实现 trait。与类型参数中的默认设置不同,在 trait定义中默认存在 Self: ?Sized约束。
  • 结构体可以包含一个 DST 作为最后一个字段,这使得该结构体也成为是一个 DST

注意变量、函数参数、常量项和静态项必须是 Sized

类型布局

类型的布局描述类型的尺寸(size)、对齐量(alignment)和字段(fields)的相对偏移量(relative offsets)。对于枚举,其判别值(discriminant)的布局和解释也是类型布局的一部分。

每次编译都有可能更改类型布局。这里我们只阐述当前编译器所保证的内容,而没试图去阐述编译器对此做了什么。

尺寸和对齐量

所有值都有一个对齐量和尺寸。

值的对齐量指定了哪些地址可以有效地存储该值。对齐量为 n 的值只能存储地址为 n 的倍数的内存地址上。例如,对齐量为 2 的值必须存储在偶数地址上,而对齐量为 1 的值可以存储在任何地址上。对齐量是用字节数来度量的,必须至少是 1,并且总是 2 的幂次。值的对齐量可以通过函数 align_of_val 来检测。

值的尺寸是同类型的值组成的数组中连续两个元素之间的字节偏移量,此偏移量包括了为保持程序项类型内部对齐而对此类型做的对齐填充。值的尺寸总是其对齐量的非负整数倍数。值的尺寸可以通过函数 size_of_val 来检测。

如果某类型的所有的值都具有相同的尺寸和对齐量,并且两者在编译时都是已知的,并且实现了 Sized trait,则可以使用函数 size_ofalign_of 对此类型进行检测。没有实现 Sized trait 的类型被称为动态尺寸类型。由于实现了 Sized trait 的某一类型的所有值共享相同的尺寸和对齐量,所以我们分别将这俩共享值称为该类型的尺寸和该类型的对齐量。

原生类型的布局

下表给出了大多数原生类型(primitives)的尺寸。

类型size_of::<Type>()
bool1
u8 / i81
u16 / i162
u32 / i324
u64 / i648
u128 / i12816
f324
f648
char4

usizeisize 的尺寸足以包含目标平台上的每个内存地址。例如,在 32-bit 目标上,它们是 4 个字节,而在 64-bit 目标上,它们是 8 个字节。

大多数原生类型的对齐量通常与它们的尺寸保持一致,尽管这是特定于平台的行为。比较典型的就是在 x86 平台上,u64 和 f64 都上 32-bit 的对齐量。

指针和引用的布局

指针和引用具有相同的布局。指针或引用的可变性不会影响其布局。

指向固定尺寸类型(sized type)的值的指针具有和 usize 相同的尺寸和对齐量。

指向非固定尺寸类型(unsized types)的值的指针是固定尺寸的。其尺寸和对齐量至少等于一个指针的尺寸和对齐量

注意:虽然不应该依赖于此,但是目前所有指向 DST 的指针都是 usize 的两倍尺寸,并且具有相同的对齐量。

数组的布局

数组的布局使得数组的第 n 个(nth)元素为从数组开始的位置向后偏移 n * 元素类型的尺寸(n * the size of the element's type) 个字节数。数组 [T; n] 的尺寸为 size_of::<T>() * n,对齐量和 T 的对齐量相同。

切片的布局

切片的布局与它们所切的那部分数组片段相同。

注意:这是关于原生的 [T]类型,而不是指向切片的指针(&[T]Box<[T]> 等)。

字符串切片(str)的布局

字符串切片是一种 UTF-8 表示形式(representation)的字符序列,它们与 [u8]类型的切片拥有相同的类型布局。

元组的布局

元组对于其布局没有任何保证。

一个例外情况是单元结构体(unit tuple)(())类型,它被保证为尺寸为 0,对齐量为 1。

trait对象的布局

trait对象的布局与 trait对象的值相同。

注意:这是关于原生 trait对象类型(raw trait object type)的,而不是指向 trait对象的指针(&dyn TraitBox<dyn Trait> 等)。

闭包的布局

闭包的布局没有保证。

表形/表示形式

所有用户定义的复合类型(结构体(struct)、枚举(enum)和联合体(union))都有一个*表形(representation)*属性,该属性用于指定该类型的布局。类型的可能表形有:

类型的表形可以通过对其应用 repr属性来更改。下面的示例展示了一个 C表形的结构体。


#![allow(unused)]
fn main() {
#[repr(C)]
struct ThreeInts {
    first: i16,
    second: i8,
    third: i32
}
}

可以分别使用 alignpacked 修饰符增大或缩小对齐量。它们可以更改属性中指定表形的对齐量。如果未指定表形,则更改默认表形的。


#![allow(unused)]
fn main() {
// 默认表形,把对齐量缩小到2。
#[repr(packed(2))]
struct PackedStruct {
    first: i16,
    second: i8,
    third: i32
}

// C表形,把对齐量增大到8
#[repr(C, align(8))]
struct AlignedStruct {
    first: i16,
    second: i8,
    third: i32
}
}

注意:由于表形是程序项的属性,因此表形不依赖于泛型参数。具有相同名称的任何两种类型都具有相同的表形。例如,Foo<Bar>Foo<Baz> 都有相同的表形。

类型的表形可以更改字段之间的填充,但不会更改字段本身的布局。例如一个使用 C表形的结构体,如果它包含一个默认表形的字段 Inner,那么它不会改变 Inner 的布局。

默认表形

没有 repr属性的标称(nominal)类型具有默认表形。非正式地的情况下,也称这种表形为 rust表形。

这种表形不保证每次编译都有统一的数据布局。

C表形

C表形被设计用于双重目的:一个目的是创建可以与 C语言互操作的类型;第二个目的是创建可以正确执行依赖于数据布局的操作的类型,比如将值重新解释为其他类型。

因为这种双重目的存在,可以只利用其中的一个目的,如只创建有固定布局的类型,而放弃与 C语言的互操作。

这种表型可以应用于结构体(structs)、联合体(unions)和枚举(enums)。一个例外是零变体枚举(zero-variant enums),它的 C表形是错误的。

#[repr(C)]结构体

结构体的对齐量是其*最大对齐量的字段(most-aligned field)*的对齐量。

字段的尺寸和偏移量则由以下算法确定:

  1. 把当前偏移量设为从 0 字节开始。

  2. 对于结构体中的每个字段,按其声明的先后顺序,首先确定其尺寸和对齐量;如果当前偏移量不是对其齐量的整倍数,则向当前偏移量添加填充字节,直至其对齐量的倍数1;至此,当前字段的偏移量就是当前偏移量;下一步再根据当前字段的尺寸增加当前偏移量。

  3. 最后,整个结构体的尺寸就是当前偏移量向上取整到结构体对齐量的最小整数倍数。

下面用伪代码描述这个算法:

/// 返回偏移(`offset`)之后需要的填充量,以确保接下来的地址将被安排到可对齐的地址。
fn padding_needed_for(offset: usize, alignment: usize) -> usize {
    let misalignment = offset % alignment;
    if misalignment > 0 {
        // 向上取整到对齐量(`alignment`)的下一个倍数
        alignment - misalignment
    } else {
        // 已经是对齐量(`alignment`)的倍数了
        0
    }
}

struct.alignment = struct.fields().map(|field| field.alignment).max();

let current_offset = 0;

for field in struct.fields_in_declaration_order() {
    // 增加当前字的偏移量段(`current_offset`),使其成为该字段对齐量的倍数。
    // 对于第一个字段,此值始终为零。
    // 跳过的字节称为填充字节。
    current_offset += padding_needed_for(current_offset, field.alignment);

    struct[field].offset = current_offset;

    current_offset += field.size;
}

struct.size = current_offset + padding_needed_for(current_offset, struct.alignment);

警告:这个伪代码使用了一个简单粗暴的算法,是为了清晰起见,它忽略了溢出问题。要在实际代码中执行内存布局计算,请使用 Layout

注意:此算法可以生成零尺寸的结构体。在 C 语言中,像 struct Foo { } 这样的空结构体声明是非法的。然而,gcc 和 clang 都支持启用此类结构体的选项,并将其尺寸指定为零。跟 Rust 不同的是 C++ 给空结构体指定的尺寸为 1,并且除非它们是继承的,否则它们是具有 [[no_unique_address]] 属性的字段(在这种情况下,它们不会增大结构体的整体尺寸)。

#[repr(C)]联合体

使用 #[repr(C)] 声明的联合体将与相同目标平台上的 C语言中的 C联合体声明具有相同的尺寸和对齐量。联合体的对齐量等同于其所有字段的最大对齐量,尺寸将为其所有字段的最大尺寸,再对其向上取整到对齐量的最小整数倍。这些最大值可能来自不同的字段。


#![allow(unused)]
fn main() {
#[repr(C)]
union Union {
    f1: u16,
    f2: [u8; 4],
}

assert_eq!(std::mem::size_of::<Union>(), 4);  // 来自于 f2
assert_eq!(std::mem::align_of::<Union>(), 2); // 来自于 f1

#[repr(C)]
union SizeRoundedUp {
   a: u32,
   b: [u16; 5],
}

assert_eq!(std::mem::align_of::<SizeRoundedUp>(), 4); // 来自于 a

assert_eq!(std::mem::size_of::<SizeRoundedUp>(), 12);  // 首先来自于b的尺寸10,然后向上取整到最近的4的整数倍12。

}

#[repr(C)]无字段枚举

对于无字段枚举(field-less enums)C表形的尺寸和对齐量与目标平台的 C ABI 的默认枚举尺寸和对齐量相同。

注意:C中的枚举的表形是由枚举的相应实现定义的,所以在 Rust 中,给无字段枚举应用 C表形得到的表型很可能是一个“最佳猜测”。特别是,当使用某些特定命令行参数来编译特定的 C代码时,这可能是不正确的。

警告:C语言中的枚举与 Rust 中的那些应用了 #[repr(C)]表型的无字段枚举之间有着重要的区别。C语言中的枚举主要是 typedef 加上一些具名常量;换句话说,C枚举(enum)类型的对象可以包含任何整数值。例如,C枚举通常被用做标志位。相比之下,Rust的无字段枚举只能合法地2保存判别式的值,其他的都是未定义行为。因此,在 FFI 中使用无字段枚举来建模 C语言中的枚举(enum)通常是错误的。

#[repr(C)]带字段枚举

带字段的 repr(C)枚举的表形其实等效于一个带两个字段的 repr(C)结构体(这种在 C语言中也被称为“标签联合(tagged union)”),这两个字段:

  • 一个为 repr(C)表形的枚举(在这个等效结构体内,它也被叫做标签(the tag)字段),它就是原枚举所有的判别值组合成的新枚举,也就是它的变体是原枚举变体移除了它们自身所带的所有字段。
  • 一个为 repr(C)表形的联合体(在这个等效结构体内,它也被叫做载荷(the payload)字段),它的各个字段就是原枚举的各个变体把自己下面的字段重新组合成的 repr(C)表形的结构体。

注意:由于等效出的结构体和联合体是 repr(C)表形的,因此如果原来某一变体只有单个字段,则直接将该字段放入等效出的联合体中,或将其包装进一个次级结构体后再放入联合体中是没有区别的;因此,任何希望操作此类枚举表形的系统都可以选择使用这两种形式里对它们来说更方便或更一致的形式。


#![allow(unused)]
fn main() {
// 这个枚举的表形等效于 ...
#[repr(C)]
enum MyEnum {
    A(u32),
    B(f32, u64),
    C { x: u32, y: u8 },
    D,
 }

// ... 这个结构体
#[repr(C)]
struct MyEnumRepr {
    tag: MyEnumDiscriminant,
    payload: MyEnumFields,
}

// 这是原判别式组成的新枚举类型.
#[repr(C)]
enum MyEnumDiscriminant { A, B, C, D }

// 这是原变体的字段组成的联合体.
#[repr(C)]
union MyEnumFields {
    A: MyAFields,   // 译者注:因为原枚举变体A只有一个字段,所以此处的类型标注也可以直接替换为 u32,以省略 MyAFields这层封装
    B: MyBFields,
    C: MyCFields,
    D: MyDFields,
}

#[repr(C)]
#[derive(Copy, Clone)]
struct MyAFields(u32);

#[repr(C)]
#[derive(Copy, Clone)]
struct MyBFields(f32, u64);

#[repr(C)]
#[derive(Copy, Clone)]
struct MyCFields { x: u32, y: u8 }

// 这个结构体可以被省略(它是一个零尺寸类型),但它必须出现在 C/C++ 头文件中
#[repr(C)]
#[derive(Copy, Clone)]
struct MyDFields;
}

注意: 联合体(union)可带有未实现 Copy 的字段的功能还没有纳入稳定版,具体参见 55149

原语表形

原语表形是与原生整型具有相同名称的表形。也就是:u8u16u32u64u128usizei8i16i32i64i128isize

原语表形只能应用于枚举,此时枚举有没有字段会给原语表形带来不同的表现。给零变体枚举应用原始表形是错误的。将两个原语表形组合在一起也是错误的

无字段枚举的原语表形

对于无字段枚举,原语表形将其尺寸和对齐量设置成与给定表形同名的原生类型的表形的值。例如,一个 u8表形的无字段枚举只能有0和255之间的判别值。

带字段枚举的原语表形

带字段枚举的原语表形是一个 repr(C)表形的联合体,此联合体的每个字段对应一个和原枚举变体对应的 repr(C)表形的结构体。这些结构体的第一个字段是原枚举的变体移除了它们所有的字段组成的原语表形版的无字段枚举(“the tag”),那这些结构体的其余字段是原变体移走的字段。

注意:如果在联合体中,直接把标签的成员赋予给标签(“the tag”),那么这种表形结构仍不变的,并且这样操作对您来说可能会更清晰(尽管遵循 c++ 的标准,标签也应该被包装在结构体中)。


#![allow(unused)]
fn main() {
// 这个枚举的表形效同于 ...
#[repr(u8)]
enum MyEnum {
    A(u32),
    B(f32, u64),
    C { x: u32, y: u8 },
    D,
 }

// ... 这个联合体.
#[repr(C)]
union MyEnumRepr {
    A: MyVariantA,  //译者注:此字段类型也可直接用 u32 直接替代
    B: MyVariantB,  //译者注:此字段类型也可直接用 (f32, u64) 直接替代
    C: MyVariantC,
    D: MyVariantD,
}

// 这是原判别值组合成的新枚举。
#[repr(u8)]
#[derive(Copy, Clone)]
enum MyEnumDiscriminant { A, B, C, D }

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantA(MyEnumDiscriminant, u32);

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantB(MyEnumDiscriminant, f32, u64);

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantC { tag: MyEnumDiscriminant, x: u32, y: u8 }

#[repr(C)]
#[derive(Clone, Copy)]
struct MyVariantD(MyEnumDiscriminant);
}

注意: 联合体(union)带有未实现 Copy trait 的字段的功能还没有纳入稳定版,具体参见 55149

带字段枚举的原语表形与#[repr(C)]表形的组合使用

对于带字段枚举,还可以将 repr(C) 和原语表形(例如,repr(C, u8))结合起来使用。这是通过将判别值组成的枚举的表形改为原语表形来实现的。因此,如果选择组合 u8表形,那么组合出的判别值枚举的尺寸和对齐量将为 1 个字节。

那么这个判别值枚举就从前面示例中的样子变成:


#![allow(unused)]
fn main() {
#[repr(C, u8)] // 这里加上了 `u8`
enum MyEnum {
    A(u32),
    B(f32, u64),
    C { x: u32, y: u8 },
    D,
 }

// ...

#[repr(u8)] // 所以这里就用 `u8` 替代了 `C`
enum MyEnumDiscriminant { A, B, C, D }

// ...
}

例如,对于有 repr(C, u8)属性的枚举,不可能有257个唯一的判别值(“tags”),而同一个枚举,如果只有单一 repr(C)表形属性,那在编译时就不会出任何问题。

repr(C) 附加原语表形可以改变 repr(C)表形的枚举的尺寸:


#![allow(unused)]
fn main() {
#[repr(C)]
enum EnumC {
    Variant0(u8),
    Variant1,
}

#[repr(C, u8)]
enum Enum8 {
    Variant0(u8),
    Variant1,
}

#[repr(C, u16)]
enum Enum16 {
    Variant0(u8),
    Variant1,
}

// C表形的尺寸依赖于平台
assert_eq!(std::mem::size_of::<EnumC>(), 8);
// 一个字节用于判别值,一个字节用于 Enum8::Variant0 中的值
assert_eq!(std::mem::size_of::<Enum8>(), 2);
// 两个字节用于判别值,一个字节用于Enum16::Variant0中的值,加上一个字节的填充
assert_eq!(std::mem::size_of::<Enum16>(), 4);
}

对齐量的修饰符

alignpacked 修饰符可分别用于增大和减小结构体的和联合体的对齐量。packed 也可以改变字段之间的填充。

对齐量被指定为整型参数,形式为 #[repr(align(x))]#[repr(packed(x))]。对齐量的值必须是从1到229之间的2的次幂数。对于 packed,如果没有给出任何值,如 #[repr(packed)],则对齐量的值为1。

对于 align,如果类型指定的对齐量比其不带 align修饰符时的对齐量小,则该指定的对齐量无效。

对于 packed,如果类型指定的对齐量比其不带 packed修饰符时的对齐量大,则该指定的对齐量和布局无效。为了定位字段,每个字段的对齐量是指定的对齐量和字段的类型的对齐量中较小的那个对齐量。

alignpacked 修饰符不能应用于同一类型,且 packed 修饰的类型不能直接或间接地包含另一个 align 修饰的类型。alignpacked 修饰符只能应用于默认表形C表形中。

align修饰符也可以应用在枚举上。如果这样做了,其对枚举对齐量的影响与将此枚举包装在一个新的使用了相同的 align修饰符的结构体中的效果相同。

***警告:***解引用一个未对齐的指针是未定义行为,但可以安全地创建指向 packed修饰的字段的未对齐指针。就像在安全(safe) Rust 中所有创建未定义行为的方法一样,这是一个 bug。

透明(transparent)表形

透明(transparent)表型只能在只有一个字段的结构体(struct)上或只有一个变体的枚举(enum)上使用,这里只有一个字段/变体的意思是:

  • 只能有一个非零尺寸的字段/变体,和
  • 任意数量的尺寸为零对齐量为1的字段(例如:PhantomData<T>

使用这种表形的结构体和枚举与只有那个非零尺寸的字段具有相同的布局和 ABI。

这与 C表形不同,因为带有 C表形的结构体将始终拥有 C结构体(C struct)的ABI,例如,那些只有一个原生类型字段的结构体如果应用了透明表形(transparent),将具有此原生类型字段的ABI。

因为此表形将类型布局委托给另一种类型,所以它不能与任何其他表形一起使用。

1

至此,上一个字段就填充完成,开始计算本字段了。也就是说每一个字段的偏移量是其字段的段首位置;那第一个字段的偏移量就始终为 0。

2

这里合法的意思是变体的判别值受 repr(u8) 这样的表形属性约束,像这个例子中,变体的判别值就只能位于 0~255 之间。

内部可变性

有时一个类型需要在存在多个别名时进行更改。在 Rust 中,这是通过一种叫做内部可变性的模式实现的。如果一个类型的内部状态可以通过对它的共享引用来进行更改,那么就说这个类型就具有内部可变性。这违背了共享引用所指向的值不能被更改的通常要求

std::cell::UnsafeCell<T>类型是 Rust 中唯一可以合法实现此要求的方法。当 UnsafeCell<T> 存在其他不变性的别名1时,仍然可以安全地对它包含的 T 进行更改或获得 T 的一个可变引用。与所有其他类型一样,拥有多个 &mut UnsafeCell<T> 别名是未定义行为。

通过使用 UnsafeCell<T> 作为字段,可以创建具有内部可变性的其他类型。标准库提供了几种这样的类型,这些类型都提供了安全的内部变更的 API。例如,std::cell::RefCell<T> 使用运行时借用检查来确保多个引用存在时的常规规则的执行。std::sync::atomic模块包含了一些类型,这些类型包装了一个只能通过原子操作访问的值,以允许在线程之间共享和修改该值。

1

当变量和指针表示的内存区域有重叠时,它们互为对方的别名。

子类型化和型变

子类型化是隐式的,可以出现在类型检查或类型推断的任何阶段。 Rust 中的子类型化的适用范围非常有限,仅出现在和生存期(lifetimes)的型变(variance)相关的地方,以及那些和高阶生存期相关的类型型变之间。 如果我们擦除了类型的生存期,那么唯一的子类型化就只是类型相等(type equality)了。

考虑下面的例子:字符串字面量总是拥有 'static生存期。不过,我们还是可以把 s 赋值给 t


#![allow(unused)]
fn main() {
fn bar<'a>() {
    let s: &'static str = "hi";
    let t: &'a str = s;
}
}

因为 'static 比生存期参数 'a 的寿命长,所以 &'static str&'a str 的子类型。

高阶函数指针trait对象可以形成另一种父子类型的关系。 它们是那些通过替换高阶生存期而得出的类型的子类型。举些例子:


#![allow(unused)]
fn main() {
// 这里 'a 被替换成了 'static
let subtype: &(for<'a> fn(&'a i32) -> &'a i32) = &((|x| x) as fn(&_) -> &_);
let supertype: &(fn(&'static i32) -> &'static i32) = subtype;

// 这对于 trait对象也是类似的
let subtype: &(for<'a> Fn(&'a i32) -> &'a i32) = &|x| x;
let supertype: &(Fn(&'static i32) -> &'static i32) = subtype;

// 我们也可以用一个高阶生存期来代替另一个
let subtype: &(for<'a, 'b> fn(&'a i32, &'b i32))= &((|x, y| {}) as fn(&_, &_));
let supertype: &for<'c> fn(&'c i32, &'c i32) = subtype;
}

型变

型变是泛型类型相对其参数具有的属性。 泛型类型在它的某个参数上的型变是描述该参数的子类型化去如何影响此泛型类型的子类型化。

  • 如果 TU 的一个子类型意味着 F<T>F<U> 的一个子类型(即子类型化“通过(passes through)”),则 F<T>T 上是协变的(covariant)
  • 如果 TU 的一个子类型意味着 F<U>F<T> 的一个子类型,则 F<T>T 上是逆变的(contravariant)
  • 其他情况下(即不能由参数类型的子类型化关系推导出此泛型的型变关系),F<T>T 上是的不变的(invariant)

类型的型变关系由下表中的规则自动确定:

Type'a 上的型变T 上的型变
&'a T协变的协变的
&'a mut T协变的不变的
*const T协变的
*mut T不变的
[T][T; n]协变的
fn() -> T协变的
fn(T) -> ()逆变的
fn(T) -> T不变的
std::cell::UnsafeCell<T>不变的
std::marker::PhantomData<T>协变的
dyn Trait<T> + 'a协变的不变的

结构体(struct)、枚举(enum)、联合体(union)和元组(tuple)类型上的型变关系是通过查看其字段类型的型变关系来决定的。 如果参数用在了多处且具有不同型变关系的位置上,则该类型在该参数上是不变的。 例如,下面示例的结构体在 'aT 上是协变的,在 'bU 上是不变的。


#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;
struct Variance<'a, 'b, T, U: 'a> {
    x: &'a U,               // 这让 `Variance` 在 'a 上是协变的, 也让在 U 上是协变的, 但是后面也使用了 U
    y: *const T,            // 在 T 上是协变的
    z: UnsafeCell<&'b f64>, // 在 'b 上是不变的
    w: *mut U,              // 在 U 上是不变的, 所以让整个结构体在 U 上是不变的
}
}

trait 约束和生命周期约束

句法
TypeParamBounds :
   TypeParamBound ( + TypeParamBound )* +?

TypeParamBound :
      Lifetime | TraitBound

TraitBound :
      ?? ForLifetimes? TypePath
   | ( ?? ForLifetimes? TypePath )

LifetimeBounds :
   ( Lifetime + )* Lifetime?

Lifetime :
      LIFETIME_OR_LABEL
   | 'static
   | '_

trait约束和生命周期约束为泛型程序项提供了一种方法来限制将哪些类型和生命周期可被用作它们的参数。通过 where子句可以为任何泛型提供约束。对于某些常见的情况,也可以使用如下简写形式:

  • 跟在泛型参数声明之后的约束:fn f<A: Copy>() {}fn f<A> where A: Copy () {} 效果等价。
  • 在 trait声明中作为指定超类trait(supertraits) 约束时:trait Circle : Shape {} 等同于 trait Circle where Self : Shape {}
  • 在 trait声明中作为指定关联类型上的约束时:trait A { type B: Copy; } 等同于 trait A where Self::B: Copy { type B; }

在程序项上应用了约束就要求在使用该程序项时使用者必须满足这些约束。当对泛型程序项进行类型检查和借用检查时,约束可用来确认当前准备用来单态化此泛型的实例类型是否实现了约束给出的 trait。例如,给定 Ty: Trait

  • 在泛型函数体中,Trait 中的方法可以被 Ty类型的值调用。同样,Trait 上的相关常数也可以被使用。
  • Trait 上的关联类型可以被使用。
  • 带有 T: Trait约束的泛型函数或类型可以在使用 T 的地方替换使用 Ty

#![allow(unused)]
fn main() {
type Surface = i32;
trait Shape {
    fn draw(&self, Surface);
    fn name() -> &'static str;
}

fn draw_twice<T: Shape>(surface: Surface, sh: T) {
    sh.draw(surface);           // 能调用此方法上因为 T: Shape
    sh.draw(surface);
}

fn copy_and_draw_twice<T: Copy>(surface: Surface, sh: T) where T: Shape {
    let shape_copy = sh;        // sh 没有被使用移动语义移走,是因为 T: Copy
    draw_twice(surface, sh);    // 能使用泛型函数 draw_twice 是因为 T: Shape
}

struct Figure<S: Shape>(S, S);

fn name_figure<U: Shape>(
    figure: Figure<U>,          // 这里类型 Figure<U> 的格式正确是因为 U: Shape
) {
    println!(
        "Figure of two {}",
        U::name(),              // 可以使用关联函数
    );
}
}

trait 和生命周期约束也被用来命名 trait对象

?Sized

? 仅用于声明 Sized trait 可能不会被某类型参数或关联类型实现。?Sized 还不能用作其他类型的约束。

生命周期约束

生命周期约束可以应用于类型或其他生命周期。约束 'a: 'b 通常被解读为 'a'b 存活的时间久'a: 'b意味着 'a 持续的时间比 'b 长,所以只要 &'b () 有效,引用 &'a () 就有效。1


#![allow(unused)]
fn main() {
fn f<'a, 'b>(x: &'a i32, mut y: &'b i32) where 'a: 'b {
    y = x;                      // 因为 'a: 'b,所以&'a i32 是 &'b i32 的子类型
    let r: &'b &'a i32 = &&0;   // &'b &'a i32 格式合法是因为 'a: 'b
}
}

T: 'a 意味着 T 的所有生命周期参数都比 'a 存活得时间长。例如,如果 'a 是一个任意的(unconstrained)生命周期参数,那么 i32: 'static&'static str: 'a 都合法,但 Vec<&'a ()>: 'static 不合法。

高阶 trait 约束

可以在生命周期上再进行更高阶的类型约束。这些高阶约束指定了一个对所有生命周期都为真的约束。例如,像 for<'a> &'a T: PartialEq<i32> 这样的约束需要一个如下的实现


#![allow(unused)]
fn main() {
struct T;
impl<'a> PartialEq<i32> for &'a T {
    // ...
   fn eq(&self, other: &i32) -> bool {true}
}
}

这样就可以拿任意生命周期的 &'a Ti32 做比较啦。

下面这类场景只能使用高阶trait约束,因为引用的生命周期比函数的生命周期参数短:2


#![allow(unused)]
fn main() {
fn call_on_ref_zero<F>(f: F) where for<'a> F: Fn(&'a i32) {
    let zero = 0;
    f(&zero);}
}

译者注:译者下面举例代码可以和上面原文的代码对比着看。下面代码中,因为 F 没约束 'a,导致参数 f 引用了未经扩展生命周期的 zero


#![allow(unused)]
fn main() {
fn call_on_ref_zero<'a, F>(f: F) where F: Fn(&'a i32) {
    let zero = 0;
    f(&zero);
}
}

高阶生命周期也可以贴近 trait 来指定,唯一的区别是生命周期参数的作用域,像下面这样 'a 的作用域只扩展到后面跟的 trait 的末尾,而不是整个约束3。下面这个函数和上一个等价。


#![allow(unused)]
fn main() {
fn call_on_ref_zero<F>(f: F) where F: for<'a> Fn(&'a i32) {
    let zero = 0;
    f(&zero);
}
}
1

译者理解:理解这种关系时,可以把生命周期 'a'b 理解成去引用对象时需传入的参数,给定 'a: 'b 和类型 T,如果 'b T有效,那此时再传入 'a 就去引用 T 必定有效。

4

译者理解:高阶 trait约束就是对带生命周期的类型重新进行约束。像这句中的例子就是对 &'a T 加上了 PartialEq<i32> 的约束,其中 for<'a> 可以理解为:对于 'a 的所有可能选择。更多信息请参见:https://doc.rust-lang.org/std/cmp/trait.PartialEq.html 和 https://doc.rust-lang.org/nightly/nomicon/hrtb.html

2

译者理解此例中的代码 for<'a> F: Fn(&'a i32) 为:F 对于 'a 的所有可能选择都受 Fn(&'a i32) 的约束。

3

译者理解这句的意思是:如果 F 的约束有多个 trait,那这种方式里, 'a 的作用域只是扩展它后面紧跟的那个 trait 的方法,即 Fn(&'a i32) 里。

类型自动强转

类型自动强转是改变值的类型的隐式操作。它们在特定的位置自动发生,但实际自动强转的类型也受到很多限制。

任何允许自动强转的转换都可以由类型强制转换操作符 as 来显式执行。

自动强转最初是在 RFC 401 中定义的,并在[ RFC 1558] 中进行了扩展。

自动强转点

自动强转只能发生在程序中的某些自动强转点(coercion sites)上;典型的位置是那些所需的类型是显式给出了的地方,或者是那些可以从给出的显式类型传播推导(be derived by propagation)出所需的类型(注意这里不是类型推断)的地方。可能的强转点有:

  • let语句中显式给出了类型。

    例如,下面例子中 &mut 42 自动强转成 &i8 类型:

    
    #![allow(unused)]
    fn main() {
    let _: &i8 = &mut 42; // 译者注释:`&i8` 是显示给出的所需类型
    }
    
  • 静态(static)项和常量(const)项声明(类似于 let语句)。

  • 函数调用的参数

    被强制的值是实参(actual parameter),它的类型被自动强转为形参(formal parameter)的类型。

    例如,下面例子中 &mut 42 自动强转成 &i8 类型:

    fn bar(_: &i8) { }
    
    fn main() {
        bar(&mut 42);
    }
    

    对于方法调用,接受者(self参数)只能使用非固定尺寸类型自动强转(unsized coercion)

  • 实例化结构体、联合体或枚举变体的字段。

    例如,下面例子中 &mut 42 自动强转成 &i8 类型:

    struct Foo<'a> { x: &'a i8 }
    
    fn main() {
        Foo { x: &mut 42 };
    }
    
  • 函数结果—块中的最终表达式或者 return语句中的任何表达式。

    例如,下面例子中 x 将自动强转成 &dyn Display 类型:

    
    #![allow(unused)]
    fn main() {
    use std::fmt::Display;
    fn foo(x: &u32) -> &dyn Display {
        x
    }
    }
    

如果一在自动强转点中的表达式是自动强转传播型表达式(coercion-propagating expression),那么该表达式中的对应子表达式也是自动强转点。传播从这些新的自动强转点开始递归。传播表达式(propagating expressions)及其相关子表达式有:

  • 数组字面量,其数组的类型为 [U; n]。数组字面量中的每个子表达式都是自动强转到类型 U 的自动强转点。

  • 重复句法声明的数组字面量,其数组的类型为 [U; n]。重复子表达式是用于自动强转到类型 U 的自动强转点。

  • 元组,其中如果元组是自动强转到类型 (U_0, U_1, ..., U_n) 的强转点,则每个子表达式都是相应类型的自动强转点,比如第0个子表达式是到类型 U_0的 自动强转点。

  • 圆括号括起来的子表达式((e)):如果整个括号表达式的类型为 U,则子表达式 e 是自动强转到类型 U 的自动强转点。

  • 块:如果块的类型是 U,那么块中的最后一个表达式(如果它不是以分号结尾的)就是一个自动强转到类型 U 的自动强转点。这里的块包括作为控制流语句的一部分的条件分支代码块,比如 if/else,当然前提是这些块的返回需要有一个已知的类型。

自动强转类型

自动强转允许发生在下列类型之间:

  • TU 如果 TU 的一个子类型 (反射性场景(reflexive case))

  • T_1T_3T_1 可自动强转到 T_2 同时 T_2 又能自动强转到 T_3 (传递性场景(transitive case))

    注意这个还没有得到完全支持。

  • &mut T&T

  • *mut T*const T

  • &T*const T

  • &mut T*mut T

  • &T&mut T&U 如果 T 实现了 Deref<Target = U>。例如:

    use std::ops::Deref;
    
    struct CharContainer {
        value: char,
    }
    
    impl Deref for CharContainer {
        type Target = char;
    
        fn deref<'a>(&'a self) -> &'a char {
            &self.value
        }
    }
    
    fn foo(arg: &char) {}
    
    fn main() {
        let x = &mut CharContainer { value: 'y' };
        foo(x); //&mut CharContainer 自动强转成 &char.
    }
    
  • &mut T&mut U 如果 T 实现了 DerefMut<Target = U>.

  • TyCtor(T) 到 TyCtor(U),其中 TyCtor(T) 是下列之一1

    • &T
    • &mut T
    • *const T
    • *mut T
    • Box<T>

    并且 U 能够通过非固定尺寸类型自动强转得到。

  • 非捕获闭包(Non capturing closures)到函数指针(fn pointers)

  • ! 到任意 T

非固定尺寸类型自动强转

下列自动强转被称为非固定尺寸类型自动强转(unsized coercions),因为它们与将固定尺寸类型(sized types)转换为非固定尺寸类型(unsized types)有关,并且在一些其他自动强转不允许的情况(也就是上面罗列的情况之外的情况)下允许使用。也就是说它们可以发生在任何自动强转发生的地方。

UnsizeCoerceUnsized 这两个 trait 被用来协助这种转换的发生,并公开给标准库来使用。以下自动强转方式是内置的,并且,如果 T 可以用其中任一方式自动强转成 U,那么就会为 T 提供一个 Unsize<U> 的内置实现:

  • [T; n][T].

  • Tdyn U, 当 T 实现 U + Sized, 并且 U对象安全的时。

  • Foo<..., T, ...>Foo<..., U, ...>, 当:

    • Foo 是一个结构体。
    • T 实现了 Unsize<U>
    • Foo 的最后一个字段是和 T 相关的类型。
    • 如果这最后一个字段是类型 Bar<T>,那么 Bar<T> 实现了 Unsized<Bar<U>>
    • T 不是任何其他字段的类型的一部分。

此外,当 T 实现了 Unsize<U>CoerceUnsized<Foo<U>> 时,类型 Foo<T> 可以实现 CoerceUnsized<Foo<U>>。这就允许 Foo<T> 提供一个到 Foo<U> 的非固定尺寸类型自动强转。

注:虽然非固定尺寸类型自动强转的定义及其实现已经稳定下来,但 UnsizeCoerceUnsized 这两个 trait 本身还没稳定下来,因此还不能直接用于稳定版的 Rust。

最小上界自动强转

在某些上下文中,编译器必须将多个类型强制在一起,以尝试找到最通用的类型。这被称为“最小上界(Least Upper Bound,简称 LUB)”自动强转。LUB自动强转只在以下情况中使用:

  • 为一系列的 if分支查找共同的类型。
  • 为一系列的匹配臂查找共同的类型。
  • 为数组元素查找共同的类型。
  • 为带有多个返回项语句的闭包的返回类型查找共同的类型。
  • 检查带有多个返回语句的函数的返回类型。

在这每种情况下,都有一组类型 T0..Tn 被共同自动强转到某个未知的目标类型 T_t,注意开始时 T_t 是未知的。LUB 自动强转的计算过程是不断迭代的。首先把目标类型 T_t 定为从类型 T0 开始。对于每一种新类型 Ti,考虑如下步骤是否成立:

  • 如果 Ti 可以自动强转为当前目标类型 T_t,则不做任何更改。
  • 否则,检查 T_t 是否可以被自动强转为 Ti;如果是这样,T_t 就改为 Ti。(此检查还取决于到目前为止所考虑的所有源表达式是否带有隐式自动强转。)
  • 如果不是,尝试计算一个 T_tTi 的共同的超类型(supertype),此超类型将成为新的目标类型。

示例:


#![allow(unused)]
fn main() {
let (a, b, c) = (0, 1, 2);
// if分支的情况
let bar = if true {
    a
} else if false {
    b
} else {
    c
};

// 匹配臂的情况
let baw = match 42 {
    0 => a,
    1 => b,
    _ => c,
};

// 数组元素的情况
let bax = [a, b, c];

// 多个返回项语句的闭包的情况
let clo = || {
    if true {
        a
    } else if false {
        b
    } else {
        c
    }
};
let baz = clo();

// 检查带有多个返回语句的函数的情况
fn foo() -> i32 {
    let (a, b, c) = (0, 1, 2);
    match 42 {
        0 => a,
        1 => b,
        _ => c,
    }
}
}

在这些例子中,ba* 的类型可以通过 LUB自动强转找到。编译器检查 LUB自动强转在处理函数 foo 时,是否把 abc 的结果转为了 i32

附加说明

我们这种描述显然是非正式的,但目前使文字描述更精确的工作正作为精细化 Rust 类型检查器的一般性工作的一部分正紧锣密鼓的进行中。

1

TyCtor为类型构造器 type constructor 的简写。

析构函数

当一个初始化了的变量临时变量超出作用域时,其析构函数(destructor)将运行,或者说它将被销毁(dropped)。此外赋值操作也会运行其左操作数的析构函数(如果它已经初始化了)。如果变量已部分初始化了,则只销毁其已初始化的字段。

类型 T 的析构函数由以下内容组成:

  1. 如果有约束 T: Drop, 则调用 <T as std::ops::Drop>::drop
  2. 递归运行其所有字段的析构函数。
    • 结构体(struct)的字段按照声明顺序被销毁。
    • 活动枚举变体的字段按声明顺序销毁。
    • 元组中的字段按顺序销毁。
    • 数组或拥有所有权的切片的元素的销毁顺序是从第一个元素到最后一个元素。
    • 闭包通过移动(move)语义捕获的变量的销毁顺序未明确指定。
    • trait对象的销毁会运行其非具名基类(underlying type)的析构函数。
    • 其他类型不会导致任何进一步的销毁动作发生。

如果析构函数必须手动运行,比如在实现自定义的智能指针时,可以使用标准库函数 std::ptr::drop_in_place

举些(析构函数的)例子:


#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);

impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("{}", self.0);
    }
}

let mut overwritten = PrintOnDrop("当覆写时执行销毁");
overwritten = PrintOnDrop("当作用域结束时执行销毁");

let tuple = (PrintOnDrop("Tuple first"), PrintOnDrop("Tuple second"));

let moved;
// 没有析构函数在赋值时运行
moved = PrintOnDrop("Drops when moved");
// 这里执行销毁,但随后变量进入未初始化状态
moved;

// 未初始化不会被销毁
let uninitialized: PrintOnDrop;

// 在部分移动之后,后续销毁动作只销毁剩余字段。
let mut partial_move = (PrintOnDrop("first"), PrintOnDrop("forgotten"));
// 执行部分移出,只留下 `partial_move.0` 处于初始化状态
core::mem::forget(partial_move.1);
// 当 partial_move 的作用域结束时, 这里就只有第一个字段被销毁。
}

销毁作用域

每个变量或临时变量都与一个销毁作用域(drop scope)1相关联。当控制流离开一个销毁作用域时,与该作用域关联的所有变量都将按照其声明(对变量而言)或创建(对临时变量而言)的相反顺序销毁。

销毁作用域是在将 forif letwhile let 这些表达式替换为等效的 match表达式之后确定的。在确定销毁作用域这事儿上,对重载操作符与内置操作符不做区分2,匹配模式的变量绑定方式(binding modes)也不去考虑。

给定一个函数或闭包,存在以下的销毁作用域:

  • 整个函数
  • 每个语句
  • 每个表达式
  • 每个块,包括函数体
    • 当在块表达式上时,整个块和整个块表达式的销毁作用域是相同的
  • 匹配(match)表达式的每条匹配臂(arm)

销毁作用域相互嵌套如规则下。当同时离开多个作用域时,比如从函数返回时,变量会从内层的向层依次销毁。

  • 整个函数作用域是最外层的作用域。
  • 函数体块包含在整个函数作用域内。
  • 表达式语句中的表达式的父作用域是该语句的作用域。
  • let语句的初始化器(initializer)的父作用域是 let语句的作用域。
  • 语句作用域的父作用域是包含该语句的块作用域。
  • 匹配守卫(match guard)表达式的父作用域是该守卫所在的匹配臂的作用域。
  • 在匹配表达式(match expression)里的 => 之后的表达式的父作用域是此表达式对应的匹配臂所在的那个作用域。
  • 匹配臂的作用域的父作用域是它所在的匹配表达式(match expression)的作用域。
  • 所有其他作用域的父作用域都是直接封闭该表达式的作用域。

函数参数的作用域

所有函数参数都在整个函数体的作用域内有效,因此在对函数求值时,它们是最后被销毁的。实参会在其内部值被形参的模式绑定之后销毁。


#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
// 先销毁第二个参数, 接下来是 `y`, 然后是第一个参数r, 最后是 `x`
fn patterns_in_parameters(
    (x, _): (PrintOnDrop, PrintOnDrop),
    (_, y): (PrintOnDrop, PrintOnDrop),
) {}

// 销毁顺序是 3 2 0 1
patterns_in_parameters(
    (PrintOnDrop("0"), PrintOnDrop("1")),
    (PrintOnDrop("2"), PrintOnDrop("3")),
);
}

本地变量的作用域

let语句中声明的局部变量与包含此 let语句的块作用域相关联。在匹配(match)表达式中声明的局部变量与声明它们的匹配(match)臂的匹配臂作用域相关联。


#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
let declared_first = PrintOnDrop("在外层作用域内最后销毁");
{
    let declared_in_block = PrintOnDrop("在内层作用域内销毁");
}
let declared_last = PrintOnDrop("在外层作用域内最先销毁");
}

如果在一个匹配(match)表达式的同一个匹配臂中使用了多个模式,则毁顺序不确定。(译者注:这里译者不确定后半句翻译是否准确,这里给出原文:then an unspecified pattern will be used to determine the drop order.)

临时作用域

表达式的临时作用域用于该表达在位置上下文中求出的结果被保存进的那个临时变量的作用域。有些情况下,此表达式求出的结果会被提升,则此表达式不存在临时作用域。3

除了生存期扩展之外,表达式的临时作用域是包含该表达式的最小作用域,它适用于以下情况之一:

  • 整个函数体。
  • 一条语句。
  • if表达式、while表达式 或 loop表达式这三种表达式的代码体。
  • if表达式的 else块。
  • if表达式的条件表达式,while表达式的条件表达式,或匹配表达式中的匹配(match)守卫。
  • 匹配臂上的表达式。
  • 惰性布尔表达式的第二操作数。

注意

在函数体的最终表达式(final expression)中创建的临时变量会在任何具名变量销毁之后销毁,因为这里没有更小的封闭它的临时作用域。

匹配表达式的检验对象表达式本身不是一个临时作用域(,但它内部可以包含临时作用域),因此可以在匹配(match)表达式之后销毁检验对象表达式中的临时作用域。例如,match 1 { ref mut z => z }; 中的 1 所在的临时变量一直存活到此语句结束。

一些示例:


#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
let local_var = PrintOnDrop("local var");

// 在条件表达式执行后立即销毁
if PrintOnDrop("If condition").0 == "If condition" {
    // 在此块的末尾处销毁
    PrintOnDrop("If body").0
} else {
    unreachable!()
};

// 在此条语句的末尾处销毁
(PrintOnDrop("first operand").0 == ""
// 在 ) 处销毁
|| PrintOnDrop("second operand").0 == "")
// 在此表达式的末尾处销毁
|| PrintOnDrop("third operand").0 == "";

// 在函数末尾处,局部变量之后销毁之后销毁
// 将下面这段更改为一个包含返回(return)表达式的语句将使临时变量在本地变量之前被删除。
// 如果把此临时变量绑定到一个变量,然后返回这个变量,也会先删除这个临时变量
match PrintOnDrop("Matched value in final expression") {
    // 在条件表达式执行后立即销毁
    _ if PrintOnDrop("guard condition").0 == "" => (),
    _ => (),
}
}

操作数

在同一表达式中,在对其他操作数求值时,也会创建临时变量来将已求值的操作数的结果保存起来。临时变量与该操作数所属的表达式的作用域相关联。因为一旦表达式求值,临时变量就被移走了,所以销毁它们没有任何效果和意义,除非整个表达式的某一操作数出现异常,导致表达式求值失败,或提前返回,或触发了 panic。


#![allow(unused)]
fn main() {
struct PrintOnDrop(&'static str);
impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("drop({})", self.0);
    }
}
loop {
    // 元组表达式未结束求值就提前返回了,所以其操作数按声明的反序销毁
    (
        PrintOnDrop("Outer tuple first"),
        PrintOnDrop("Outer tuple second"),
        (
            PrintOnDrop("Inner tuple first"),
            PrintOnDrop("Inner tuple second"),
            break,
        ),
        PrintOnDrop("Never created"),
    );
}
}

常量提升

如果可以通过借用的方式将某一值表达式写入常量,并且还能通过解引用此借用的方式来解出原始写入的表达式,并且如果这种做法也不更改运行时行为,那 Rust 会将值表达式提升到静态('static) slot 作用域内。也就是说,提升的表达式可以在编译时求值,这求得的值不具备内部可变性或不包含析构函数(。这些属性是根据可能的值来确定的,例如 &None 的类型总是 &'static Option<_>,因为 &None 的值是唯一确定的)。

临时生存期扩展

注意:临时生存期扩展的确切规则可能还会改变。这里只描述了当前的行为表现。

let语句中表达式的临时作用域有时会扩展到包含此 let语句的块作用域内。根据某些句法规则,当通常的临时作用域太小时,就会这样做。例如:


#![allow(unused)]
fn main() {
let x = &mut 0;
// 通常上面存储0的临时变量(或者临时位置)到这里就会被丢弃,但这里是一直存在到块的末尾。
println!("{}", x);
}

如果一个借用、解引用、字段或元组索引表达式有一个扩展的临时作用域,那么它们的操作数也会同样扩展。如果索引表达式有扩展的临时作用域,那么被索引的表达式也会一并扩展。

基于模式的扩展

*扩展临时作用域的模式(extending pattern)*是下面任一:

所以 ref xV(ref x)[ref x, y] 都是扩展临时作用域的模式,但是 x&ref x&(ref x,) 不是。

如果 let语句中的模式是扩展临时作用域的模式,那么初始化器表达式中的临时作用域将被扩展。

基于表达式的扩展

对于带有初始化器的 let语句来说,*扩展临时作用域的表达式(extending expression)*是以下表达式之一:

因此,在 &mut 0(&1, &mut 2)Some { 0: &mut 3 } 中的借用表达式都是扩展临时作用域的表达式。在 &0 + &1 和一些 Some(&mut 0) 中的借用不是:它们在句法上是函数调用表达式。

任何扩展了临时作用域的借用表达式的操作数的临时作用域都随此表达式的临时作用域的扩展而扩展。

示例

这是一些带有扩展的临时作用域的表达式:


#![allow(unused)]
fn main() {
fn temp() {}
trait Use { fn use_temp(&self) -> &Self { self } }
impl Use for () {}
// 在这些情况下,存储 `temp()` 结果的临时变量与x在同一个作用域中。
let x = &temp();
let x = &temp() as &dyn Send;
let x = (&*&temp(),);
let x = { [Some { 0: &temp(), }] };
let ref x = temp();
let ref x = *&temp();
x;
}

下面是一些表达式没有扩展临时作用域的例子:


#![allow(unused)]
fn main() {
fn temp() {}
trait Use { fn use_temp(&self) -> &Self { self } }
impl Use for () {}
// 在这些情况下,存储 `temp()` 结果的临时变量只存活到 let语句结束。

let x = Some(&temp());         // ERROR
let x = (&temp()).use_temp();  // ERROR
x;
}

阻断运行析构函数

在 Rust 中,即便类型不是 'static,禁止运行析构函数也是允许的,也是安全的。std::mem::ManuallyDrop 提供了一个包装器(wrapper)来防止变量或字段被自动销毁。

1

后文有时也直接简称为作用域。

2

这里说这句是因为操作符的操作数也涉及到销毁作用域范围的确定。

3

对这句话,这里译者按自己的理解再翻译一遍:一个表达式在位置表达式上使用时会被求值,如果此时没有具名变量和此值绑定,那就会先被保存进一个临时变量里,临时作用域就是伴随这此临时变量而生成。此作用域通常在此表达式所在的语句结束时结束,但如果求出的值被通过借用绑定给具名变量,此作用域会扩展到此具名变量的作用域(后面生存期扩展会讲到)。如果求出的值比较特殊,这个作用域还会提升到全局作用域,这就是所谓的常量提升

生命周期(类型参数)省略

Rust 拥有一套允许在多种位置省略生命周期的规则,但前提是编译器在这些位置上能推断出合理的默认生命周期。

函数上的生命周期(类型参数)省略

为了使常用模式使用起来更方便,可以在函数项函数指针闭包trait1的签名中省略生命周期类型参数。以下规则用于推断出被省略的生命周期类型参数。省略不能被推断出的生命周期类型参数是错误的。占位符形式的生命周期,'_,也可以用这一套规则来推断出。对于路径中的生命周期,首选使用 '_。trait对象的生命周期类型参数遵循不同的规则,具体这里讨论。

  • 参数中省略的每个生命周期类型参数都会(被推断)成为一个独立的生命周期类型参数。
  • 如果参数中只使用了一个生命周期(省略或不省略都行),则将该生命周期作为所有省略的输出生命周期类型参数。

在方法签名中有另一条规则

  • 如果接受者(receiver)类型为 &Self&mut Self,那么对 Self 的引用的生命周期会被作为所有省略的输出生命周期类型参数。

示例:


#![allow(unused)]
fn main() {
trait T {}
trait ToCStr {}
struct Thing<'a> {f: &'a i32}
struct Command;

trait Example {
fn print1(s: &str);                                   // 省略
fn print2(s: &'_ str);                                // 也省略
fn print3<'a>(s: &'a str);                            // 未省略

fn debug1(lvl: usize, s: &str);                       // 省略
fn debug2<'a>(lvl: usize, s: &'a str);                // 未省略

fn substr1(s: &str, until: usize) -> &str;            // 省略
fn substr2<'a>(s: &'a str, until: usize) -> &'a str;  // 未省略

fn get_mut1(&mut self) -> &mut dyn T;                 // 省略
fn get_mut2<'a>(&'a mut self) -> &'a mut dyn T;       // 未省略

fn args1<T: ToCStr>(&mut self, args: &[T]) -> &mut Command;                  // 省略
fn args2<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command; // 未省略

fn new1(buf: &mut [u8]) -> Thing<'_>;                 // 省略 - 首选的
fn new2(buf: &mut [u8]) -> Thing;                     // 省略
fn new3<'a>(buf: &'a mut [u8]) -> Thing<'a>;          // 未省略
}

type FunPtr1 = fn(&str) -> &str;                      // 省略
type FunPtr2 = for<'a> fn(&'a str) -> &'a str;        // 未省略

type FunTrait1 = dyn Fn(&str) -> &str;                // 省略
type FunTrait2 = dyn for<'a> Fn(&'a str) -> &'a str;  // 未省略
}

#![allow(unused)]
fn main() {
// 下面的示例展示了不允许省略生命周期类型参数的情况。

trait Example {
// 无法推断,因为没有可以推断的起始参数。
fn get_str() -> &str;                                 // 非法

// 无法推断,这里无法确认输出的生命周期类型参数该遵从从第一个还是第二个参数的。
fn frob(s: &str, t: &str) -> &str;                    // 非法
}
}

默认的 trait对象的生命周期

假定存在于(代表) trait对象(的那个胖指针)上的生命周期(assumed lifetime)类型参数称为此 trait对象的默认对象生命周期约束(default object lifetime bound)。这些在 RFC 599 中定义,在 RFC 1156 中修定增补。

当 trait对象的生命周期约束被完全省略时,会使用默认对象生命周期约束来替代上面定义的生命周期类型参数省略规则。但如果使用 '_ 作为生命周期约束,则该约束仍遵循上面通常的省略规则。

如果将 trait对象用作泛型类型的类型参数,则首先使用此容器泛型来尝试为此 trait对象推断一个约束(来替代那个假定的生命周期)。

  • 如果存在来自此容器泛型的唯一约束,则该约束就为此 trait对象的默认约束
  • 如果此容器泛型有多个约束,则必须指定一个约显式束为此 trait对象的默认约束

如果这两个规则都不适用,则使用该 trait对象的声明时的 trait约束:

  • 如果原 trait 声明为单生命周期约束,则此 trait对象使用该约束作为默认约束。
  • 如果 'static 被用做原 trait声明的任一一个生命周期约束,则此 trait对象使用 'static 作为默认约束。
  • 如果原 trait声明没有生命周期约束,那么此 trait对象的生命周期会在表达式中根据上下文被推断出来,在表达式之外直接用 'static

#![allow(unused)]
fn main() {
// 对下面的 trait 来说,...
trait Foo { }

// 这两个是等价的,就如 `Box<T>` 对 `T` 没有生命周期约束一样
// These two are the same as Box<T> has no lifetime bound on T
type T1 = Box<dyn Foo>;  //译者注:此处的 `T1` 和 上行备注中提到的 `Box<T>` 都是本节规则中所说的泛型类型,即容器泛型
type T2 = Box<dyn Foo + 'static>;

// ...这也是等价的
impl dyn Foo {}
impl dyn Foo + 'static {}

// ...这也是等价的, 因为 `&'a T` 需要 `T: 'a`
type T3<'a> = &'a dyn Foo;
type T4<'a> = &'a (dyn Foo + 'a);

// `std::cell::Ref<'a, T>` 也需要 `T: 'a`, 所以这俩也是等价的
type T5<'a> = std::cell::Ref<'a, dyn Foo>;
type T6<'a> = std::cell::Ref<'a, dyn Foo + 'a>;
}

#![allow(unused)]
fn main() {
// 这是一个反面示例:
trait Foo { }
struct TwoBounds<'a, 'b, T: ?Sized + 'a + 'b> {
    f1: &'a i32,
    f2: &'b i32,
    f3: T,
}
type T7<'a, 'b> = TwoBounds<'a, 'b, dyn Foo>;
//                                  ^^^^^^^
// 错误: 不能从上下文推导出此对象类型的生命周期约束
}

注意,像 &'a Box<dyn Foo> 这样多层包装的,只需要看最内层包装 dyn Foo 的那层,所以扩展后仍然为 &'a Box<dyn Foo + 'static>


#![allow(unused)]
fn main() {
// 对下面的 trait 来说,...
trait Bar<'a>: 'a { }

// ...这两个是等价的:
type T1<'a> = Box<dyn Bar<'a>>;
type T2<'a> = Box<dyn Bar<'a> + 'a>;

// ...这俩也是等价的:
impl<'a> dyn Bar<'a> {}
impl<'a> dyn Bar<'a> + 'a {}
}

静态('static)生命周期省略

除非指定了显式的生命周期,引用类型的常量项声明和静态项声明都具有隐式的静态('static)生命周期。因此,有 'static生命周期的常量项声明在编写时可以略去其生命周期。


#![allow(unused)]
fn main() {
// STRING: &'static str
const STRING: &str = "bitstring";

struct BitsNStrings<'a> {
    mybits: [u32; 2],
    mystring: &'a str,
}

// BITS_N_STRINGS: BitsNStrings<'static>
const BITS_N_STRINGS: BitsNStrings<'_> = BitsNStrings {
    mybits: [1, 2],
    mystring: STRING,
};
}

注意,如果静态项(static)或常量项(const)包含对函数或闭包的引用,而这些函数或闭包本身也包含引用,此时编译器将首先尝试使用标准的省略规则来推断生命周期类型参数。如果它不能通过通常的生命周期省略规则来推断出生命周期类型参数,那么它将报错。举个例子:


#![allow(unused)]
fn main() {
struct Foo;
struct Bar;
struct Baz;
fn somefunc(a: &Foo, b: &Bar, c: &Baz) -> usize {42}
// 解析为 `fn<'a>(&'a str) -> &'a str`.
const RESOLVED_SINGLE: fn(&str) -> &str = |x| x;

// 解析为 `Fn<'a, 'b, 'c>(&'a Foo, &'b Bar, &'c Baz) -> usize`.
const RESOLVED_MULTIPLE: &dyn Fn(&Foo, &Bar, &Baz) -> usize = &somefunc;
}

#![allow(unused)]
fn main() {
struct Foo;
struct Bar;
struct Baz;
fn somefunc<'a,'b>(a: &'a Foo, b: &'b Bar) -> &'a Baz {unimplemented!()}
// 没有足够的信息将返回值的生命周期与参数的生命周期绑定起来,因此这是一个错误
const RESOLVED_STATIC: &dyn Fn(&Foo, &Bar) -> &Baz = &somefunc;
//                                            ^
// 这个函数的返回类型包含一个借用来的值,但是签名没有说明它是从参数1还是从参数2借用来的
}
1

指 Fn、FnMute 和 FnOnce 这三个 trait。

评论 (0)