跳转到内容

Rust 高级泛型:const 泛型、const fn 与 PhantomData

在泛型那一页,泛型参数总是一个类型:Pair<T> 可以装 i32,也可以装 &str。这一页介绍三个工具,把同样的思路再往前推一步:

  • const 泛型(const generics)让一个数字成为泛型参数,就像 [i32; 3] 里的那个 3。
  • const fn 让编译器在构建程序时就运行你的函数,程序启动之前答案就已经准备好了。
  • PhantomData 让一个类型带上标签(比如“这是用户 ID”或者“这个长度的单位是英尺”),却不用额外存储任何数据。

想想鸡蛋盒。6 个装和 12 个装的鸡蛋盒是两种不同的商品,虽然它们都叫“鸡蛋盒”。容量是它们本身的一部分。const 泛型让你在 Rust 里表达这一点。这一页可以慢慢读:每一节都能单独看,后面几节等你需要的时候再回来看也不迟。

const 泛型:把数字当作泛型参数

Section titled “const 泛型:把数字当作泛型参数”

在 Rust 里,数组的长度是它类型的一部分。[u32; 3] 和 [u32; 4] 的差别,就像 i32 和 String 那么大。一个接收 [u32; 3] 的函数不会接受 4 个元素:

error[E0308]: mismatched types
--> src/main.rs:7:26
|
7 | println!("{}", total(basket));
| ----- ^^^^^^ expected an array with a size of 3, found one with a size of 4
| |
| arguments to this function are incorrect

const 泛型可以解决这个问题。在尖括号里写上 const N: usize,函数就能处理任意长度的数组:

fn summary<const N: usize>(prices: [u32; N]) -> String {
let mut sum = 0;
for price in prices {
sum += price;
}
// 在函数内部,N 就是一个普通的数字
format!("{N} 件商品共 {sum} 元")
}
fn main() {
let small_basket = [4, 10, 2];
let big_basket = [3, 8, 1, 12, 6];
println!("{}", summary(small_basket));
println!("{}", summary(big_basket));
}

输出:

3 件商品共 16 元
5 件商品共 30 元
▶ 动手试一试

<const N: usize> 可以读作“对任意一个 usize 类型的数字 N”。Rust 会根据你传进去的数组推断出 N,就像普通泛型推断 T 一样。也和普通泛型一样,编译器会为你用到的每一种长度各生成一份 summary,所以没有运行时开销。

说句实话:对这个小函数来说,用 slice(prices: &[u32])一样好用,而且更简单,详见 slice。const 泛型真正派上用场,是在长度必须成为类型的一部分的时候:不用堆分配就能存进结构体,或者出现在返回类型里。下面就来看。

想象一张电影院的座位表:有若干排,每排的座位数一样。下面这个矩阵的行数 R 和列数 C 都是它类型的一部分:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new(cells: [[i32; C]; R]) -> Self {
Matrix { cells }
}
fn size(&self) -> String {
format!("{R}x{C}")
}
// 行变成列:R x C 的矩阵变成 C x R 的矩阵
fn transpose(&self) -> Matrix<C, R> {
let mut cells = [[0; R]; C];
for row in 0..R {
for col in 0..C {
cells[col][row] = self.cells[row][col];
}
}
Matrix { cells }
}
}
fn main() {
let seats = Matrix::new([[1, 2, 3], [4, 5, 6]]);
let flipped = seats.transpose();
println!("{} {:?}", seats.size(), seats.cells);
println!("{} {:?}", flipped.size(), flipped.cells);
}

输出:

2x3 [[1, 2, 3], [4, 5, 6]]
3x2 [[1, 4], [2, 5], [3, 6]]
▶ 动手试一试

看看 transpose 的返回类型:Matrix<C, R>。编译器知道 2x3 的矩阵转置后是 3x2。如果你在函数里写错了,比如造出了形状不对的矩阵,代码就编译不过。整张表都放在结构体里,没有 Vec,也不用堆。impl<const R: usize, const C: usize> 这一行的作用和 impl<T> 一样:它表示“这些方法对每一种尺寸都存在”。

实用示例:固定大小的环形缓冲区

Section titled “实用示例:固定大小的环形缓冲区”

