自从最初的常量泛型 RFC 被接受以来,经过三年多的时间,常量泛型的第一个版本现在已在 Rust beta 通道中可用! 它将在 1.51 版本中发布,预计发布日期为 2021 年 3 月 25 日。常量泛型是 Rust 中最受期待的功能之一,我们很高兴人们开始利用此新增功能带来的语言强大功能。
即使您不知道什么是常量泛型(如果是这种情况,请继续阅读!),您可能已经从中受益:常量泛型已在 Rust 标准库中使用,以提高数组和诊断的人机工程学;下面会详细介绍。
随着常量泛型进入 beta 测试阶段,让我们快速浏览一下实际稳定化的内容、这在实践中的意义以及下一步是什么。
什么是常量泛型?
常量泛型是泛型参数,它涵盖常量值,而不是类型或生命周期。 例如,这允许类型由整数参数化。 实际上,自 Rust 开发早期以来,就有一个常量泛型类型的示例:数组类型 [T; N]
,对于某些类型 T
和 N: usize
。但是,以前没有办法抽象任意大小的数组:如果您想为任何大小的数组实现特征,则必须手动为每个可能的值执行此操作。长期以来,由于这个问题,即使是数组的标准库方法也仅限于长度最大为 32 的数组。此限制最终在 Rust 1.47 中取消- 此更改是由于常量泛型才得以实现的。
这是一个使用常量泛型的类型和实现的示例:一个包装了相同大小的数组对的类型。
struct ArrayPair<T, const N: usize> {
left: [T; N],
right: [T; N],
}
impl<T: Debug, const N: usize> Debug for ArrayPair<T, N> {
// ...
}
当前限制
常量泛型的第一个迭代版是经过刻意约束的:换句话说,此版本是常量泛型的 MVP(最小可行产品)。此决定的动机是通用常量泛型的额外复杂性(通用常量泛型的实现尚未完成,但我们认为 1.51 中的常量泛型已经非常有用),以及逐步引入大型功能,以获得任何潜在缺陷和困难的经验的愿望。我们打算在 Rust 的未来版本中取消这些限制:请参阅下一步是什么。
只允许使用整型作为常量泛型
目前,唯一可以用作常量泛型参数类型的类型是整数类型(即有符号和无符号整数,包括 isize
和 usize
)以及 char
和 bool
。这涵盖了常量的主要用例,即抽象数组。将来,此限制将被解除,以允许更复杂的类型,例如 &str
和用户定义的类型。
常量参数中没有复杂的泛型表达式
目前,常量参数只能由以下形式的常量参数实例化
- 一个独立的常量参数。
- 一个字面量(即整数、布尔值或字符)。
- 一个具体的常量表达式(用
{}
括起来),不涉及泛型参数。
例如
fn foo<const N: usize>() {}
fn bar<T, const M: usize>() {
foo::<M>(); // ok: `M` is a const parameter
foo::<2021>(); // ok: `2021` is a literal
foo::<{20 * 100 + 20 * 10 + 1}>(); // ok: const expression contains no generic parameters
foo::<{ M + 1 }>(); // error: const expression contains the generic parameter `M`
foo::<{ std::mem::size_of::<T>() }>(); // error: const expression contains the generic parameter `T`
let _: [u8; M]; // ok: `M` is a const parameter
let _: [u8; std::mem::size_of::<T>()]; // error: const expression contains the generic parameter `T`
}
按值数组迭代器
除了上述的语言更改外,我们还开始向标准库添加利用常量泛型的方法。 虽然大多数方法尚未准备好在此版本中稳定化,但有一个方法已经稳定化。array::IntoIter
允许按值而不是按引用来迭代数组,这解决了重大的缺陷。关于直接为数组实现 IntoIterator
的可能性仍在讨论中,但仍有 向后兼容性问题 需要解决。IntoIter::new
充当临时解决方案,使使用数组变得更加简单。
use std::array;
fn needs_vec(v: Vec<i32>) {
// ...
}
let arr = [vec![0, 1], vec![1, 2, 3], vec![3]];
for elem in array::IntoIter::new(arr) {
needs_vec(elem);
}
下一步是什么?
常量泛型和默认参数
泛型参数目前必须按照特定的顺序出现:生命周期、类型、常量。但是,当尝试将默认参数与常量参数一起使用时,这会造成困难。为了使编译器知道哪个泛型参数是什么,任何默认参数都需要放在最后。这两个约束 - “类型在常量之前”和“默认值放在最后” - 对于具有默认类型参数和常量参数的定义相互冲突。
解决此问题的方法是放宽排序约束,以便常量参数可以出现在类型参数之前。但是,事实证明,在实现此更改时存在一些微妙之处,因为 Rust 编译器目前对参数排序做出了假设,需要一些技巧才能删除这些假设。
鉴于有关常量参数默认值的类似设计问题,这些参数目前在 1.51 版本中也不受支持。但是,解决上述参数排序问题也将解除常量默认值的限制。
自定义类型的常量泛型
理论上,为了使类型作为常量参数的类型有效,我们必须能够在编译时比较该类型的值。此外,值的相等性应该表现良好(即,它应该是确定性的、自反的、对称的和传递的)。为了保证这些属性,在常量泛型 RFC 中引入了结构相等的概念:本质上,这包括具有 #[derive(PartialEq, Eq)]
且其成员也满足结构相等性的任何类型。
关于结构相等性的行为方式以及仍然存在一些问题,以及实现的先决条件。基本类型要简单得多,这使我们能够在更通用的类型之前稳定这些类型的常量泛型。
具有复杂表达式的常量泛型
支持复杂表达式涉及多个复杂性。在 Nightly 通道中有一个功能标志 feature(const_evaluatable_checked)
,它为常量泛型启用复杂表达式支持的一个版本。
一个困难在于必须有一种比较未评估常量的方法,因为编译器不会自动知道两个语法上相同的表达式实际上是相等的。这涉及一种关于表达式的符号推理,这通常是一个复杂的问题。
// The two expressions `N + 1` and `N + 1` are distinct
// entities in the compiler, so we need a way to check
// if they should be considered equal.
fn foo<const N: usize>() -> [u8; N + 1] {
[0; N + 1]
}
我们还需要一种处理评估泛型操作时潜在错误的方法。
fn split_first<T, const N: usize>(arr: [T; N]) -> (T, [T; N - 1]) {
// ...
}
fn generic_function<const M: usize>(arr: [i32; M]) {
// ...
let (head, tail) = split_first(arr);
// ...
}
如果没有办法限制此处 M
的可能值,则调用 generic_function::<0>()
将在评估 0 - 1
时导致错误,该错误不会在声明时捕获,因此可能会意外地导致下游用户失败。
关于如何准确表达这些边界,存在设计问题,需要在稳定化复杂的常量参数之前解决这些问题。
总结
对于如此重要的新功能,可能会有一些不完善之处。如果您遇到任何问题,即使是像令人困惑的错误消息这样的小问题,请提出一个问题!我们希望用户体验尽可能好 - 现在存在的任何问题对于常量泛型的下一次迭代可能更加重要。