【问题标题】:How to create a typesafe range-limited numeric type?如何创建类型安全的范围限制数字类型?
【发布时间】:2017-01-08 06:32:34
【问题描述】:

在 Rust 中,我需要一个数字类型,它的属性是域围绕 0 对称。如果数字 n 是有效值,那么数字 -n em> 也必须有效。在初始化和算术期间如何确保类型安全?在类型上实现模数和饱和算法的最佳方式是什么?


问题最简单的例子是:

type MyNumber = i8; // Bound to domain (-100, 100)

fn main() {
    let a = MyNumber(128); // Doesn't panic when 128 > 100
}

需要考虑一些因素,我尝试了不同的解决方案。对于下面的示例,我将避免使用泛型编程:

  • 基于枚举类型确保只有有效值是可能的值。这很快就会变得一团糟:

    enum MyNumber {
        One,
        Two,
        ...
    }
    impl MyNumber {
        fn convert(i8) -> MyNumber {
            match {
                1 => MyNumber::One,
                2 => MyNumber::Two,
                ...
            }
        }
    }
    
  • 公开一个在设置字段之前检查参数的方法,教科书associated function。这并不妨碍使用 struct 构造函数进行赋值。

  • 每当操作发生时验证操作数(并强制纠正它们)。这看起来很合理,但需要每个方法重复验证代码。

    extern crate num;
    
    use num::Bounded;
    use std::cmp;
    struct MyNumber {
        val: i8,
    }
    
    impl Bounded for MyNumber {
        fn max_value() -> Self {
            MyNumber { val: 65 }
        }
        fn min_value() -> Self {
            MyNumber { val: -50 }
        }
    }
    impl MyNumber {
        fn clamp(&mut self) {
            self.val = cmp::min(MyNumber::max_value().val, 
                                cmp::max(MyNumber::min_value().val, self.val))
        }
        fn add(&mut self, mut addend: Self) {
            self.clamp();
            addend.clamp(); 
            //TODO: wrap or saturate result
            self.val = self.val + addend.val
        }
    }
    
    fn main() {
        let mut a = MyNumber { val: i8::max_value() };
        let b = MyNumber { val: i8::min_value() };
        a.add(b);
        println!("{} + {} = {}",
                 MyNumber::max_value().val,
                 MyNumber::min_value().val, 
                 a.val);
    }
    

上述解决方案都不是非常优雅的 - 在某种程度上这是因为它们是原型实现。必须有一种更简洁的方法来限制数字类型的域!

类型和特征的哪种组合可以检查边界,将它们用于模/饱和算术,并轻松转换为数字基元?

编辑:这个问题已被标记为来自2014 的一个更老的问题的重复。我不相信这些问题是相同的,因为 Rust 是 pre alpha,并且版本 1.0 带来了对该语言的重大改进。与 Python 2 和 3 相比,差距更大。

【问题讨论】:

  • 即使 如果 更改或添加了足够大的内容到 Rust,将这些答案添加到现有问题仍然是合适的。否则,Stack Overflow 上的每一个问题都需要每隔几个月重新提出一次,以防周围环境发生了变化。您在此处收到的答案与先前存在的答案相呼应(直至构造函数和特征实现),因此我没有看到它们不重复的原因。如果您认为一个较老的问题需要更多关注,还有赏金途径。
  • @Shepmaster 感谢您的澄清。这是对此事的公平看法。
  • 别担心!更广泛的社区可能仍然不同意我并投票重新开放;我只是在努力保持整洁。 ^_^
  • @Aaron3468 没有版本相关的 rust 标记?
  • @MYGz 虽然它们存在并且我认为还有其他因素会减少版本标签的数量(rust-0.8、rust-0.9、rust-0.11),但您的暗示是公平的。目前看来“生锈”是 1.0+,之前的一切都不是很生锈。但这是我第一次观察到一种年轻的语言成为事实上的语言,所以我不知道术语——或者需要区分——已经固化到什么程度。

标签: validation rust modular-arithmetic


【解决方案1】:

公开一个在设置字段之前检查参数的方法, 教科书相关功能。这并不妨碍使用 结构构造函数。

如果字段是私有的,它会这样做。

在 Rust 中,同一模块或子模块中的函数可以看到私有项...但是如果将类型放入其自己的模块中,则私有字段无法从外部获得:

mod mynumber {
    // The struct is public, but the fields are not.
    // Note I've used a tuple struct, since this is a shallow
    // wrapper around the underlying type.
    // Implementing Copy since it should be freely copied,
    // Clone as required by Copy, and Debug for convenience.
    #[derive(Clone,Copy,Debug)]
    pub struct MyNumber(i8);

这是一个简单的impl,带有一个饱和添加,它利用i8 内置的saturating_add 来避免包裹,以便简单的夹紧工作。可以使用pub fn new 函数构造该类型,该函数现在返回Option<MyNumber>,因为它可能会失败。

    impl MyNumber {
        fn is_in_range(val: i8) -> bool {
            val >= -100 && val <= 100
        }
        fn clamp(val: i8) -> i8 {
            if val < -100 {
                return -100;
            }
            if val > 100 {
                return 100;
            }
            // Otherwise return val itself
            val
        }
        pub fn new(val: i8) -> Option<MyNumber> {
            if MyNumber::is_in_range(val) {
                Some(MyNumber(val))
            } else {
                None
            }
        }

        pub fn add(&self, other: MyNumber) -> MyNumber {
            MyNumber(MyNumber::clamp(self.0.saturating_add(other.0)))
        }
    }
}

其他模块可以use类型:

use mynumber::MyNumber;

还有一些例子使用:

fn main() {
    let a1 = MyNumber::new(80).unwrap();
    let a2 = MyNumber::new(70).unwrap();
    println!("Sum: {:?}", a1.add(a2));
    // let bad = MyNumber(123); // won't compile; accessing private field
    let bad_runtime = MyNumber::new(123).unwrap();  // panics
}

Playground

在更完整的实现中,我可能会实现std::ops::Add 等,这样我就可以使用a1 + a2 而不是调用命名方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-19
    • 1970-01-01
    • 2020-07-10
    • 2014-02-23
    • 2020-02-27
    相关资源
    最近更新 更多