行车记录仪只保留最近几分钟的视频。存满之后,最新的录像会覆盖最旧的。这就是环形缓冲区(ring buffer),它和 const 泛型是绝配:容量固定,所以可以把元素存在数组里。

这个例子同时对类型 T 和大小 N 泛型,所以 impl 那一行写成 impl<T, const N: usize>:

struct RingBuffer<T, const N: usize> {
items: [T; N],
next: usize, // 下一次 push 写入的位置
len: usize, // 已存储的元素个数(永远不超过 N)
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
fn new() -> Self {
RingBuffer { items: [T::default(); N], next: 0, len: 0 }
}
fn push(&mut self, item: T) {
self.items[self.next] = item; // 存满时覆盖最旧的元素
self.next = (self.next + 1) % N;
if self.len < N {
self.len += 1;
}
}
// 返回已存储的元素,最旧的在前
fn to_vec(&self) -> Vec<T> {
let start = (self.next + N - self.len) % N;
(0..self.len).map(|i| self.items[(start + i) % N]).collect()
}
}
fn main() {
// 只保留最近 3 次温度读数
let mut recent: RingBuffer<i32, 3> = RingBuffer::new();
for temp in [18, 19, 21, 24, 22] {
recent.push(temp);
println!("加入 {temp} 后:{:?}", recent.to_vec());
}
}

输出:

加入 18 后:[18]
加入 19 后:[18, 19]
加入 21 后:[18, 19, 21]
加入 24 后:[19, 21, 24]
加入 22 后:[21, 24, 22]
▶ 动手试一试

[T::default(); N] 用 N 份默认值(i32 的默认值是 0)填满数组。像这样重复一个值需要 T: Copy,所以 impl 要求 Copy + Default。RingBuffer<i32, 3> 和 RingBuffer<i32, 60> 是两个不同的类型,你永远不可能把 3 格的缓冲区传给需要 60 格的地方。如果你想用标准库里可增长的版本,请看 VecDeque 与 BinaryHeap。

const 参数可以有默认值,写成 const N: usize = 4。你也可以传入一个具名常量,或者用花括号包起来的表达式:

struct Buffer<const N: usize = 4> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self {
Buffer { data: [0; N] }
}
}
const PAGE: usize = 8;
fn main() {
let small: Buffer = Buffer::new(); // 不写数字:使用默认值 4
let page = Buffer::<PAGE>::new(); // 具名常量
let double = Buffer::<{ PAGE * 2 }>::new(); // 表达式需要 { }
println!("{} {} {}", small.data.len(), page.data.len(), double.data.len());
}

输出:

4 8 16
▶ 动手试一试

只有当你写出类型而不写数字时(比如 let small: Buffer),默认值才会生效。光写 let small = Buffer::new(); 会报 error[E0284]: type annotations needed for Buffer<_>,因为 Rust 不会根据表达式去猜默认值。

稳定版 Rust 的 const 泛型很实用,但有意做了限制。在撞上这些边界之前先了解一下,会省不少事:

  • 允许的类型:只有整数(usize、u8、i32……)、bool 和 char。如果写 const NAME: &'static str,会得到“&'static str is forbidden as the type of a const generic parameter”。
  • 不能在类型里用泛型参数做运算。在泛型结构体或函数签名里写 [f32; N * 2] 会报错(“generic parameters may not be used in const operations”)。单独写 N 没问题;在函数体里写 N * 2 也没问题,因为在那里 N 只是一个值。能让类型里出现 N * 2 的特性(generic_const_exprs)目前仍然只在 nightly 版可用。
  • 检查取值。你没法写“N 必须大于 0”这样的约束。最接近的办法是在函数里写一个内联 const 块:const { assert!(N > 0, "need at least one slot") };。当你用不合法的 N 构建程序时,它会报错(cargo check 不一定能发现)。

有些商店会把价目表提前印好,而不是在收银台一个个现算价格。const fn 就是编译器在构建程序时可以运行的函数,所以答案会提前“印”进程序里。

你已经在变量那一页见过 const 项。它的值必须在编译期就确定,所以通常只能用字面量和简单的运算。有了 const fn,你也可以在那里调用自己写的函数:

