Newtypes
设计模式
请重点看两个[例程],[例程]写得真的很好,[例程]更精彩。
适用场景:
- 克服【孤儿原则】,间接地将第三方
crate
声明的trait
(e.g.Display trait
)实现于第三方crate
定义的type
(e.g.Vec<T>
)上。从而,低成本地增强一个已有类型,而不是“重新造轮子”。在js
中,这类作法比比皆是。 - 进一步语义化数据类型。举个例子,让
rustc
类型系统识别一个i32
字面量5
为5
米,而不是5
头猪。这样可以杜绝程序计算中出现“5
米5
头猪”的逻辑错误。
场景一:克服【孤儿原则】
操作步骤 [例程1]:
- 首先,在本地
crate
给第三方type
定义一个“薄”包装器类型Wrapper
(一般为【元组结构体】)。 - 然后,将第三方
type
作为本地Wrapper
私有字段的数据类型。 - 接着,给本地
Wrapper
实现第三方trait
。 - 于是,形成了类型链条:“第三方
trait
-- 被实现于 --> 本地Wrapper
类型 -- 代理 --> 第三方type
”。- 给本地
Wrapper
实现Deref / DerefMut trait
,将其变形为【智能指针】。 - 借助于
Deref Coercion
,本地Wrapper
类型实例能够直接.
出第三方type
的成员方法与字段。从而,达成【代理】的目的。
- 给本地
- 最后,【孤儿原则】破防。
场景二:语义化数据类型
最直观的作法是:
给每一个语义单位(比如,米、千米、斤、吨)分别创建一个独立的(tuple) struct
(比如,struct Miles(f64);
)来
- 包装标量值
- 明确语义
从而避免在程序中出现“n
米 m
斤”的错误逻辑,因为rustc
会警告类型不匹配。这个作法的弊端就是:
- 当对语义化数据类型做【操作符-重载】时,操作符
trait
(比如,std::ops::Add
)需要在每个语义化(tuple) struct
上都被实现一遍。 - 于是,相似的
trait
实现代码会被重复多次,因为,无论语义单位是“斤”还是“米”,其标量值的四则运算规则实际都是相同的。
更高级的作法是:
- 将【语义单位】抽象成为共用【语义-包装类型】的【泛型类型参数】。而不是,给每一个语义单位分别创建一个独立的具体类型 --- 真有点傻乎乎的。
- 借助于
std::marker::PhantomData
,将代表了语义单位的【泛型类型参数】作为【编译时】的类型标记,而不是【运行时】值。 - 在静态类型检查之后,该类型标记便会被抛弃掉,而不会造成任何的运行时成本 --- 仅作为辅助【类型系统】静态代码检查的临时语法项。
所以,我理解std::marker::PhantomData
newtypes
设计模式 = 零(运行时)成本的语义化抽象。
具体的作法 [例程2]:
- 声明一个
(tuple) struct
作为【语义-包装类型】。比如,struct SemVal<A, B>(A, PhantomData<B>);
。- 前者为标量值数据类型;
- 后者为编译时语义标记。
- 前一个字段保存标量值;
- 后一个字段为
std::marker::PhantomData
占位类型标记。 - 有两个字段。
- 有两个泛型类型形参。
(tuple) struct
是通用【语义-包装器】。而,所有语义信息都存储在它的泛型类型参数里。
- 给
(tuple) struct
做各种“赋能”- 【操作符-重载】,赋能标量值的“四则运算”能力。
- 实现
std::fmt::Display trait
,赋能打印日志输出能力。即,输出有语义类型说明的标量值。 - 派生
PartialEq, PartialOrd trait
,赋能【大小比较】能力。 - 派生
Clone, Copy trait
,使其如标量值一样具有【复制-语义】,而不是【所有权-转移】。 - 实现
std::ops::Deref / std::ops::DerefMut trait
,将其变形成【智能指针】和支持Deref Coercion
。 - 实现
std::convert::From trait
,赋能不同语义单位之间的标量值换算。比如,英寸<->
厘米。
- 给每个【语义单位】分别定义一个
unit type
。比如,用struct Centimeter;
代表厘米。- 敲黑板,强调重点:虽然此
unit type
仅只作为【编译时】类型标记(并不会渗入【运行时】),但由于【auto trait
扩散规则】,咱们也必须对其做Clone, Copy, PartialEq, PartialOrd trait
的派生。否则,【语义-包装类型】将不具有【复制-语义】与【大小比较】能力。
- 敲黑板,强调重点:虽然此
- 实例化一个有(业务逻辑)语义“加持”的标量值。例如,
let cm1 = SemVal::<_, Centimeter>::new(5.0, PhantomData);
- 标量的具体类型由
rustc
推断 - 泛型参数
Centimeter
标记(业务逻辑)语义类型,代表5.0
是厘米 - 由
PhantomData
实例占位。
- 标量的具体类型由
- 最后,
rust
类型系统就会确保- 不同(业务逻辑)单位之间,标量值不能四则运算与大小比较。
- 但,它们可相互做单位换算。
- 由于【复制语义】,它们不会所有权转移。
- 能
.
出所有标量类型的成员方法。比如,求【对数】和【弧度换算】等。
结束语
关于Newtypes
设计模式的分享大约就这些。后续有新的感悟与收获,我再补充。请大家持续关注。
例程1:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=5f0e0536db246c407e7736551c5a78ee
例程2:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=157195d69872478c9a77acad2107868c