新类型是Rust中的通用参数吗?

时间:2019-10-30 02:37:06

标签: generics rust

假设我具有以下新类型:

pub struct Num(pub i32);

现在,我有一个函数可以接受可选的Num

pub fn calc(nu: Option<Num>) -> i32 {
    let real_nu = match nu { // extract the value inside Num
        Some(nu) => nu.0,
        None     => -1
    };
    // performs lots of complicated calculations...
    real_nu * 1234
}

我要编写的是一个通用的extract函数,例如下面的函数(不会编译):

// T here would be "Num" newtype
// R would be "i32", which is wrapped by "Num"

pub fn extract<T, R>(val: Option<T>) -> R {
    match val {
        Some(val) => val.0, // return inner number
        None      => -1 as R
    }
}

这样我就可以绕过calc函数中的match

pub fn calc(nu: Option<Num>) -> i32 {
    // do a lot of complicated calculations...
    extract(nu) * 1234 // automatically extract i32 or -1
}

我怎么写extract

动机:在我编写的程序中,有几种新类型,例如Num,它们包装了i8i16和{{1} }。并且有许多不同的i32函数。在每个calc函数的开头写入所有这些match变得非常重复。

3 个答案:

答案 0 :(得分:2)

这种功能通常是不安全的,因为内部可能是私有的(因此访问受到限制)。例如,假设我们有一个新类型并为其实现Drop

struct NewType(String);

impl Drop for NewType {
    fn drop(&mut self) {
        println!("{}", self.0)
    }
}

fn main() {
    let x = NewType("abc".to_string());
    let y = Some(x);

    // this causes a compiler error
    // let s = match y {
    //     Some(s) => s.0,
    //     None => panic!(),
    // };
}

(playground)

如果函数正常工作,则可以将内部字符串移出新类型。然后,在删除该结构时,它可以访问无效的内存。

尽管如此,您仍可以编写一个宏来实现这些内容。如果您尝试在实现Drop的对象上使用宏,则编译器会抱怨,但否则应该可以。

macro_rules! extract_impl {
    (struct $struct_name: ident($type_name: ty);) => {
        struct $struct_name($type_name);
        impl $struct_name {
            fn extract(item: Option<Self>) -> $type_name {
                match item {
                    Some(item) => item.0,
                    None => panic!(), // not sure what you want here
                }
            }
        }
    };
}

extract_impl! {
    struct Num(i32);
}

impl Num {
    fn other_fun(&self) {}
}

fn main() {
    let x = Num(5);
    println!("{}", Num::extract(Some(x)));
}

(playground)

在宏的输出中具有impl块不会引起任何问题,因为您可以根据需要在原始模块中为单个类型设置多达impl个块。 / p>

更好的API是让extract返回一个选项,而不是一些毫无意义的值或恐慌。然后,调用者可以轻松处理任何错误。

macro_rules! extract_impl {
    (struct $struct_name: ident($type_name: ty);) => {
        struct $struct_name($type_name);
        impl $struct_name {
            fn extract(item: Option<Self>) -> Option<$type_name> {
                item.map(|item| item.0)
            }
        }
    };
}

extract_impl! {
    struct Num(i32);
}

impl Num {
    fn other_fun(&self) {}
}

fn main() {
    let x = Num(5);
    println!("{:?}", Num::extract(Some(x)));
}

(playground)

答案 1 :(得分:1)

这里有两个主要缺失的部分:

  1. 您需要抽象Num的结构,以提供一种在不知道外部类型的情况下提取内部值的方法。
  2. 您需要限制R具有类似数字的属性,以便您可以表达-1的想法。

第一个可以通过为Deref实现Num并将其用作特征绑定来解决。这将使您可以访问“内部”值。还有其他具有相似功能的特征,但Deref可能是您想要的特征:

第二个可以通过实现从One板条箱导入的num-traits特质(以获得1值的思想)和实现std::ops::Neg来解决。可以取反以得到-1。您还需要将R设为CopyClone,以便将其移出引用。

use num_traits::One;
use std::ops::{Deref, Neg}; // 0.2.8

pub struct Num(pub i32);

impl Deref for Num {
    type Target = i32;
    fn deref(&self) -> &i32 {
        &self.0
    }
}

pub fn extract<T, R>(val: Option<T>) -> R
where
    T: Deref<Target = R>,
    R: Neg<Output = R> + One + Copy,
{
    match val {
        Some(val) => *val,
        None => -R::one(),
    }
}

根据您打算如何使用它,您可能希望摆脱R,因为它始终由T确定。照原样,调用方会告知函数TR的具体类型,并确保RT的取消引用目标。但是,如果呼叫者只需要提供T并从R中推导T,那就更好了。

pub fn extract<T>(val: Option<T>) -> T::Target
where
    T: Deref,
    <T as Deref>::Target: Neg<Output = T::Target> + One + Copy,
{
    match val {
        Some(val) => *val,
        None => -T::Target::one(),
    }
}

答案 2 :(得分:0)

结果证明,我想出了一种更轻松,更优雅的方法来完成此任务。首先,为我的新类型实现Default特征:

use std::default::Default;

pub struct Num(pub i32);

impl Default for Num {
    fn default() -> Self {
        Self(-1)
    }
}

然后,在需要时,只需使用unwrap_or_default访问第一个newtype元组元素:

pub fn calc(nu: Option<Num>) -> i32 {
    // do a lot of complicated calculations...
    nu.unwrap_or_default().0 * 1234
}