本文展示了如何在Rust中使用PhantomType消除重复代码;
源代码:
Rust中的PhantomType 引出问题 如果我们有一个表示距离的类型Meter:
examples/0_normal_type.rs
1 2 3 4 5 6 7 8 9 10 #[derive(Debug, Copy, Clone)] struct Meter { value: f64 , } impl Meter { fn new (value: f64 ) -> Self { Self { value } } }
此时我们需要其支持加法和减法运算,因此我们为其实现了Add和Sub Trait:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 impl Add for Meter { type Output = Meter; fn add (self , another: Meter) -> Self ::Output { let value = self .value + another.value; Meter { value } } } impl Sub for Meter { type Output = Meter; fn sub (self , another: Meter) -> Self ::Output { let value = self .value - another.value; Meter { value } } }
测试一下:
1 2 3 4 5 6 7 8 9 10 fn main () { let one = Meter::new (1.1 ); let three = Meter::new (3.3 ); let four = one + three; dbg!(&four); let two = three - one; dbg!(&two); }
输出如下:
1 2 3 4 5 6 [examples\0_normal_type.rs:37] &four = Meter { value: 4.4, } [examples\0_normal_type.rs:40] &two = Meter { value: 2.1999999999999997, }
如果此时,我们还需要为Kilogram、Liter等类型实现相同的逻辑,则需要重复的实现多个Add和Sub Trait;
虽然我们可以使用过程宏(Macro)实现;
实际上,在Rust中可以使用一个PhantomType来实现,即虚拟的类型!
使用PhantomType PhantomType指的是那些在Runtime中实际上并不存在的类型,但是可以帮助代码在编译器对类型做一些限制;
概念上有些类似于Go中泛型的实现;
我们可以定义一个通用类型:Unit<T>并为其实现Add和Sub Trait;
examples/1_phantom_type.rs
1 2 3 4 5 6 7 8 9 10 11 12 #[derive(Debug, Copy, Clone)] struct Unit <T> { value: f64 , } #[derive(Debug, Copy, Clone)] struct MeterType ;type Meter = Unit<MeterType>;#[derive(Debug, Copy, Clone)] struct KilogramType ;type Kilogram = Unit<KilogramType>;
上面同时声明了Meter和Kilogram类型,MeterType和KilogramType的大小都是0;
同时,得益于Rust的零抽象成本,声明的多个类型在运行时没有其他额外开销!
编译上面的代码会报错:
1 2 3 4 5 6 7 error[E0392]: parameter `T` is never used --> src/main.rs:38:13 | 38 | struct Unit<T> { | ^ unused parameter = help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData` = help: if you intended `T` to be a const parameter, use `const T: usize` instead
编译器要求泛型必须要被使用,因此我们修改Unit声明:
1 2 3 4 5 #[derive(Debug, Copy, Clone)] struct Unit <T> { value: f64 , unit_type: T, }
并增加构造函数:
1 2 3 4 5 6 7 8 impl <T: Default > Unit<T> { fn new (value: f64 ) -> Self { Self { value, unit_type: T::default (), } } }
这里使用的构造函数是在类型T中默认的构造函数,因此避免了此时由于T的类型未确定而无法初始化的问题;
为Unit实现Add和Sub Trait:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 impl <T: Default > Add for Unit <T> { type Output = Unit<T>; fn add (self , another: Unit<T>) -> Self ::Output { let new_value = self .value + another.value; Unit::new (new_value) } } impl <T: Default > Sub for Unit <T> { type Output = Unit<T>; fn sub (self , another: Unit<T>) -> Self ::Output { let new_value = self .value - another.value; Unit::new (new_value) } }
最后,为我们的MeterType和LiterType类型实现默认构造函数:
1 2 3 4 5 6 7 8 9 10 11 impl Default for MeterType { fn default () -> Self { MeterType } } impl Default for KilogramType { fn default () -> Self { KilogramType } }
下面,测试我们的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 fn main () { let one = Meter::new (1.1 ); let three = Meter::new (3.3 ); let four = one + three; dbg!(&four); let one = Kilogram::new (1.1 ); let three = Kilogram::new (3.3 ); let four = one + three; dbg!(&four); }
可以看到,Meter和Kilogram类型都已经实现了加法;
而对于Meter和Kilogram相互之间,是无法操作的,完美!
使用PhantomData 其实在Rust标准库中已经提供了PhantomData类型,用于作为某个结构体数据类型的占位:PhantomData ;
实际上Rust的编译器已经提醒了我们:
1 2 help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`
官方文档中对PhantomData的说明:
Zero-sized type used to mark things that “act like” they own a T.
一个零大小的类型,用于标志他们对一个T类型的所有权;
补充PhantomType说明:
Though they both have scary names, PhantomData and ‘phantom types’ are related, but not identical.
A phantom type parameter is simply a type parameter which is never used. In Rust, this often causes the compiler to complain, and the solution is to add a “dummy” use by way of PhantomData.
PhantomData和PhantomType是不同的;
PhantomType仅仅被用于声明一个永远不会被用到的参数,在Rust中经常会导致编译器告警,因此经常会使用PhantomData代替;
下面的代码使用了PhantomData:
examples/2_phantom_data.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 use std::marker::PhantomData;use std::ops::{Add, Sub};#[derive(Debug, Copy, Clone)] struct Unit <T> { value: f64 , unit_type: PhantomData<T>, } impl <T> Unit<T> { fn new (value: f64 ) -> Self { Self { value, unit_type: PhantomData, } } } #[derive(Debug, Copy, Clone)] struct MeterType ;type Meter = Unit<MeterType>;#[derive(Debug, Copy, Clone)] struct KilogramType ;type Kilogram = Unit<KilogramType>;impl <T> Add for Unit <T> { type Output = Unit<T>; fn add (self , another: Unit<T>) -> Self ::Output { let new_value = self .value + another.value; Unit::new (new_value) } } impl <T> Sub for Unit <T> { type Output = Unit<T>; fn sub (self , another: Unit<T>) -> Self ::Output { let new_value = self .value - another.value; Unit::new (new_value) } } fn main () { let one = Meter::new (1.1 ); let three = Meter::new (3.3 ); let four = one + three; dbg!(&four); let one = Kilogram::new (1.1 ); let three = Kilogram::new (3.3 ); let four = one + three; dbg!(&four); }
从上面的代码可以看到,得益于PhantomData:
我们直接使用PhantomData对unit_type进行了初始化,而无需为各种类型实现Default Trait;
同时,使用 PhantomData<T>,我们可以将类型参数的用途明确地在代码中传达;
附录 源代码: