【发布时间】:2020-01-24 00:43:25
【问题描述】:
我有一个接受u16 类型参数的函数。是否有一种优雅的方式来定义行为与u16 完全相同但只有 0 到 100 之间的值的自定义数据类型?
【问题讨论】:
我有一个接受u16 类型参数的函数。是否有一种优雅的方式来定义行为与u16 完全相同但只有 0 到 100 之间的值的自定义数据类型?
【问题讨论】:
据我了解,这需要 dependent types,而 Rust 没有。 这不需要依赖类型(请参阅 cmets),但 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,但你也不希望它这样做!例如,u16 可以超过 100... 如果对您的新类型进行加/减/乘/除等有意义的话,您必须推理。
为了最大限度地保护,您应该将您的类型和任何相关功能移到一个模块中。这利用了 Rust 的可见性规则来防止人们意外访问 newtype 中的值并使约束无效。
您可能还希望实现TryFrom(从u16 到您的类型)或From(从您的类型到u16)以更好地与通用代码集成。
需要注意的重要一点是,这种新类型与u16 占用的空间量相同——在编译代码时,包装类型会被有效擦除。类型检查器确保在该点之前一切都网格化。
【讨论】:
Age 放在模块内,不要公开该字段。
不幸的是,std crate 中没有这样的东西。
但是,您可以使用夜间通用 const 以优化的方式自己完成,计划在 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 这样的 crate,它允许使用宏即时生成有界整数。
【讨论】:
使用夜间功能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`
}
【讨论】:
据我所知,不完全是。但是您可以使用特征来接近。示例,其中吨位是一个无符号的 8 位整数,预计为 20-100 和 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()
);
}
【讨论】: