文 Akisann@CNblogs / zhaihj@Github

本篇文章同时发布在Github上:https://zhaihj.github.io/enum-generic-and-templates.html

在很久之前,我曾经写过(或者说,翻译过)一篇关于OOC里泛型的博客,在那个时候,我对OOC的泛型设计是持否定态度的——相比起OOC的动态泛型,那时的我认为类似C++的泛型更加好用。类型在编译时是确定的,因此编译器可以进行静态类型检查,同时没有执行时的性能损失,也不需要在使用时cast,不会出现错误……总之,似乎没有理由去选择OOC的设计。

在那之后的2~3年里,我也一直都是这么认为的。

当然,Rust也是这样的,因此这几年我也一直很满足,知道最近遇到的问题。

An Example of Deserialization

让我们先来考虑一个简单的场景,有某个服务用Json传送信息,里面包含了一个服务器列表,服务器有几种类型,每一种有不同的属性,比如:

  1. {
  2. "server_list": [
  3. {
  4. "name": "server_a",
  5. "role": "front",
  6. "scale": 10
  7. },
  8. {
  9. "name": "server_b",
  10. "role": "worker",
  11. "is_debug": false,
  12. "restart_time": "23:55",
  13. "restart_type": "everyday"
  14. },
  15. {
  16. "name": "server_c",
  17. "role": "backup",
  18. "scale": "100",
  19. "storage_limit": "24G",
  20. "log_level": "debug"
  21. }
  22. ]
  23. }

直接操作json肯定不是好选项,大部分情况下用serde先Deserialize是个不错的办法。

  1. struct Server {
  2. server_list: Vec<...>,
  3. ....
  4. }
  5. let server_list : Server = serde_json::from_str(&json_str)?;
  6. ....

现在问题就来了,server_list显然是一个Vec,但它的内容不是一致的——里面其实有数个不同的类型。

并且这种写法并不少见,json,xml,yaml等等都可以这么做。

如果不同类型的属性名称是不同的,那么我们可以把它们全部合并成一个巨大的struct,然后根据role来判断需要哪些field:

  1. struct ServerItem {
  2. name: String,
  3. role: String,
  4. scale: Option<i64>,
  5. is_debug: Option<bool>,
  6. restart_time: Option<String>,
  7. restart_type: Option<String>,
  8. storage_limit: Option<String>,
  9. log_level: Option<String>,
  10. ...
  11. }
  12. for item in &server_list.server_list {
  13. match item.role.as_str() {
  14. "front" => {
  15. ...
  16. },
  17. "worker" => {
  18. ...
  19. },
  20. "backup" => {
  21. ...
  22. },
  23. _ => {
  24. unreachable!()
  25. }
  26. }
  27. }

这样我们就能统一的访问这些成员了。当然,每一次访问都需要判断role,并且要处理大量的Option,导致代码看起来很冗长。(Rust的Option的Zero-cost是指内存上的,但并不代表代码上写起来是zore-cost的)

并且,另外一个更重要的问题是——如果不同种类的属性之间有冲突,这个办法就没法用了。比如这里的scale,在front里他是一个数字,然而在backup里他是一个字符串。这样处理起来就麻烦多了。

当然,serde也能处理这种情况:

  1. fn any_to_str<T, S>(data: &T, s: S) -> Result<S::Ok, S::Error>
  2. where
  3. S: Serializer,
  4. T: std::fmt::Debug,
  5. {
  6. s.serialize_str(&format!("{:?}", &data))
  7. }
  8. struct ServerItem {
  9. ...
  10. #[serde(deserialize_with="any_to_string")]
  11. scale: Option<String>,
  12. }

这样,任何类型的scale都会转换成字符串,我们可以在后面的处理中根据需要再parse回数字。

很显然的,这种做法效率很低,并且会导致代码进一步的复杂,如果未来消息里不停的有这种情况,我们要不停的修改这个巨大的struct,并且跟着修改各种对应的parse。并且,随着类型的增加,这个巨大struct会失去维护性——从字面上根本看不出哪些类型拥有哪些属性,我们也无法在deserialize时检查数据是不是正确的了(因为他们全都是Option的)。

