有没有一种方法可以创建一个只接受一定范围值的数据类型?

28

我有一个函数,它的参数类型是u16。有没有一种优雅的方式来定义一个自定义数据类型,它的行为与u16完全相同,但只有0到100之间的值?

4个回答

19
作为我的理解,这需要依赖类型,而 Rust 并没有。这不需要依赖类型(见注释),但 Rust 仍然缺少所需的支持。
作为一种解决方法,您可以创建一个新类型并自行验证:
#[derive(Debug)]
struct Age(u16);

impl Age {
    fn new(age: u16) -> Option<Age> {
        if age <= 100 {
            Some(Age(age))
        } else {
            None
        }
    }
}

fn main() {
    let age1 = Age::new(30);
    let age2 = Age::new(500);

    println!("{:?}, {:?}", age1, age2);
    assert_eq!(
        std::mem::size_of::<Age>(),
        std::mem::size_of::<u16>()
    );
}

当然,它的行为不会完全像,但你也不希望它这样!例如,u16可以超过100...你必须推理出是否有意义添加/减去/乘以/除以等你的新类型。
为了最大限度地保护,您应将您的类型和任何相关函数移动到一个模块中。这利用了Rust的可见性规则,防止人们意外访问新类型内部的值并使约束无效。
您还可以实现TryFrom(从到您的类型)或From(从您的类型到)以更好地与通用代码集成。
需要注意的重要事情是,这个新类型占用与相同的空间 - 当代码编译时,包装器类型被有效地擦除。类型检查器确保在此之前一切都很好。

这大致是我猜想我必须要做的。我认为对于我的行为,我只需要夹紧。所以如果它们提供其他值也没关系,但是任何超过100的值都会被夹紧到100。 - Kurtis Nusbaum
2
它不需要依赖类型,那些是用于将符号(表示运行时值)链接到类型参数值的,并且有点更加复杂。Ada没有依赖类型,但却让您定义这样的自定义积分(内置于语言中),而C++具有非类型模板参数以实现类似的效果。 - Matthieu M.
1
@MatthieuM,谢谢!这个概念有通用名称吗?之前我遇到过它,被告知它是“依赖类型”。我想知道真正的名称(如果有的话!)。 - Shepmaster
1
@Shepmaster:如果有的话,我不知道。在C++中,它们被称为非类型模板参数,这真的是语言特定的... - Matthieu M.
不幸的是,这并不是100%安全的。您仍然可以执行以下操作,从而绕过构造函数的限制:let age = Age {0: 500};我也对解决此问题的更可靠方案感兴趣。 - Vagelis Prokopiou
3
@VagelisProkopiou 这可以通过使用Rust的模块系统来解决。将Age放在一个模块中,并不要将字段公开。 - Shepmaster

9

很不幸,在 std crate 中没有这样的东西。

但是,你可以使用夜版泛型常量以优化的方式自己实现它,这项功能计划在 Rust 1.51 版本中稳定。例如:

// 1.51.0-nightly (2020-12-30)
pub struct BoundedI32<const LOW: i32, const HIGH: i32>(i32);

impl<const LOW: i32, const HIGH: i32> BoundedI32<{ LOW }, { HIGH }> {
    pub const LOW: i32 = LOW;
    pub const HIGH: i32 = HIGH;

    pub fn new(n: i32) -> Self {
        BoundedI32(n.min(Self::HIGH).max(Self::LOW))
    }

    pub fn fallible_new(n: i32) -> Result<Self, &'static str> {
        match n {
            n if n < Self::LOW => Err("Value too low"),
            n if n > Self::HIGH => Err("Value too high"),
            n => Ok(BoundedI32(n)),
        }
    }

    pub fn set(&mut self, n: i32) {
        *self = BoundedI32(n.min(Self::HIGH).max(Self::LOW))
    }
}

impl<const LOW: i32, const HIGH: i32> std::ops::Deref for BoundedI32<{ LOW }, { HIGH }> {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let dice = BoundedI32::<1, 6>::fallible_new(0);
    assert!(dice.is_err());

    let mut dice = BoundedI32::<1, 6>::new(0);
    assert_eq!(*dice, 1);

    dice.set(123);
    assert_eq!(*dice, 6);
}

然后你可以实现数学等内容。

如果您想在运行时选择边界,则不需要此功能,您只需要执行以下操作:

pub struct BoundedI32 {
    n: i32,
    low: i32,
    high: i32,
}

你还可以使用像bounded-integer这样的箱子,它允许你使用宏在运行时生成一个有界整数。


现在常量泛型已经稳定了,这段代码实际上不再需要使用夜间版就可以运行。 - undefined

8

有了每夜更新功能 generic_const_exprs,就可以在编译时验证:

#![feature(generic_const_exprs)]

struct If<const COND: bool>;

trait True {}
impl True for If<true> {}

const fn in_bounds(n: usize, low: usize, high: usize) -> bool {
    n > low && n < high
}

struct BoundedInteger<const LOW: usize, const HIGH: usize>(usize);

impl<const LOW: usize, const HIGH: usize> BoundedInteger<LOW, HIGH>
where
    If<{ LOW < HIGH }>: True,
{
    fn new<const N: usize>() -> Self
    where
        If<{ in_bounds(N, LOW, HIGH) }>: True,
    {
        Self(N)
    }
}

错误信息不是最好的,但它可以工作!

fn main() {
    let a = BoundedInteger::<1, 10>::new::<5>();
    let b = BoundedInteger::<10, 1>::new::<5>(); // ERROR: doesn't satisfy `If<{ LOW < HIGH }>: True`
    let c = BoundedInteger::<2, 5>::new::<6>(); // ERROR: expected `false`, found `true`
}

2
据我所知,不完全是这样。但您可以使用 trait 来接近。例如,在以下示例中,吨位是一个预期为 20-100 的无符号 8 位整数,并且是 5 的倍数:
pub trait Validator{
    fn isvalid(&self) -> bool;
}

pub struct TotalRobotTonnage{
    pub tonnage: u8,
}

impl Validator for TotalRobotTonnage{
    //is in range 20-100 and a multiple of 5
    fn isvalid(&self) -> bool{
        if self.tonnage < 20 || self.tonnage > 100 ||  self.tonnage % 5 != 0{
            false
        }else{
            true
        }
    } 
}

fn main() {
    let validtonnage = TotalRobotTonnage{tonnage: 100};
    let invalidtonnage_outofrange = TotalRobotTonnage{tonnage: 10};
    let invalidtonnage_notmultipleof5 = TotalRobotTonnage{tonnage: 21};
    println!("value {} [{}] value {} [{}] value {} [{}]", 
    validtonnage.tonnage, 
    validtonnage.isvalid(),
    invalidtonnage_outofrange.tonnage, 
    invalidtonnage_outofrange.isvalid(),
    invalidtonnage_notmultipleof5.tonnage, 
    invalidtonnage_notmultipleof5.isvalid()
);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接