如何创建类型安全范围有限的数字类型



在Rust中,我需要一个数字类型,其属性是域在0周围对称。如果数字n是有效值,则数字-n也必须有效。如何确保初始化和算术期间的类型安全?如何最好地在类型上实现模块化和饱和运算?


问题最简单的例子是:

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,
    ...
    }
    }
    }
    
  • 公开一种在设置字段之前检查参数的方法,即教科书相关的函数。这并不妨碍使用结构构造函数进行赋值。

  • 每当操作发生时,验证操作数(并强制更正它们)。这似乎是合理的,但需要每个方法重复验证代码。

    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是alpha之前的版本,1.0版本对语言进行了重大改进。这种差异比Python 2和Python 3之间的差异更大。

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

如果字段是私有字段,则执行此操作。

在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
}

游乐场

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

最新更新