假设我具有以下新类型:
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
,它们包装了i8
,i16
和{{1} }。并且有许多不同的i32
函数。在每个calc
函数的开头写入所有这些match
变得非常重复。
答案 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!(),
// };
}
如果函数正常工作,则可以将内部字符串移出新类型。然后,在删除该结构时,它可以访问无效的内存。
尽管如此,您仍可以编写一个宏来实现这些内容。如果您尝试在实现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)));
}
在宏的输出中具有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)));
}
答案 1 :(得分:1)
这里有两个主要缺失的部分:
Num
的结构,以提供一种在不知道外部类型的情况下提取内部值的方法。R
具有类似数字的属性,以便您可以表达-1
的想法。第一个可以通过为Deref
实现Num
并将其用作特征绑定来解决。这将使您可以访问“内部”值。还有其他具有相似功能的特征,但Deref
可能是您想要的特征:
第二个可以通过实现从One
板条箱导入的num-traits
特质(以获得1
值的思想)和实现std::ops::Neg
来解决。可以取反以得到-1
。您还需要将R
设为Copy
或Clone
,以便将其移出引用。
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
确定。照原样,调用方会告知函数T
和R
的具体类型,并确保R
是T
的取消引用目标。但是,如果呼叫者只需要提供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
}