Rust的面向对象(二)——枚举

Rust的面向对象(二)——枚举

从1973年C语言主体完成到1983年C++正式命名,10年的时间,编程理念经历了一次从面向过程到面向对象的蜕变。数据的组织形式也从struct到class得到了升华。自那以后,几乎所有语言的类都是class,相当一部分程序员知道怎么用class却不能准确说出来面向对象是什么。然而,Go和Rust却同时站出来说:“我们不需要class。”顿时,全场静音。

事实上,面向对象的概念早在1967年的Simula语言中就开始提出来了,比C语言的面世还早了6年。可是时至今日,全网也找不到一个权威的面向对象的定义,有的都是用会意的方式来解释什么是面向对象。

相比面向对象抽象的定义而言,面向对象的三要素却是非常具体的,它们是:

  • 封装:为了增强安全性和简化编程的复杂性,隐藏对象的属性和实现细节,仅对外公开接口
  • 继承:为了提高代码复用率,子类可继承父类的属性和方法
  • 多态:为了消除类型之间的耦合关系,提升代码的灵活性,子类可覆盖父类的方法

封装很好理解,就是定义类的属性和方法,并且提供访问级别来控制其它类的对象是否可以访问它们。Rust虽然没有类,但可以使用结构体或枚举来定义属性,并为结构体或枚举附加方法来实现。

而继承是类之间的关系,因为没有类,Rust和Go都没有继承的概念。不过Rust巧妙的用泛型以及泛型的特性实现了一套与继承相类似的代码共享机制。

多态是建立在继承之上的,它允许在使用父类的地方使用子类,Rust的特性也拥有类似的能力。

在学习Rust的面向对象之前,需要充分意识到接下来要学习的东西与你之前认识的世界有着天壤之别,不要被过去的知识所束缚,退一步,进入一个崭新的世界。

学习Rust的面向对象是一个漫长的过程,里面要掌握的知识非常多,我们可以使用下面的路线图来学习Rust的面向对象:

  1. 创建结构体枚举
  2. 为结构体或枚举实现方法
  3. 为结构体或枚举实现特性

枚举

枚举是C/C++语言里常用的数据类型,后来渐渐的被其他语言(如python)遗忘。在Rust语言中,枚举类型又回来了,不但回来了,而且得到了前所未有的发展壮大。

一般的枚举

在C语言中,枚举的意义在于为整型取名字,提高代码的可读性,如星期几就是一个很常见的例子。在Rust中,枚举不再被映射为整型,但仍然可以这么使用:

#[derive(Debug)]
enum WeekDay {
    Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}

fn main() {
    let day = WeekDay::Sunday;
    println!("day is {:?}", day);
}

不一般的枚举

除了星期几,加上一些自定义的状态等,貌似枚举也就能用在这样的场合了,使用常量同样能达到类似的目的,所以枚举渐渐的被其他语言遗忘,但Rust却为枚举赋予了新生。

假如我们要访问一台主机,可以通过主机名访问,也可以通过IP地址访问。像同一天只可能是WeekDay中一个一样,同一次访问只能选择主机名或IP地方其中之一,这才是枚举的本质。

也许你会说,主机名和IP地址都是一个字符串,我根本不需要区分它们。是的,但对于DNS而言,他们就像单位名称和门牌号码一样,完全不同。那么,它首先要知道这个字符串是什么类型,然后才能进行不同的操作。你是不是一下就想到了解决方案:

#[derive(Debug)]
enum AddrType {
    Domain, Ip
}

#[derive(Debug)]
struct Addr {
    addrtype: AddrType,
    addr: String
}

fn main() {
    let host = Addr {
        addrtype: AddrType::Domain, 
        addr: String::from("localhost")
    };
    println!("host is {:?}", host);
}

这样做是没有问题的,但这还是一般枚举的思路,Rust依然有捷径可以利用:

#[derive(Debug)]
enum Addr {
    Domain(String), Ip(u8, u8, u8, u8)
}

fn main() {
    let host = Addr::Ip(127, 0, 0, 1);
    println!("host is {:?}", host);
}

惊不惊喜,意不意外,这个例子一下就让我们进入了新的天地:枚举中可以任意的类型,而且每个枚举项可以是不同的类型。

模式匹配

将枚举与模式匹配深度结合,是Rust最伟大的创举之一。有了模式匹配,枚举变的更加实用。最简单的,它代替了if else:

enum WeekDay {
    Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}

fn main() {
    let day = WeekDay::Sunday;
    match day {
        WeekDay::Monday => println!("还没休息过来呢,又要开始上班了。"),
        WeekDay::Tuesday => println!("昨天光抱怨了,今天必须努力工作。"),
        WeekDay::Wednesday => println!("还好昨天效率高,耽误的工作补回来了。"),
        WeekDay::Thursday => println!("这周过去一半多了,胜利就在前方。"),
        WeekDay::Friday => println!("为了明天不加班,今天还要再努力。"),
        WeekDay::Saturday => println!("终于可以休息了,吃什么好吃的呢。"),
        WeekDay::Sunday => println!("在家憋了一天,今天出去逛逛吧。")
    }
}

接下来就该处理我们的DNS了,为了IP地址更通用,我把枚举改造一下,将IP地址抽取成元组结构体:

use std::collections::HashMap;

struct IpAddr(u8, u8, u8, u8);

enum Addr {
    Domain(String), Ip(IpAddr)
}

fn dns(domain: String) {
    let mut route_table = HashMap::new();
    route_table.insert(String::from("localhost"), IpAddr(127, 0, 0, 1));

    match route_table.get(&domain) {
        None => println!("无法映射到该主机名"),
        Some(ip) => println!("映射到IP地址({}.{}.{}.{})", ip.0, ip.1, ip.2, ip.3)
    }
}

fn main() {
    let host = Addr::Domain(String::from("localhost"));
    
    match host {
        Addr::Domain(domain) => dns(domain),
        Addr::Ip(ip) => println!("通过IP地址访问({}.{}.{}.{})", ip.0, ip.1, ip.2, ip.3)
    }
}

这个例子中,使用了两次模式匹配。main函数中是对我定义的Addr枚举进行处理,当访问方式为主机名时,调用dns函数查找路由表将主机名翻译为ip地址,当访问方式为IP地址时,则直接访问(输出)。

在dns函数中,因为路由表中可能无该主机名的信息,所以可能会得到空的结果,route_table.get()的返回值就是一个Option枚举(后面会专门学习它),必做对有值和没值的情况分别处理。

内置的枚举

Option

Rust的标准库中,有一个内置的枚举类型,名为Option,它被广泛应用于需要处理空值的地方。在太多太多的场景中,一个变量,要么有值 ,要么没值,当你像有值那样处理空值 ,就会出现bug,而这种bug出现的频率非常之高。Rust创造了Option,就是为了减少这种bug的出现。

Option<T>包含在标准库中,定义如下:

enum Option<T> {
    Some(T),
    None,
}

它太过重要,以至于Rust对其进行了特殊处理,你并不需要引用就可以使用它,甚至可以直接使用Some(T),而不需要使用Option::Some(T)。

<T>对于使用过C++的人来说并不陌生,它是泛型,先不去管它。重点在于它可以接受任意类型的参数,如:

let some_number = Some(5);
let some_string = Some("a string");

因为省略了Option::,我们看上去创建了一个Some对象,实际上,它是一个Option<T>枚举的成员。就像之前创建day那样:

let day = WeekDay::Sunday;

而如果成员的类型是None,则必做指明<T>的具体类型:

let absent_number: Option<i32> = None;

因为它Option枚举要么有值要么没值,可以起到与空值一样的效果,它的意义在于,通过Option枚举,编译器可以强制你对空值进行处理,避免遗忘而造成bug。例如下面的python代码会出现空值异常:

a = 10
b = None
print(a + b)

而它的Rust版本不会通过编译:

fn main() {
    let a:i32 = 10;
    let b:Option<i32> = None;
    println!("{}", a+b);
}

甚至,即使b不是None,而是有意义的值,也不能通过编译:

fn main() {
    let a:i32 = 100;
    let b:Option<i32> = Some(20);
    println!("{}", a+b);
}

编译时都会出现错误:no implementation for `i32 + std::option::Option<i32>。

Result

Result是Rust内置的另一个枚举,通常被用于错误处理。它的定义如下:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

与Option的<T>类似,它有两个泛型参数<T, E>,T代表正常情况下Ok成员的值,而E代表的错误发生时Err成员的值。标准库的io操作经常会使用Rusult枚举:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    match f {
        Ok(file) => println!("open file success"),
        Err(error) => println!("open file error:{:?}", error)
    };
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值