Enum Varints

一个比较常见的解决办法就是用Enum了,Rust的Enum Variants可以像Struct一样用自己的成员,因此,对上面的例子我们可以这样写:

  1. #[serde(tag = "role")]
  2. enum ServerItem {
  3. front {
  4. name: String,
  5. scale: i64,
  6. ...
  7. },
  8. worker {
  9. name: String,
  10. is_debug: bool,
  11. restart_time: String,
  12. restart_type: String,
  13. ...
  14. },
  15. backup {
  16. name: String,
  17. scale: String,
  18. storage_limit: String,
  19. log_level: String,
  20. ...
  21. },
  22. }

这样,我们可以把列表Parse成一个ServerItem的Vec了,每一个属性都是只跟当前的类型有关,不再需要类型转换和Option了。这样,在处理Vec的时候会变得很轻松。

不过,其实还有一个问题——处理的时候我们依然需要判断类型,就像这样:

  1. for item in &server_list.server_list {
  2. match item {
  3. ServerItem::front{name, scale, ...} => {
  4. ...
  5. },
  6. ServerItem::worker{name, ...} => {},
  7. serverItem::backup{name, scale, ...} => {},
  8. }
  9. }

在第一次遇到一个ServerItem的时候,判断类型并没有什么问题,然而就算我们已经知道了它的类型,每次用到它还是需要重新来一次:

  1. fn process_front(item: &ServerItem) {
  2. match item {
  3. &ServerItem::front{ name, scale, ...} => {},
  4. _ => { unreachable!() },
  5. }
  6. }
  7. // 我们已经知道这是一个front
  8. process_front(&item);

可以想象到每次改变scope,我们都要重新确认item到底是什么,但我们早就知道了——因此这除了让代码边长之外并没有什么意义。为了避免这种情况,我们需要一些办法。

Enum::as_struct

一个很直白的方法就是:对enum,我们准备很多as_..的方法,把每个variant都转换成对应的struct。实际上,serde_json的Value就是这么做的

  1. enum ServerItem { ... }
  2. struct Front { ... }
  3. struct Worker { ... }
  4. struct Backup { ... }
  5. impl ServerItem {
  6. fn is_front(self) -> bool { ... }
  7. fn is_worker(self) -> bool { ... }
  8. fn is_backup(self) -> bool { ... }
  9. fn as_front(self) -> Option<Front> { ... }
  10. fn as_worker(self) -> Option<Worker> { ... }
  11. fn as_backup(self) -> Option<Backup> { ... }
  12. }

这样,我们在处理之前,可以把它们转换成对应的类型:

  1. ...
  2. let front = item.as_front();
  3. process_front(&front);

这样下来,后面的处理就变得简洁多了。这也是目前主流的做法。

但还有一个问题,这种处理能不能变得更简洁一些?

Enum Variants as Type

一个很直接的想法就是,让每个Enum Variant都成为单独的类型,这样我们就能把参数定义成这个variant,或者用泛型来处理了,比如:

  1. fn process_front(front_item: ServerItem::Front) {
  2. ...
  3. }

显然如果能够这么做,那么上面的问题大都不存在了,我们甚至不需要这种函数,因为在上面的循环里直接处理就已经很清晰了:

  1. for item in server_list.server_list {
  2. // process item
  3. match item {
  4. ServerItem::Front => {
  5. // process item directly
  6. },
  7. ...
  8. }
  9. }

在这里,每个item在match之前就已经带着类型了,这里的match仅仅是一个guard,并不涉及类型转换。按照这个设计,下面的写法也是正确的:

  1. enum Foo {
  2. A (i32, i64 ),
  3. B (String, i8),
  4. C,
  5. }
  6. let foo = Foo::A { 10, 20};
  7. match foo {
  8. A | B => {
  9. // handle foo
  10. },
  11. C => {
  12. // do nothing
  13. }
  14. }