const fn square(n: usize) -> usize {
n * n
}
const BOARD_SIDE: usize = 4;
const CELLS: usize = square(BOARD_SIDE); // 由编译器算出来
fn main() {
// 数组长度必须在编译期确定,而 CELLS 正是如此
let board = [0u8; CELLS];
println!("{BOARD_SIDE}x{BOARD_SIDE} 的棋盘有 {} 个格子", board.len());
println!("运行时计算 square(12) 得 {}", square(12));
}

输出:

4x4 的棋盘有 16 个格子
运行时计算 square(12) 得 144
▶ 动手试一试

const fn 仍然是一个普通函数。main 里的 square(12) 就是运行时的一次普通调用。只有在常量上下文(const context)中使用时,它才必须在编译期运行:const 或 static 项、数组长度、const 泛型实参,或者 const { ... } 块。

一个经典用法是查找表:在编译期把一组值算好,运行时直接读取。下面让编译器为平年生成“每个月的第一天之前已经过了多少天”这张表:

const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const fn days_before_each_month() -> [u32; 12] {
let mut table = [0; 12];
let mut month = 1;
while month < 12 {
table[month] = table[month - 1] + DAYS_IN_MONTH[month - 1];
month += 1;
}
table
}
// 由编译器生成一次。运行时它只是 12 个数字。
const DAYS_BEFORE: [u32; 12] = days_before_each_month();
fn day_of_year(month: usize, day: u32) -> u32 {
DAYS_BEFORE[month - 1] + day
}
fn main() {
println!("{:?}", DAYS_BEFORE);
println!("3 月 1 日是第 {} 天", day_of_year(3, 1));
println!("12 月 25 日是第 {} 天", day_of_year(12, 25));
}

输出:

