Тип данных, который принимает только диапазон значений

Скажем, у меня есть функция, которая принимает аргумент типа u16. Есть ли элегантный способ определения пользовательского типа данных, который ведет себя точно так же, как u16, но имеет только значения от 0 до 100?

Ответ 1

Насколько я понимаю, для этого требуются зависимые типы, которых у 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);
    println!("{}, {}", std::mem::size_of::<Age>(), std::mem::size_of::<u16>());
}

Конечно, он не ведет себя точно так же, как u16, но вы тоже этого не хотите! Например, u16 может выходить за рамки 100... Вы должны будете рассуждать, если имеет смысл добавлять/вычитать/умножать/делить и т.д. Также ваш новый тип.

Важно отметить, что этот новый тип занимает столько же места, сколько u16 - тип оболочки эффективно стирается при компиляции кода. Средство проверки типов проверяет, что все находится в зацеплении до этого момента.

Ответ 2

К сожалению, внутри стандартного ящика такого нет.

Тем не менее, вы можете сделать это самостоятельно оптимизированным способом с ночными универсальными константами. Пример:

#![feature(const_generics)]

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, который позволяет генерировать ограниченное целое число на лету с помощью макроса.