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 incorrectconst 泛型可以解决这个问题。在尖括号里写上 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 泛型真正派上用场,是在长度必须成为类型的一部分的时候:不用堆分配就能存进结构体,或者出现在返回类型里。下面就来看。
带 const 泛型的结构体:座位表
Section titled “带 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。
默认值、花括号和目前的限制
Section titled “默认值、花括号和目前的限制”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 stris 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:让编译器替你算
Section titled “const fn:让编译器替你算”有些商店会把价目表提前印好,而不是在收银台一个个现算价格。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) 得 144const 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。详见共享状态。
PhantomData:不会搞混的标签
Section titled “PhantomData:不会搞混的标签”下面这个 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。开头的 _ 只是一个常见的命名习惯,表示“这个字段永远不会被读取”。
计量单位:米不是英尺
Section titled “计量单位:米不是英尺”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> 就能服务你以后要相加的所有单位。
typestate:打开时不能上锁的门
Section titled “typestate:打开时不能上锁的门”同样的技巧还可以用来跟踪某个东西的状态。一扇门可以是开着、关着或者锁着的。关着的门可以上锁,但给开着的门上锁就说不通了。把状态标签放进类型里,非法的操作干脆就不存在了:
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],}
// 只适用于方阵:行数和列数都是同一个 Nimpl<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大矩阵:迹为 15big.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 m29032.00 ft = 8848.95 m29032.00 ft = 5.50 miU::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 那一页有讲解。
什么时候不该用这些工具
Section titled “什么时候不该用这些工具”这些特性很强大,也正因为如此很容易让人忍不住想用。说几句实在话:
- 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` trait15 | show(id); | -- value moved here16 | 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> {}1. 补齐分数
Section titled “1. 补齐分数”编写 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]。
2. 编译期计算超时时间
Section titled “2. 编译期计算超时时间”编写两个 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 编译失败。
3. 不能混用的货币
Section titled “3. 不能混用的货币”定义 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
- Const generics — The Rust Reference(英文):const 参数的确切规则、允许哪些类型,以及什么时候需要花括号。
- Const functions — The Rust Reference(英文):什么是常量上下文,以及
const fn里可以做什么。 PhantomData— 标准库文档(英文):官方说明,附有未使用类型参数的示例。- 虚类型参数 — 通过例子学 Rust:简短的可运行示例,包括一个计量单位的案例。
- Const generics MVP hits beta — Rust Blog(英文):这篇公告解释了为什么要加入 const 泛型,以及第一个版本能做什么。