zoukankan      html  css  js  c++  java
  • Enum, Generic and Templates

    文 Akisann@CNblogs

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

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

    An Example of Deserialization

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

    {
        "server_list": [
            {
                "name": "server_a",
                "role": "front",
                "scale": 10
            },
            {
                "name": "server_b",
                "role": "worker",
                "is_debug": false,
                "restart_time": "23:55",
                "restart_type": "everyday"
            },
            {
                "name": "server_c",
                "role": "backup",
                "scale": "100",
                "storage_limit": "24G",
                "log_level": "debug"
            }
        ]
    }
    

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

    struct Server { 
        server_list: Vec<...>,
        ....
    }
    
    let server_list : Server = serde_json::from_str(&json_str)?;
    ....
    

    现在问题就来了,server_list显然是一个Vec,但它的内容不是一致的——里面其实有数个不同的类型。
    并且这种写法并不少见,json,xml,yaml等等都可以这么做。
    如果不同类型的属性名称是不同的,那么我们可以把它们全部合并成一个巨大的struct,然后根据role来判断需要哪些field:

    struct ServerItem {
        name: String,
        role: String,
        scale: Option<i64>,
        is_debug: Option<bool>,
        restart_time: Option<String>,
        restart_type: Option<String>,
        storage_limit: Option<String>,
        log_level: Option<String>,
        ...
    }
    
    for item in &server_list.server_list {
        match item.role.as_str() {
            "front" => {
                ...
            },
            "worker" => {
                ...
            },
            "backup" => {
                ...
            },
            _ => {
                unreachable!()
            }
        }
    }
    

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

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

    fn any_to_str<T, S>(data: &T, s: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
        T: std::fmt::Debug,
    {
        s.serialize_str(&format!("{:?}", &data))
    }
    
    struct ServerItem {
        ...
        #[serde(deserialize_with="any_to_string")]
        scale: Option<String>,
    }
    

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

    Enum Varints

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

    #[serde(tag = "role")]
    enum ServerItem {
        front {
            name: String,
            scale: i64,
            ...
        },
        worker {
            name: String,
            is_debug: bool,
            restart_time: String,
            restart_type: String,
            ...
        },
        backup {
            name: String,
            scale: String,
            storage_limit: String,
            log_level: String,
            ...
        },
    }
    

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

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

    for item in &server_list.server_list {
       match item {
           ServerItem::front{name, scale, ...} => {
               ...
           },
           ServerItem::worker{name, ...} => {},
           serverItem::backup{name, scale, ...} => {},
       }
    }
    

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

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

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

    Enum::as_struct

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

    enum ServerItem { ... }
    
    struct Front { ... } 
    struct Worker { ... }
    struct Backup { ... }
    
    impl ServerItem {
        fn is_front(self) -> bool { ... }
        fn is_worker(self) -> bool { ... }
        fn is_backup(self) -> bool { ... }
    
        fn as_front(self) -> Option<Front> { ... }
        fn as_worker(self) -> Option<Worker> { ... }
        fn as_backup(self) -> Option<Backup> { ... }
    }
    

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

    ...
    let front = item.as_front();
    process_front(&front);
    

    这样下来,后面的处理就变得简洁多了。这也是目前主流的做法。
    但还有一个问题,这种处理能不能变得更简洁一些?

    Enum Variants as Type

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

    fn process_front(front_item: ServerItem::Front) {
        ...
    }
    

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

    for item in server_list.server_list {
        // process item
        match item {
            ServerItem::Front => {
                // process item directly
            },
            ...
        }
    }
    

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

    enum Foo {
        A (i32, i64 ),
        B (String, i8),
        C,
    }
    
    let foo = Foo::A { 10, 20};
    match foo {
       A | B => {
           // handle foo
       },
       C => {
           // do nothing
       }
    }
    

    到这里,所有熟悉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打算实现这么一个东西:

    对于这么一个定义:

    enum Foo {
        A(i32, i64),
        B(String, i8),
    }
    

    编译器会把它翻译成:

    struct A {
        _ano1: i32,
        _ano2: i64,
    }
    
    struct B {
        _ano1: String,
        _ano2: i8,
    }
    
    trait Foo{
        fn whoami(&self) -> TypeId;
    }
    
    impl Foo for A {
        fn whoami(&self) -> TypeId {
            std::any::TypeId::of<A>()
        }
    }
    impl Foo for B {
        fn whoami(&self) -> TypeId {
            std::any::TypeId::of<B>()
        }
    }
    

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

    比如下面的代码:

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

    其实会被翻译成:

    let foo = Box::new(A {64, 32}) as Box<dyn Foo>;
    match foo.whoami() {
        std::any::TypeId::of<A> => {
            let _tmp_foo = (foo as Box<dyn Any>).downcast_ref::<A>();
            //这时,_tmp_foo的类型已经是A了。
            process(_tmp_foo);
        },
        std::any::TypeId::of<B> => {
            let _tmp_foo = (foo as Box<dyn Any>).downcast_ref::<B>();
            process(_tmp_foo);
        }
    }
    

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

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

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

  • 相关阅读:
    Github上的英文解释
    快速搭建脚手架的方法
    vue生命周期简介和钩子函数
    vue2.0 路由模式mode="history"的作用
    浅谈vue $mount()
    vue——解决“You may use special comments to disable some warnings. Use // eslint-disable-next-line to ignore the next line. Use /* eslint-disable */ to ignore all warnings in a file. ”
    Vue组件中的父子传值
    URL中的hash(井号)
    大数据-高并发网络基础1
    大数据-6Linux-shell编程
  • 原文地址:https://www.cnblogs.com/akisan/p/12353283.html
Copyright © 2011-2022 走看看