到这里,所有熟悉Rust的人都会看出问题——这跟目前的类型系统是有矛盾的。A和B是不同的类型,虽然foo的类型是确定的,但在A|B的Arm下我们并不知道它到底是哪一种,因此也无法取出它的内部数据。就算写成A(bar, baz) | B (bar, baz),这里的bar和baz的类型依然是冲突的,它的类型在编译期不确定,自然也没法这么使用。(纵使给他们不同的名字,我们也不知道到底那个Arm match了,因此每个变量都是Option的,我们还是要挨个判断)

其实,Rust的开发者们从2016年就想给Enum Variants加类型了,但上面这个问题一直是绊脚石。2018年有人重新提起了这个问题,也并没有获得很多正面反馈。

Enum Variants and Generics

这让我想起了过去的OOC。实际上OOC的Generics看起来就像是专门用来解决这个问题的。用Rust的语言来说,其实OOC打算实现这么一个东西:

对于这么一个定义:

  1. enum Foo {
  2. A(i32, i64),
  3. B(String, i8),
  4. }

编译器会把它翻译成:

  1. struct A {
  2. _ano1: i32,
  3. _ano2: i64,
  4. }
  5. struct B {
  6. _ano1: String,
  7. _ano2: i8,
  8. }
  9. trait Foo{
  10. fn whoami(&self) -> TypeId;
  11. }
  12. impl Foo for A {
  13. fn whoami(&self) -> TypeId {
  14. std::any::TypeId::of<A>()
  15. }
  16. }
  17. impl Foo for B {
  18. fn whoami(&self) -> TypeId {
  19. std::any::TypeId::of<B>()
  20. }
  21. }

因此,实际上Foo并不是真正的类型(这里仅仅使用Rust的语言来描述,我们只能用Trait,实际上OOC的定义要更自然一些,更接近一个Meta类型)。当我们使用它的variants时,其实是这样的:

比如下面的代码:

  1. // 实际上,foo的类型是Box<dyn Foo>,它的“实际类型”是A。
  2. let foo = Foo::A(64, 32);
  3. match foo {
  4. // 编译器有foo的所有信息,显然这里是可以判定的,但cast则是由用户完成的。
  5. // 这意味着用户可以故意的把一个A cast成一个B,但这会导致运行时Panic。
  6. Foo::A => process(foo as A),
  7. Foo::B => process2(foo as B),
  8. ...
  9. }

其实会被翻译成:

  1. let foo = Box::new(A {64, 32}) as Box<dyn Foo>;
  2. match foo.whoami() {
  3. std::any::TypeId::of<A> => {
  4. let _tmp_foo = (foo as Box<dyn Any>).downcast_ref::<A>();
  5. //这时,_tmp_foo的类型已经是A了。
  6. process(_tmp_foo);
  7. },
  8. std::any::TypeId::of<B> => {
  9. let _tmp_foo = (foo as Box<dyn Any>).downcast_ref::<B>();
  10. process(_tmp_foo);
  11. }
  12. }

当然,这并没有解决所有的问题(尤其是Rust存在的binding问题),但对于大部分的情况,它足够强壮,也足够优雅了——我们有了variants的类型,没有失去类型检查,编译器可以解决绝大部分的转换问题,除了稍微有一点运行时的损耗(但这是必不可少的)。

所以,每次会想起在OOC里发生的争论,我对会回过头来看Rust里的设计,C++的Template是否真的比OOC的Generic优雅?运行时的检查和Cast是否比确定性的生成要受限制?每次用到泛型时写一次cast是否真的比编译器的静态检查要冗长?

三年前,我或许会毫不犹豫的回答“是”,但现在,我又没法下结论了。