[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
3 月 1 日是第 60 天
12 月 25 日是第 359 天
▶ 动手试一试

注意这里用的是 while 循环,而不是 for 循环。这不是风格偏好。下面是目前在 const fn 里可以使用的东西:

  • let 和 let mut、算术运算、比较、if、match、loop 和 while
  • 数组、元组、结构体和枚举,以及 table[month] 这样的索引
  • 调用其他 const fn(很多标准库函数都是 const,比如 u32::pow 或 slice.len())
  • &mut 引用和浮点运算(分别从 Rust 1.83 和 1.82 起稳定)
  • panic! 和 assert!,在编译期触发时会变成编译错误

目前还不能使用的有:for 循环(它需要 Iterator trait,而 trait 方法在稳定版里还不是 const)、调用 String::from 或 println! 这样的普通函数,以及任何需要在堆上分配的操作,比如往 Vec 里 push。

const 还是 static?两者都是永远不会改变的全局值,但存储方式不同:

const static
存放位置 复制到每个使用它的地方 内存中一个固定的位置
地址 每次可能不同 永远相同,存活于整个程序期间('static)
能否可变 永远不能 有 static mut,但需要 unsafe
适用场景 数字、小型表格、配置项 想只存一份的大表,或者需要拿到引用的全局变量

拿不准的时候,就用 const。如果需要一个可以修改的全局变量,请用内部带原子类型或 Mutex 的 static,而不是 static mut。详见共享状态。

下面这个 bug 在真实系统里经常发生。用户 ID 和订单 ID 都只是数字,所以没有任何东西能阻止你把用户 ID 传给 cancel_order。你希望类型能带上一个标签:Id<User> 和 Id<Order>。你的第一次尝试可能是这样的:

struct Id<T> {
value: u32,
}
error[E0392]: type parameter `T` is never used
--> src/main.rs:1:11
|
1 | struct Id<T> {
| ^ unused type parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`

Rust 不接受没有被任何字段使用的类型参数。解决办法就是编译器建议的那个。std::marker::PhantomData<T> 是一个字段,它表明“这个结构体和 T 有关”,但里面什么也不存。它占用零字节:

use std::marker::PhantomData;
struct User;
struct Order;
struct Id<T> {
value: u32,
_marker: PhantomData<T>, // 零字节,只是一个标签
}
impl<T> Id<T> {
fn new(value: u32) -> Self {
Id { value, _marker: PhantomData }
}
}
fn find_user(id: &Id<User>) -> &'static str {
if id.value == 7 { "小明" } else { "未知" }
}
fn cancel_order(id: &Id<Order>) {
println!("正在取消订单 #{}", id.value);
}
fn main() {
let user_id: Id<User> = Id::new(7);
let order_id: Id<Order> = Id::new(7);
println!("用户 #{} 是{}", user_id.value, find_user(&user_id));
cancel_order(&order_id);
println!("一个 Id 占 {} 字节", std::mem::size_of::<Id<User>>());
}

输出:

用户 #7 是小明
正在取消订单 #7
一个 Id 占 4 字节
▶ 动手试一试

两个 ID 里存的都是数字 7,而且都占 4 字节,和普通的 u32 完全一样。但现在把 cancel_order(&user_id); 加到 main 的最后一行:

error[E0308]: mismatched types
--> src/main.rs:32:18
|
32 | cancel_order(&user_id);
| ------------ ^^^^^^^^ expected `&Id<Order>`, found `&Id<User>`
| |
| arguments to this function are incorrect

程序还没运行,这个混用就被发现了。PhantomData 既是类型也是值,所以构造结构体时要写 _marker: PhantomData。开头的 _ 只是一个常见的命名习惯,表示“这个字段永远不会被读取”。

1999 年,NASA 损失了火星气候探测者号(Mars Climate Orbiter),原因是一个软件输出的是英制单位,另一个软件期望的却是公制单位。两边都“只是数字”。有了 PhantomData,单位就成了类型的一部分,而且一个泛型 Length<U> 就能涵盖所有单位:

use std::marker::PhantomData;
use std::ops::Add;
struct Meters;
struct Feet;
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
}
// 一个 impl 适用于所有单位,但两边必须是同一个单位
impl<U> Add for Length<U> {
type Output = Length<U>;
fn add(self, other: Length<U>) -> Length<U> {
Length::new(self.value + other.value)
}
}
impl Length<Feet> {
fn to_meters(self) -> Length<Meters> {
Length::new(self.value * 0.3048)
}
}
fn main() {
let run: Length<Meters> = Length::new(400.0) + Length::new(250.0);
let climb: Length<Feet> = Length::new(1000.0);
let climb_m = climb.to_meters();
println!("爬升:{:.1} 米", climb_m.value);
let total = run + climb_m;
println!("总计:{:.1} 米", total.value);
}

输出:

爬升:304.8 米
总计:954.8 米
▶ 动手试一试

如果写成 run + climb(忘了 to_meters()),Rust 会拒绝:

error[E0308]: mismatched types
--> src/main.rs:39:23
|
39 | let total = run + climb;
| ^^^^^ expected `Length<Meters>`, found `Length<Feet>`

在高级 trait 那一页,Meters 和 Centimeters 是两个独立的结构体,各自有自己的 Add 实现。而这里,一个 impl<U> Add for Length<U> 就能服务你以后要相加的所有单位。

同样的技巧还可以用来跟踪某个东西的状态。一扇门可以是开着、关着或者锁着的。关着的门可以上锁,但给开着的门上锁就说不通了。把状态标签放进类型里,非法的操作干脆就不存在了:

use std::marker::PhantomData;
struct Open;
struct Closed;
struct Locked;
struct Door<State> {
room: String,
_state: PhantomData<State>,
}
// 在任何状态下都可用
impl<State> Door<State> {
fn room(&self) -> &str {
&self.room
}
// 还是同一扇门,换上新的状态标签
fn into_state<Next>(self, action: &str) -> Door<Next> {
println!("{}:{action}", self.room);
Door { room: self.room, _state: PhantomData }
}
}
impl Door<Closed> {
fn new(room: &str) -> Self {
Door { room: room.to_string(), _state: PhantomData }
}
fn open(self) -> Door<Open> {
self.into_state("已打开")
}
fn lock(self) -> Door<Locked> {
self.into_state("已上锁")
}
}
impl Door<Open> {
fn close(self) -> Door<Closed> {
self.into_state("已关上")
}
}
impl Door<Locked> {
fn unlock(self) -> Door<Closed> {
self.into_state("已解锁")
}
}
fn main() {
let door = Door::new("机房").open().close().lock();
println!("{}晚上锁好了", door.room());
let door = door.unlock().open();
println!("{}又打开了", door.room());
}

输出:

机房:已打开
机房:已关上
机房:已上锁
机房晚上锁好了
机房:已解锁
机房:已打开
机房又打开了
▶ 动手试一试

现在试着给开着的门上锁:Door::new("机房").open().lock():

error[E0599]: no method named `lock` found for struct `Door<Open>` in the current scope
--> src/main.rs:50:39
|
50 | let door = Door::new("机房").open().lock();
| ^^^^ method not found in `Door<Open>`
|
= note: the method was found for `Door<Closed>`

每个方法都按值获取 self,所以旧的门会被消耗掉,打开之后你就不能再继续使用那扇“关着的”门了。设计模式那一页展示了同样的 typestate 思路,只不过每个状态还各自带着数据。当状态只是纯粹的标签时,PhantomData 是更轻量的版本。

只针对某种尺寸的方法,以及关联常量

Section titled “只针对某种尺寸的方法,以及关联常量”

你在泛型那一页见过 impl Pair<f64>:只为某一个具体类型定义的方法。const 泛型也可以这样用。有些方法只对特定尺寸才有意义:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
// 只适用于方阵:行数和列数都是同一个 N
impl<const N: usize> Matrix<N, N> {
fn trace(&self) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += self.cells[i][i];
}
sum
}
}
// 只适用于 2x2 矩阵
impl Matrix<2, 2> {
fn determinant(&self) -> i32 {
let [[a, b], [c, d]] = self.cells;
a * d - b * c
}
}
fn main() {
let small = Matrix { cells: [[3, 8], [4, 6]] };
let big = Matrix { cells: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] };
println!("小矩阵:迹为 {},行列式为 {}", small.trace(), small.determinant());
println!("大矩阵:迹为 {}", big.trace());
}

输出:

小矩阵:迹为 9,行列式为 -14
大矩阵:迹为 15
▶ 动手试一试

big.determinant() 编译不过,在 2x3 的矩阵上调用 trace() 也一样:你会得到类似 error[E0599]: no method named trace found for struct Matrix<2, 3> 的错误,并附带提示 the method was found for Matrix<N, N>。“只有方阵才有迹”这条规则,现在写进了类型里。

关联常量(associated const)是属于某个类型或 trait 的常量,就像方法属于它们一样。在 trait 里,每个实现选定自己的值,泛型代码可以用 U::NAME 读取它。我们给前面的单位加上符号和换算大小:

use std::fmt;
use std::marker::PhantomData;
trait Unit {
const SYMBOL: &'static str;
const IN_METERS: f64; // 一个该单位等于多少米
}
struct Meters;
struct Feet;
struct Miles;
impl Unit for Meters {
const SYMBOL: &'static str = "m";
const IN_METERS: f64 = 1.0;
}
impl Unit for Feet {
const SYMBOL: &'static str = "ft";
const IN_METERS: f64 = 0.3048;
}
impl Unit for Miles {
const SYMBOL: &'static str = "mi";
const IN_METERS: f64 = 1609.344;
}
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U: Unit> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
// 利用各自的常量,在任意两个单位之间换算
fn convert<To: Unit>(&self) -> Length<To> {
Length::new(self.value * U::IN_METERS / To::IN_METERS)
}
}
impl<U: Unit> fmt::Display for Length<U> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2} {}", self.value, U::SYMBOL)
}
}
fn main() {
let marathon: Length<Miles> = Length::new(26.2);
let everest: Length<Feet> = Length::new(29032.0);
println!("{} = {}", marathon, marathon.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Miles>());
}

输出:

26.20 mi = 42164.81 m
29032.00 ft = 8848.95 m
29032.00 ft = 5.50 mi
▶ 动手试一试

U::IN_METERS 在编译期就查好了,运行时不需要去查什么表。新增一个单位,比如 Kilometers,只要写一个小小的 impl Unit 块,convert 和 Display 立刻就能用在它身上。固有 impl 也可以有关联常量,例如 impl<T, const N: usize> RingBuffer<T, N> { const CAPACITY: usize = N; },读取时写 RingBuffer::<i32, 3>::CAPACITY。

如果你想为所有满足某个条件的类型实现同一个 trait,那就是 blanket 实现,在高级 trait 那一页有讲解。

这些特性很强大,也正因为如此很容易让人忍不住想用。说几句实在话:

  • slice 或 Vec 往往就够了。如果函数只是读取元素,&[T] 能接受任意长度,读起来也更简单。如果长度只有在运行时才知道(来自文件、用户输入),你反正也得用 Vec。
  • 大数组放在栈上。RingBuffer<u8, 10_000_000> 会在结构体里占 10 MB,可能导致栈溢出。把它放进 Box(见智能指针),或者改用 Vec。
  • 每多一种尺寸或类型,就多一份代码。一个泛型用了 20 个不同的 N,就会生成 20 个版本。这会增加编译时间和二进制体积。详见性能。
  • typestate 会增加类型,报错信息也更长。它非常适合很多人都会用的 API(builder、连接、支付流程)。如果状态取决于运行时数据,比如从数据库里读出来的状态,请用枚举和 match。

一条好用的原则:先从简单的写法开始,等真的出现了这些工具能防止的 bug 或开销时,再把它们拿出来。

在类型里用 const 参数做运算。这样写感觉很自然,但稳定版 Rust 目前还不允许:

struct Pairs<const N: usize> {
values: [f32; N * 2],
}
error: generic parameters may not be used in const operations
--> src/main.rs:2:19
|
2 | values: [f32; N * 2],
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`

解决办法:改用 [[f32; 2]; N](由数对组成的数组),再加一个参数,或者在尺寸不需要出现在类型里时改用 Vec。

忘了给表达式加花括号。Buffer::<2 + 2>::new() 会报 error: expressions must be enclosed in braces to be used as const generic arguments,help 那一行给出了修改方法:Buffer::<{ 2 + 2 }>::new()。单独的数字或 const 名字不需要花括号。

在常量上下文里调用了不是 const 的东西。const 不能调用普通函数,const fn 里也不能用 for 循环:

const GREETING: String = String::from("hello");
error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants
--> src/main.rs:1:26
|
1 | const GREETING: String = String::from("hello");
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants

如果是文本,就用 const GREETING: &str = "hello";。在 const fn 里,把 for 换成 while 循环加索引,就像上面的查找表那样。

给带 PhantomData 的结构体加 #[derive(Clone, Copy)]。你会以为 Id<User> 是 Copy 的,毕竟它只存了一个 u32。但 derive 会加上 T: Copy 这个约束,而 User 不是 Copy,所以 ID 会被移动:

error[E0382]: use of moved value: `id`
--> src/main.rs:16:10
|
14 | let id: Id<User> = Id { value: 1, _marker: PhantomData };
| -- move occurs because `id` has type `Id<User>`, which does not implement the `Copy` trait
15 | show(id);
| -- value moved here
16 | show(id);
| ^^ value used here after move
|
note: if all bounds were met, you could clone the value
--> src/main.rs:4:1
|
3 | #[derive(Clone, Copy)]
| ----- derived `Clone` adds implicit bounds on type parameters

最简单的修复是给标记类型也加上 Clone, Copy(#[derive(Clone, Copy)] struct User;)。或者手写这两个实现,这样就不需要对 T 加约束:

impl<T> Clone for Id<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Id<T> {}

编写 fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N],把分数复制到一个正好有 N 个元素的数组里。不够的位置填 0,多出来的分数丢掉。分别用 pad_scores::<5>(&[90, 85]) 和 let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]); 调用它。提示:从 [0; N] 开始,然后遍历 0..N,用 scores.get(i) 取值。

查看答案
fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] {
let mut out = [0; N];
for i in 0..N {
if let Some(&score) = scores.get(i) {
out[i] = score;
}
}
out
}
fn main() {
let five = pad_scores::<5>(&[90, 85]);
let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);
println!("{:?}", five);
println!("{:?}", top3);
}

