当前位置 : 主页 > 编程语言 > java >

[原创] 针对常量泛型参数的分类实现

来源:互联网 收集:自由互联 发布时间:2022-06-23
背景与问题 ​​const​​在 Rust 中是一个关键字,而且总是围绕着常量表达式 (constant expressions) 和编译期求值等话题。 而论及泛型参数 (generic parameters),我们总是想到 trait bounds 和生命

背景与问题

​​const​​ 在 Rust 中是一个关键字,而且总是围绕着常量表达式 (constant expressions) 和编译期求值等话题。

而论及泛型参数 (generic parameters),我们总是想到 trait bounds 和生命周期。或者有时候,我们完全没注意到“泛型参数”这个描述。

我们知道,函数参数是列在函数名之后的 ​​(...)​​​ 内的部分,而泛型参数是列在 ​​<...>​​ 内的部分。

泛型参数分为三类:

  • 生命周期参数
  • 类型参数
  • 常量参数
  • 而且它们的顺序被规定为:生命周期必须放置于后两类之前,后两类可以交叉摆放。

    对于用途最广泛的类型参数,常常利用 trait bounds 来限制实现,比如以下代码虽然声明一个泛型 ​​T​​​, 但只对 ​​T: Clone​​ 的情况实现功能。

    struct Item<T>(T);

    impl<T: Clone> Item<T> {
    fn clone_myself(&self) -> Self {
    Item(self.0.clone())
    }
    }

    而常量参数通常是具体类型,目前仅允许一些基本类型 ​​u8​​​、​​u16​​​、​​u32​​​、​​u64​​​、​​u128​​​、​​usize​​​、​​i8​​​、​​i16​​​、​​i32​​​、​​i64​​​、​​i128​​​、​​isize​​​、​​char​​​、​​bool​​ 作为常量参数。

    比如对于 ​​struct Item<const I: i32>​​​,如果我们需要对 ​​I == 0​​​ 和 ​​I != 0​​ 两种情况做不同的实现,该怎么做呢?

    struct Item<const I: i32>;

    // 当然不是以下做法,因为 Rust 不支持
    impl<const I: i32> Item<I> where I == 0 {}
    impl<const I: i32> Item<I> where I != 0 {}

    常量泛型参数

    常量泛型参数 (const generics parameters):

  • 可以在任何 常量条目 中使用,而且只能独立使用,通常作为某类型的参数出现。
  • 作为一种常量上下文 (const context),只与常量表达式和常量函数共存,无法与普通表达式一起使用。
  • 除非是单路径(单个标识符)或 literal,它必须使用​​{ ... }​​ 块表达式的形式。
  • 在单态化之后计算值,这与关联常量 (associated constants) 类似。
  • “单态化”在常量泛型参数中是一个基本视角,这意味着对于 ​​Item<const I: i32>​​​,单态化之后的 ​​Item<const I = 0>​​​ 和 ​​Item<const I = 1>​​ 被认为是两个完全不同的类型。

    而且 trait bounds 并不会考虑常量泛型参数的穷尽,Reference 给了以下一个例子:

    struct Foo<const B: bool>;
    trait Bar {}
    impl Bar for Foo<true> {}
    impl Bar for Foo<false> {}

    fn needs_bar(_: impl Bar) {}
    fn generic<const B: bool>() {
    let v = Foo::<B>;
    needs_bar(v); // ERROR: trait bound `Foo<B>: Bar` is not satisfied
    }

    所以,直接应用 trait bounds 似乎是一个不好的主意。

    ​​I​​​ 和 ​​I == 0​​

    从泛型角度看, ​​struct Item<const I: i32>;​​ 定义了一个具体类型的泛型参数,但并不限定这个值。

    所以,如果希望对所有值实现相同的功能,直接写下面的代码就行:

    struct Item<const I: i32>;
    impl<const I: i32> Item<I> {
    fn fun_for_all_i32() {}
    fn for_all_i32(self) {}
    }

    Item::<0>::fun_for_all_i32();
    Item::<1>::fun_for_all_i32();

    此外,单态化意味着我们可以对具体一种值实现单独的功能,所以 ​​I == 0​​ 的情况迎刃而解了:

    #struct Item<const I: i32>;
    impl Item<0> {
    fn fun_for_0() {}
    fn for_0(self) {}
    }

    Item::<0>::fun_for_0();
    Item::<1>::fun_for_0(); // Error

    Rust 为不存在的实现提供了良好的错误报告:

    error[E0599]: no function or associated item named `fun_for_0` found for struct `Item<1_i32>` in the current scope
    --> src/main.rs:11:12
    |
    4 |
    struct Item<const I: i32>;
    | -------------------------- function or associated item `fun_for_0` not found for this
    ...
    11 |
    Item::<1>::fun_for_0(); // Error
    | ^^^^^^^^^ function or associated item not found in `Item<1_i32>`
    |

    = note: the function or associated item was found for
    - `Item<0_i32>`

    ​​I != 0​​

    @Michael Bryan 提供了一种思路:泛型常量表达式 + trait bounds。

    ​​#![feature(generic_const_exprs)]​​​ 允许你写出良好形式 (well-formedness) 的常量泛型表达式,并且进行常量求值,没有这个功能, Rust 只允许 ​​I​​​ 或者 ​​{ I }​​ 这种“简单形式”的表达式。

    ​​I != 0​​ 是一种良好的形式(当然,常量函数调用也是一种良好的形式),所以我们可以这样写:

    // 点击右上角就可以运行代码;你可以直接在网页中编辑这段代码
    #![feature(generic_const_exprs)]
    #![allow(incomplete_features)]

    struct Item<const I: i32> {}

    impl<const I: i32> Item<I>
    where
    [(); (I != 0) as usize - 1]:, // 这里的技巧在常量表达式中非常常见
    {
    fn for_non_zero() {}
    }

    fn main() {
    Item::<1>::for_non_zero();
    // 下面这一行代码导致编译错误
    Item::<0>::for_non_zero();
    }

    从功能上看,它的确解决了问题 —— 即使我们误入不存在的函数/方法,编译器会帮助我们的:

    error[E0284]: type annotations needed: cannot satisfy `the constant `Item::<{_: i32}>::{constant#0}` can be evaluated`
    --> src/main.rs:17:5
    |
    17 | Item::for_non_zero();
    | ^^^^^^^^^^^^^^^^^^ cannot satisfy `the constant `Item::<{_: i32}>::{constant#0}` can be evaluated`
    |
    note: required by a bound in `Item::<I>::for_non_zero`
    --> src/main.rs:9:10
    |
    9 | [(); (I != 0) as usize - 1]:, // 这里的技巧在常量表达式中非常常见
    | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Item::<I>::for_non_zero`
    10 | {
    11 | fn for_non_zero() {}
    | ------------ required by a bound in this

    上面报告的错误显然不直观,我们可以施加技巧,利用类型和 trait 让错误更直观一些(虽然很间接得到):

    #![feature(generic_const_exprs)]
    #![allow(incomplete_features)]
    #![feature(negative_impls)]

    struct Item<const I: i32>;

    impl<const I: i32> Item<I>
    where
    Check<{ I != 0 }>: NonZero,
    {
    fn for_non_zero() {}
    }

    struct Check<const C: bool>;
    trait NonZero {}
    impl NonZero for Check<true> {}
    impl !NonZero for Check<false> {} // 这一步在这里并不是必要的

    fn main() {
    Item::<1>::for_non_zero();
    // Error:
    Item::<0>::for_non_zero();
    }

    error[E0599]: the function or associated item `for_non_zero` exists for struct `Item<0_i32>`, but its trait bounds were not satisfied
    --> src/main.rs:22:16
    |
    5 | struct Item<const I: i32>;
    | -------------------------- function or associated item `for_non_zero` not found for this
    ...
    14 | struct Check<const C: bool>;
    | ---------------------------- doesn't satisfy `Check<{ I != 0 }>: NonZero`
    ...
    22 | Item::<0>::for_non_zero();
    | ^^^^^^^^^^^^ function or associated item cannot be called on `Item<0_i32>` due to unsatisfied trait bounds
    |
    = note: the following trait bounds were not satisfied:
    `Check<{ I != 0 }>: NonZero`
    note: the following trait must be implemented
    --> src/main.rs:15:1
    |
    15 | trait NonZero {}
    | ^^^^^^^^^^^^^^^^

    ​​I == 0​​​ | ​​I > 0​​​ | ​​I < 0​​

    如果我们想对上面的 ​​const I: i32​​ 做更多分类呢?或者在这些分类中,我们想要同样的函数名返回不同的类型呢?

    我没有完美的答案,因为具体的需求会导致不同的代码设计。

    我给出自己的思考结果:

  • 常量泛型参数无法拓展到自定义类型,所以需要围绕基本类型来实现;
  • 常量表达式总是意味着它的值必须在编译时知晓,所以它的来源很狭窄,唯有泛型函数帮助我们做更多事情。
  • struct Item<const U: u8>;
    struct A; struct B; struct C;

    impl Item<0> { fn foo() -> A { A } }
    impl Item<1> { fn foo() -> B { B } }
    impl Item<2> { fn foo() -> C { C } }

    const fn check(i: i32) -> u8 {
    match i {
    0 => 0,
    1.. => 1,
    _ => 2,
    }
    }

    Item::<{ check(0) }>::foo(); // A
    Item::<{ check(1) }>::foo(); // B
    Item::<{ check(-1) }>::foo(); // C

    如果你使用上一小节提到的技巧,目前是无法实现同名函数的:

    // error[E0592]: duplicate definitions with name `f`
    // error[E0080]: evaluation of `main::Foo::{constant#0}` failed
    #![feature(generic_const_exprs)]
    #![allow(incomplete_features)]

    struct Foo<const I: i32 = 0> {}

    impl<const I: i32> Foo<I> where [(); (I < 0) as usize - 1]:,
    { fn f() {} }

    impl<const I: i32> Foo<I> where [(); (I > 0) as usize - 1]:,
    { fn f() {} }

    impl Foo<0>
    { fn f() {} }

    // error[E0592]: duplicate definitions with name `f`
    #![feature(generic_const_exprs)]
    #![allow(incomplete_features)]

    struct Foo<const I: i32> {}

    impl<const I: i32> Foo<I> where Check<{ check(I) }>: Greter,
    { fn f() {} }

    impl<const I: i32> Foo<I> where Check<{ check(I) }>: Less,
    { fn f() {} }

    impl<const I: i32> Foo<I> where Check<{ check(I) }>: Equal,
    { fn f() {} }
    // 后半部分代码已隐藏,见 mdbook 版

    参考资料

  • Rust User Forum: Const generics: how to impl “not equal”

  • 网友评论