Enum, Generic and Templates的更多相关文章

  1. How to Start a Business in 10 Days

    With an executive staffing venture about to open, a business loan from the in-laws gnawing at her co ...

  2. Awesome Django

     Awesome Django    If you find Awesome Django useful, please consider donating to help maintain it. ...

  3. <Effective C++>读书摘要--Templates and Generic Programming<一>

    1.The initial motivation for C++ templates was straightforward: to make it possible to create type-s ...

  4. Asp.Net 将枚举类型(enum)绑定到ListControl(DropDownList)控件

    在开发过程中一些状态的表示使用到枚举类型,那么如何将枚举类型直接绑定到ListControl(DropDownList)是本次的主题,废话不多说了,直接代码: 首先看工具类代码: /// <su ...

  5. .net MVC 中枚举类型Enum 转化成 下拉列表的数据源

    第一次写技术博文,记录下工作中遇到的问题,给自己的知识做个备份,也希望能帮助到其他的同学 最近接手了公司的一个新的项目.有个页面涉及相关设计. 分享一个经常用到的吧. 方法一: 直入主题吧 我们的目的 ...

  6. 适当使用enum做数据字典 ( .net c# winform csharp asp.net webform )

    在一些应用中,通常会用到很多由一些常量来进行描述的状态数据,比如性别(男.女),审核(未审核.已审核)等.在数据库中一般用数字形式来存储,比如0.1等. 不好的做法 经常看到一些应用(ps:最近又看到 ...

  7. C#的Enum——枚举

    枚举 枚举类型声明为一组相关的符号常数定义了一个类型名称.枚举用于“多项选择”场合,就是程序运行时从编译时已经设定的固定数目的“选择”中做出决定. 枚举类型(也称为枚举)为定义一组可以赋给变量的命名整 ...

  8. Enum Helper

    public static class EnumHelper { #region get /// <summary> /// 获得枚举类型所包含的全部项的列表 /// </summa ...

  9. [CareerCup] 14.4 Templates Java模板

    14.4 Explain the difference between templates in C++ and generics in Java. 在Java中,泛式编程Generic Progra ...

随机推荐

  1. [Micropython]TPYBoard v202 智能WIFI远控小车

    转载请注明文章来源,更多教程可自助参考docs.tpyboard.com,QQ技术交流群:157816561,公众号:MicroPython玩家汇 前言---------------------- 之 ...

  2. 被裁的第50天,我终于拿到心仪公司Offer

    今天分享的是之前分享文章中被裁的小C,可以看这篇文<寒冬之下,被cai的那些人到底去哪了?>,最近他已经找到心仪公司今日头条Offer,并且即将入职,在应我要求下,他写了篇总结文如下.下文 ...

  3. 引用dll出现的问题:发生一个或多个错误,引用无效或不支持该引用

    获取到新的项目后,然后FineUI就出现黄色的标志,肯定是不可以用的,需要重新引用下. 然后我就开始重新引用下,就出现下面的问题: 因为是购买的UI,一开始我怀疑是引用的版本不一样呢,其实都不是 只需 ...

  4. 测试必备之Java知识(四)—— 线程相关

    线程相关 Java多线程实现方式 继承Thread,实现Runnable接口,实现Callable接口(能抛异常且有返回值,不常用) 为什么有了继承Thread方式还要有Runnable接口方式 实现 ...

  5. MacOSX 安装 TensorFlow

    TensorFlow是一个端到端开源机器学习平台.它拥有一个包含各种工具.库和社区资源的全面灵活生态系统,可以让研究人员推动机器学习领域的先进技术的. 准备 安装 Anaconda TensorFlo ...

  6. colab上基于tensorflow2.0的BERT中文多分类

    bert模型在tensorflow1.x版本时,也是先发布的命令行版本,随后又发布了bert-tensorflow包,本质上就是把相关bert实现封装起来了. tensorflow2.0刚刚在2019 ...

  7. Java中SMB的应用

    目录 SMB 服务操作 Ⅰ SMB简介 Ⅱ SMB配置 2.1 Windows SMB Ⅲ 添加SMB依赖 Ⅳ 路径格式 Ⅴ 操作共享 Ⅵ 登录验证 SMB 服务操作 Ⅰ SMB简介 ​ SMB(全称 ...

  8. Deep server from scratch

    Deep server from scratch 1.install Ubuntu16.04 via flash2.wired Network by Ruijie3.install google4.S ...

  9. Nginx核心模块

    error_log 语法:error_log file [ debug | info | notice | warn | error | crit ]默认值:${prefix}/logs/error. ...

  10. java 编程小知识点

    --------------------------------- 时间不多了,抓紧做自己喜欢的事情 1. 使用位运算 & 来判断一个数是否是奇数.偶数的速度很快 (a & 1 ) = ...