输出:

[90, 85, 0, 0, 0]
[70, 95, 88]
▶ 动手试一试

这里的 N 没法从参数里推断出来(slice 的类型里不含长度),所以它来自 turbofish ::<5>,或者来自你要求的类型 [u32; 3]。

编写两个 const fn:minutes(n: u64) -> u64 和 hours(n: u64) -> u64,它们返回对应的秒数。用它们定义 const SESSION_TIMEOUT: u64 = hours(1) + minutes(30);,以及一个包含 1、5、15 分钟的数组 const RETRY_DELAYS: [u64; 3]。把两者都打印出来。提示:hours 可以调用 minutes。

查看答案
const fn minutes(n: u64) -> u64 {
n * 60
}
const fn hours(n: u64) -> u64 {
minutes(n * 60)
}
const SESSION_TIMEOUT: u64 = hours(1) + minutes(30);
const RETRY_DELAYS: [u64; 3] = [minutes(1), minutes(5), minutes(15)];
fn main() {
println!("会话超时:{SESSION_TIMEOUT} 秒");
println!("重试间隔:{:?}", RETRY_DELAYS);
}

输出:

会话超时:5400 秒
重试间隔:[60, 300, 900]
▶ 动手试一试

5400 和重试间隔表都由编译器在构建时算好,而且 hours(1) + minutes(30) 比光秃秃的 5400 好读多了。如果去掉 minutes 前面的 const,这些 const 项就会因为 E0015 编译失败。

