< 返回我的博客

爱国的张浩予 发表于 2022-04-15 08:04

Tags:newtypes,design-pattern

Newtypes设计模式

请重点看两个[例程],[例程]写得真的很好,[例程]更精彩。

适用场景:

  • 克服【孤儿原则】,间接地将第三方crate声明的trait(e.g. Display trait)实现于第三方crate定义的type(e.g. Vec<T>)上。从而,低成本地增强一个已有类型,而不是“重新造轮子”。在js中,这类作法比比皆是。
  • 进一步语义化数据类型。举个例子,让rustc类型系统识别一个i32字面量55米,而不是5头猪。这样可以杜绝程序计算中出现“5米 + 5头猪”的逻辑错误。

场景一:克服【孤儿原则】

操作步骤 [例程1]

  1. 首先,在本地crate给第三方type定义一个“薄”包装器类型Wrapper(一般为【元组结构体】)。
  2. 然后,将第三方type作为本地Wrapper私有字段的数据类型。
  3. 接着,给本地Wrapper实现第三方trait
  4. 于是,形成了类型链条:“第三方trait -- 被实现于 --> 本地Wrapper类型 -- 代理 --> 第三方type”。
    • 给本地Wrapper实现Deref / DerefMut trait,将其变形为【智能指针】。
    • 借助于Deref Coercion,本地Wrapper类型实例能够直接.出第三方type的成员方法与字段。从而,达成【代理】的目的。
  5. 最后,【孤儿原则】破防。

场景二:语义化数据类型

最直观的作法是:

给每一个语义单位(比如,米、千米、斤、吨)分别创建一个独立的(tuple) struct(比如,struct Miles(f64);)来

  • 包装标量值
  • 明确语义

从而避免在程序中出现“n米 + m斤”的错误逻辑,因为rustc会警告类型不匹配。这个作法的弊端就是:

  • 当对语义化数据类型做【操作符-重载】时,操作符trait(比如,std::ops::Add)需要在每个语义化(tuple) struct上都被实现一遍。
  • 于是,相似的trait实现代码会被重复多次,因为,无论语义单位是“斤”还是“米”,其标量值的四则运算规则实际都是相同的。

更高级的作法是:

  • 将【语义单位】抽象成为共用【语义-包装类型】的【泛型类型参数】。而不是,给每一个语义单位分别创建一个独立的具体类型 --- 真有点傻乎乎的。
  • 借助于std::marker::PhantomData,将代表了语义单位的【泛型类型参数】作为【编译时】的类型标记,而不是【运行时】值。
  • 在静态类型检查之后,该类型标记便会被抛弃掉,而不会造成任何的运行时成本 --- 仅作为辅助【类型系统】静态代码检查的临时语法项。

所以,我理解std::marker::PhantomData + newtypes设计模式 = 零(运行时)成本的语义化抽象

具体的作法 [例程2]

  1. 声明一个(tuple) struct作为【语义-包装类型】。比如,struct SemVal<A, B>(A, PhantomData<B>);
    • 有两个字段。
      • 前一个字段保存标量值;
      • 后一个字段为std::marker::PhantomData占位类型标记。
    • 有两个泛型类型形参。
      • 前者为标量值数据类型;
      • 后者为编译时语义标记。
    • (tuple) struct是通用【语义-包装器】。而,所有语义信息都存储在它的泛型类型参数里。
  2. (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,赋能不同语义单位之间的标量值换算。比如,英寸<->厘米。
  3. 给每个【语义单位】分别定义一个unit type。比如,用struct Centimeter;代表厘米。
    • 敲黑板,强调重点:虽然此unit type仅只作为【编译时】类型标记(并不会渗入【运行时】),但由于【auto trait扩散规则】,咱们也必须对其做Clone, Copy, PartialEq, PartialOrd trait的派生。否则,【语义-包装类型】将不具有【复制-语义】与【大小比较】能力。
  4. 实例化一个有(业务逻辑)语义“加持”的标量值。例如,let cm1 = SemVal::<_, Centimeter>::new(5.0, PhantomData);
    • 标量的具体类型由rustc推断
    • 泛型参数Centimeter标记(业务逻辑)语义类型,代表5.0是厘米
    • PhantomData实例占位。
  5. 最后,rust类型系统就会确保
    • 不同(业务逻辑)单位之间,标量值不能四则运算与大小比较。
    • 但,它们可相互做单位换算。
    • 由于【复制语义】,它们不会所有权转移。
    • .出所有标量类型的成员方法。比如,求【对数】和【弧度换算】等。

结束语

关于Newtypes设计模式的分享大约就这些。后续有新的感悟与收获,我再补充。请大家持续关注。

评论区

写评论

还没有评论

1 共 0 条评论, 1 页