自 const generics 的原始 RFC 被接受以来已过去超过 3 年,首个版本的 const generics 现已在 Rust beta channel 中可用!它将在 1.51 版本中提供,预计将于 2021 年 3 月 25 日发布。Const generics 是 Rust 中 最受期待 的特性之一,我们很高兴人们能开始利用这项新增特性带来的语言能力提升。
即使你不知道 const generics 是什么(如果是这样,请继续阅读!),你很可能已经从中受益了:const generics 已被 Rust 标准库使用,以改进数组的人机工程学和诊断能力;更多内容请见下文。
随着 const generics 进入 beta 阶段,让我们快速回顾一下实际稳定了哪些内容、这意味着什么以及接下来的计划。
什么是 const generics?
Const generics 是作用于常量值而不是类型或生命周期的泛型参数。这允许类型例如通过整数进行参数化。事实上,自 Rust 开发早期以来就有一个 const generic 类型的例子:数组类型 [T; N]
,其中 T
为某种类型,N: usize
。然而,以前无法对任意大小的数组进行抽象:如果你想为任意大小的数组实现 trait,你必须手动为每个可能的值进行实现。长期以来,甚至标准库中针对数组的方法也因此问题而限制为长度不超过 32 的数组。此限制在 Rust 1.47 中最终被解除 —— 这项改变正是 const generics 使之成为可能。
这是一个使用 const generics 的类型和实现示例:一个包裹两个相同大小数组的类型。
当前限制
Const generics 的第一个迭代版本被有意地进行了限制:换句话说,此版本是 const generics 的 MVP(最小可用产品)。此决定是出于对通用 const generics 额外复杂性的考虑(通用 const generics 的实现尚未完成,但我们觉得 1.51 中的 const generics 已经非常有用了),以及希望循序渐进地引入一项大型特性,以便积累处理任何潜在缺点和困难的经验。我们打算在未来的 Rust 版本中解除这些限制:请参见 接下来 的内容。
Const generics 仅允许整数类型
目前,唯一可作为 const generic 参数类型使用的类型是整数类型(即有符号和无符号整数,包括 isize
和 usize
)以及 char
和 bool
。这涵盖了 const 的一个主要用例,即对数组进行抽象。未来,此限制将解除,以允许更复杂的类型,例如 &str
和用户自定义类型。
const 参数中不允许使用复杂的泛型表达式
目前,const 参数只能通过以下形式的 const 参数实例化
- 独立的 const 参数。
- 字面量(即整数、布尔值或字符)。
- 具体的常量表达式(用
{}
括起来),不涉及泛型参数。
例如
>; // ok: const expression contains no generic parameters
+ 1 }>; // error: const expression contains the generic parameter `M`
M >; // error: const expression contains the generic parameter `T`
}
let _: ; // ok: `M` is a const parameter
let _: ; // error: const expression contains the generic parameter `T`
}
按值数组迭代器(By-value array iterator)
除了上述语言更改之外,我们还开始向标准库添加利用 const generics 的方法。虽然大多数方法在此版本中尚未准备好进行稳定,但有一个方法已经稳定了。array::IntoIter
允许对数组进行按值迭代而不是按引用迭代,弥补了一个显著的缺点。目前正在讨论是否可能直接为数组实现 IntoIterator
,尽管仍存在 向后兼容性问题 需要解决。IntoIter::new
作为临时解决方案,使处理数组变得显著简单。
use array;
let arr = ;
for elem in new
接下来是什么?
Const generics 与默认参数
泛型参数目前必须按照特定顺序排列:生命周期、类型、consts。然而,当试图在 const 参数旁边使用默认参数时,这会带来困难。为了让编译器知道哪个泛型参数是什么,任何默认参数都需要放在最后。这两个约束——“类型在 consts 之前”,以及“默认参数放在最后”——对于同时具有默认类型参数和 const 参数的定义会产生冲突。
解决此问题的办法是放宽排序约束,以便 const 参数可以排在类型参数之前。然而,在实现这一改变时会涉及一些微妙之处,因为 Rust 编译器目前对参数排序做出了假设,而移除这些假设需要一些技巧。
鉴于围绕 const 参数默认值的类似设计问题,在 1.51 版本中也暂不支持这些。然而,解决上述参数排序问题也将解除对 const 默认值的限制。
自定义类型的 Const generics
理论上,要使某个类型作为 const 参数的类型有效,我们必须能够在编译时比较该类型的值。此外,值的相等性应该行为良好(即应是确定的、自反的、对称的和传递的)。为了保证这些属性,const generics RFC 中引入了结构相等性(structural equality)的概念:本质上,这包括任何带有 #[derive(PartialEq, Eq)]
且其成员也满足结构相等性的类型。
关于结构相等性应如何精确地表现,仍有一些问题,以及 实现的先决条件。原始类型显著简单,这使得我们能够先稳定针对这些类型的 const generics,然后再稳定更通用的类型。
带有复杂表达式的 Const generics
支持复杂表达式涉及几个复杂性。一个 feature flag,feature(const_evaluatable_checked)
,在 Nightly channel 中可用,它启用了一种针对 const generics 的复杂表达式支持版本。
一个困难在于需要有办法比较未求值的常量,因为编译器无法自动知道两个语法上相同的表达式实际上是相等的。这涉及一种对表达式的符号推理,这通常是一个复杂的问题。
// 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.
我们还需要一种处理在求值泛型操作时可能出现的错误的方法。
若无办法限制这里的 M
的可能值,调用 generic_function::<0>()
在求值 0 - 1
时会导致错误,该错误在声明时未被捕获,因此可能对下游用户造成意外失败。
关于如何精确表达此类边界,存在设计问题,这些问题需要在稳定复杂的 const 参数之前解决。
总结
对于如此重要的新特性,可能会有一些不完善之处。如果你遇到任何问题,即使是像令人困惑的错误消息这样的小问题,请提交 issue!我们希望用户体验尽可能做到最好——而现在发现的任何问题对于 const generics 的后续迭代版本都可能更加重要。