定义 struct Euro; 和 struct Dollar;,再定义一个 Money<C>,它有一个 cents: i64 字段和一个 PhantomData<C> 字段。给它加上 new(cents) 和 plus(self, other: Money<C>) -> Money<C>。把一顿午饭和一杯咖啡的欧元价格加起来,打印总额。提示:因为 other 的 C 和自己相同,所以 Money<Euro> 和 Money<Dollar> 不能相加。

查看答案
use std::marker::PhantomData;
struct Euro;
struct Dollar;
struct Money<C> {
cents: i64,
_currency: PhantomData<C>,
}
impl<C> Money<C> {
fn new(cents: i64) -> Self {
Money { cents, _currency: PhantomData }
}
fn plus(self, other: Money<C>) -> Money<C> {
Money::new(self.cents + other.cents)
}
}
fn main() {
let lunch: Money<Euro> = Money::new(1250);
let coffee: Money<Euro> = Money::new(320);
let ticket: Money<Dollar> = Money::new(4999);
let total = lunch.plus(coffee);
println!("合计:{}.{:02} 欧元", total.cents / 100, total.cents % 100);
println!("门票:{}.{:02} 美元", ticket.cents / 100, ticket.cents % 100);
}

输出:

合计:15.70 欧元
门票:49.99 美元
▶ 动手试一试

