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
共 0 条评论, 1 页
评论区
写评论还没有评论