历史:常量泛型 在 Rust 1.51 中稳定,允许通过原始整数类型的常量值参数化类型,从而启用类似 [T; N] 的泛型固定大小数组。在设计阶段,语言团队明确限制常量泛型参数只能是具有结构相等性和确定性编译时评估的类型。由于违反总排序或依赖于运行时内存地址,这一限制排除了 f32、f64 和 &str 字面量。
问题:浮点类型的核心问题是存在 NaN(非数字),它违反了自反相等性(NaN != NaN),阻止编译器在单态化期间可靠地确定类型身份。对于字符串字面量(&str),问题在于它们的胖指针表示(地址 + 长度),以及它们对数据段中特定内存地址的依赖,这在不同的编译单元或 crate 中是不确定的。类型系统要求 MyStruct<1> 和 MyStruct<1> 始终引用相同的类型,因此要求常量参数的相等性能够通过位比较或结构比较在编译时确定。
解决方案:Rust 编译器通过在 HIR(高阶中间表示)降低和类型检查期间使用内部特征如 StructuralPartialEq(不稳定)来强制执行这些约束。当遇到常量泛型参数时,编译器验证该类型是否为整数、bool 或 char,或是明确标记为支持结构相等的用户自定义类型。它拒绝浮点类型,因为它们的相等性不是自反的,并拒绝像 &str 的引用,因为它们引入了生命周期和间接性,这在常量泛型所需的 'static 上下文中无法和解。在单态化期间,编译器评估常量表达式并使用结构相等性合并相同的实例,确保类型安全。
// 有效:usize 具有结构相等性 struct Matrix<const N: usize> { data: [[f64; N]; N], } // 无效:f64 缺乏总排序(NaN 问题) // struct Physics<const G: f64>; // 错误:浮点类型不能用于常量泛型 // 无效:&str 具有间接性和生命周期复杂性 // struct Label<const S: &str>; // 错误:`&str` 被禁止作为常量泛型参数的类型
你正在设计一个高频交易引擎,其中金融工具必须携带编译时常量参数用于合约规范,例如滴价(例如 0.25 美元)或乘数系数。最初的设计尝试使用 f64 常量泛型将这些精确的十进制值直接编码到类型系统中,希望消除这些常量的运行时存储并实现定价计算的编译时优化。
一种考虑的方法是通过将 f64 位转换为 u64 并将其用作常量参数,然后在实现中再转换回来,来绕过这一限制。然而,这被证明是危险的,因为位上相同的浮点数可能由于符号零(+0.0 与 -0.0)和 NaN 有效负载而代表不同的语义值,可能导致编译器将不同的金融工具视为相同类型或合并应该保持分开的计算,从而导致不正确的定价逻辑。
另一个解决方案涉及在特性中使用关联常量(trait Instrument { const TICK_SIZE: f64; })。虽然这允许使用浮点值,但牺牲了将滴价用作类型级别区分符的能力;你不能有 Vec<Instrument<TICK_SIZE>> 包含不同滴价的不同工具,而不求助于 dyn Trait 对象开销,这在热路径中引入的 vtable 间接性是不可接受的。
选择的解决方案是将浮点值编码为定点整数(例如,将 0.25 美元表示为 usize 25,隐含缩放因子为 100)。这一方法满足常量泛型约束,同时保持零成本抽象和编译时评估。结果是一个类型安全的合约系统,其中 Bond<25> 和 Bond<50> 是不同的类型,没有运行时开销,尽管它需要仔细记录缩放约定,以防止算术错误。
为什么 Rust 允许 char 和 bool 作为常量泛型参数,但排除了 &str,尽管两者从技术上来说都是原始类型?
Char 和 bool 是具有固定大小和简单结构相等的值类型;一个 char 是 32 位 Unicode 标量值,bool 严格为 0 或 1,允许位比较。&str 是一个胖指针(或引用到一个 DST),包含数据指针和长度,引入了间接性和生命周期参数。编译器无法保证两个字符串字面量在不同 crate 中驻留在同一内存地址,也无法保证它们的生命周期以满足 'static 要求的方式允许类型身份检查。因此,&str 缺乏常量泛型参数所需的结构属性,而 char 和 bool 是自包含的值。
允许浮点类型的常量泛型将如何破坏与 NaN(非数字)值的类型安全?
如果 f32 被允许,像 MyStructf32::NAN 和 MyStruct<{ 0.0 / 0.0 }> 这样的表达式都会产生 NaN 值,但编译器无法保证它们代表同一类型,因为 NaN != NaN。这将允许创建应逻辑上相同的两个不同单态化,或者相反,强迫编译器合并包含不同 NaN 有效负载的类型。这种类型身份的违反可能导致不安全性,其中单例模式失败或基于类型的优化生成不正确的代码,因为编译器假设类型参数唯一地标识一种类型。
常量泛型和关联常量之间的根本区别是什么,为什么前者需要结构相等而后者不需要?
常量泛型参数是类型身份的一部分;Container<10> 和 Container<20> 是不同的类型,具有独立的单态化。这要求值在编译时可比较,以确保全局唯一性并合并相同的实例。关联常量是与类型实现相关联的值,但不改变类型本身;TypeA 和 TypeB 仍然是不同的类型,无论它们的关联常量值如何。因此,关联常量可以是浮点或复杂类型,因为它们仅提供实现中值,而不影响类型检查或单态化,绕过了类型系统级别对结构相等的需求。