试试 lunch.plus(ticket):它会报 expected Money<Euro>, found Money<Dollar>。另外,用整数而不是 f64 存储“分”,也能避免金额计算中的舍入误差。

  • const 泛型(const N: usize)让数字成为类型的一部分。长度不同的数组是不同的类型,impl<T, const N: usize> 可以为所有尺寸编写方法。
  • 在稳定版 Rust 中,const 参数可以是整数、bool 或 char。用表达式作实参时需要加花括号({ PAGE * 2 }),而在类型里对泛型 N 做运算目前还不允许。
  • const fn 可以在常量上下文(const、static、数组长度)中于编译期运行。用 while 代替 for,并且只调用其他 const fn。
  • PhantomData<T> 是一个零大小的字段,把结构体和一个它并不存储的类型关联起来。可以用它实现类型化 ID、计量单位和 typestate。
  • impl Matrix<2, 2> 和 impl<const N: usize> Matrix<N, N> 只为部分尺寸提供方法。关联常量(如 U::IN_METERS)让每个类型拥有自己的编译期取值。
  • 这些工具会增加编译时间、降低可读性。等它们能防止真实的 bug 时再用。

小测验

1. 为什么不能把 [u32; 4] 传给一个接收 [u32; 3] 的函数?

2. PhantomData<T> 在运行时给结构体增加了什么?

3. 在稳定版 Rust 中,下面哪一项不能在 const fn 里使用?

4. impl<const N: usize> Matrix<N, N> 是什么意思?

得分: 0 / 4