在最初的Const泛型RFC被接受三年多后,**Const泛型的第一个版本现在可以在Rust测试通道中使用!** 它将在预计于**2021年3月25日**发布的1.51版本中提供。Const泛型是最受期待的Rust功能之一,我们很高兴人们开始利用此功能带来的语言能力提升。
即使您不知道Const泛型是什么(在这种情况下,请继续阅读!),您可能已经从中受益:Const泛型已经应用于Rust标准库中,以提高数组和诊断的可用性; 更多内容请见下文。
随着Const泛型进入测试阶段,让我们快速了解一下实际稳定了什么,这在实践中意味着什么,以及下一步是什么。
什么是Const泛型?
Const泛型是泛型参数,它们在常量值上进行范围限定,而不是类型或生命周期。例如,这允许类型通过整数参数化。事实上,从Rust开发初期开始,就有一个Const泛型类型的例子:数组类型[T; N]
,对于某个类型T
和N: usize
。但是,以前没有办法抽象出任意大小的数组:如果您想为任何大小的数组实现一个trait,您必须为每个可能的值手动执行此操作。很长一段时间以来,即使是标准库中针对数组的方法也仅限于长度最多为32的数组,因为存在这个问题。这个限制在Rust 1.47中最终被解除 - 这是Const泛型带来的改变。
以下是一个使用Const泛型的类型和实现示例:一个包装相同大小的两个数组的类型。
struct ArrayPair<T, const N: usize> {
left: [T; N],
right: [T; N],
}
impl<T: Debug, const N: usize> Debug for ArrayPair<T, N> {
// ...
}
当前限制
Const泛型的第一个迭代版本被有意地限制了:换句话说,这个版本是Const泛型的MVP(最小可行产品)。这个决定既是由于通用Const泛型的额外复杂性(通用Const泛型的实现尚未完成,但我们认为1.51中的Const泛型已经非常有用),也是由于希望逐步引入一个大型功能,以积累对任何潜在缺陷和困难的经验。我们打算在未来的Rust版本中解除这些限制:请参阅下一步。
仅允许整数类型用于Const泛型
目前,唯一可以作为Const泛型参数类型的类型是整数类型(即有符号和无符号整数,包括isize
和usize
)以及char
和bool
。这涵盖了Const的主要用例,即抽象化数组。将来,这个限制将被解除,以允许更复杂的类型,例如&str
和用户定义的类型。
Const参数中不允许使用复杂的泛型表达式
目前,Const参数只能由以下形式的Const参数实例化
- 一个独立的Const参数。
- 一个字面量(即一个整数、布尔值或字符)。
- 一个具体的常量表达式(用
{}
括起来),不包含泛型参数。
例如
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`
}
按值数组迭代器
除了上面描述的语言更改之外,我们还开始向标准库添加利用Const泛型的方法。虽然大多数方法尚未准备好在这个版本中稳定,但有一个方法已经稳定了。 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);
}
下一步是什么?
Const泛型和默认参数
泛型参数目前必须按照特定的顺序排列:生命周期、类型、常量。但是,当尝试将默认参数与Const参数一起使用时,这会导致困难。为了让编译器知道哪个是哪个泛型参数,任何默认参数都需要放在最后。这两个约束 - “类型在常量之前”,以及“默认值在最后” - 对于具有默认类型参数和Const参数的定义来说是相互冲突的。
解决这个问题的方法是放宽排序约束,以便Const参数可以放在类型参数之前。但是,事实证明,实现这种更改存在一些细微之处,因为Rust编译器目前对参数排序做出了假设,需要一些技巧才能消除这些假设。
鉴于围绕Const参数的默认值的类似设计问题,这些问题目前也不支持在1.51版本中使用。但是,修复上面的参数排序问题也将为Const默认值扫清障碍。
自定义类型的Const泛型
为了使一个类型在理论上有效,作为Const参数的类型,我们必须能够在编译时比较该类型的值。此外,值的相等性应该表现良好(即,它应该是确定性的、自反的、对称的和传递的)。为了保证这些属性,在Const泛型RFC中引入了结构化相等性的概念:本质上,这包括任何具有#[derive(PartialEq, Eq)]
的类型,其成员也满足结构化相等性。
关于结构化相等性应该如何表现,以及仍然存在一些问题,以及实现的先决条件。原始类型要简单得多,这使我们能够在稳定更通用的类型之前,为这些类型稳定Const泛型。
带有复杂表达式的Const泛型
支持复杂表达式涉及几个复杂因素。一个特性标志feature(const_evaluatable_checked)
在Nightly通道中可用,它为Const泛型启用了一种复杂表达式支持版本。
一个困难在于需要有一种方法来比较未评估的常量,因为编译器不会自动知道两个语法上相同的表达式实际上是相等的。这涉及到对表达式进行某种形式的符号推理,这在一般情况下是一个复杂的问题。
// 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
时导致错误,该错误在声明时不会被捕获,因此可能会意外地对下游用户失败。
关于如何精确地表达这些类型的边界,存在设计问题,这些问题需要在稳定复杂Const参数之前得到解决。
总结
对于这样一个重大的新功能,可能会有一些粗糙的边缘。如果您遇到任何问题,即使是像令人困惑的错误消息这样的小问题,也请打开一个问题!我们希望用户体验尽可能好 - 现在出现的任何问题都可能对Const泛型的下一次迭代更加重要。