写给rust初学者的教程(一):枚举、特征、实现、模式匹配

这系列RUST教程一共三篇。这是第一篇,介绍RUST语言的入门概念,主要有enum\trait\impl\match等语言层面的东西。

安装好你的rust开发环境,用cargo创建一个空项目,咱们直接上代码。懵逼的同僚可以参考我8年前的rust文章:https://www.iteye.com/blog/somefuture-2275494 ,虽然8年了,然并不过时。

背景

我们要写一个小程序寻找集合中的最小元素。如果你对这句描述有疑惑,请不要疑惑,它就是这么迷惑,继续看就好了。

对于一个有三五个元素的整型集合,元素都很小的话我们肉眼看一下就知道最小元素。所以这个程序(或者说是一个函数)返回的类型应该是一个整型的。

java程序员可能会说为啥说函数不是方法。它们的区别是函数是不依附于某个对象的,或者说方法对应Java中的实例方法,而函数是静态方法。rust没有static关键字,所以定义出来的的fn是方法还是函数看他的第一个参数,如果是self就是方法。

但再想想,如果集合是空的,返回哪个整数都不合适吧。总不能返回0吧,那一个集合真的有了个元素是0咋办?万一有的集合里面有负数呢,更难区别。
所以我们这个函数返回的是这么个类型:一个整数,或者啥也没有。

那不就是整型包装类就行吗?

在rust中我们使用枚举enum来来定义这个类型。跟其他语言不太一样,rust中的枚举非常常用,使用上有点像C++中的结构体union

enum

直接在你的main.rs文件中main函数上面写

enum IntOrNothing {
    Int(i32),
    Nothing
}

你看这个枚举里有两个成员,一个是可以携带一个整数的叫 Int,另一个是不携带数据的叫Nothing。当然你可以给他们继续增加参数来携带更多数据,完全没关系。

接下来定义这个求最小值的函数。

fn

方法和函数的定义都用关键字fn,如果要公开就在前面加pub,否则是私有的。函数签名如下:

fn vec_min(vec: Vec<i32>) -> IntOrNothing {}

函数名后面写括号,里面放参数。前面说过,如果第一个参数是self就是实例方法。参数类型用冒号指定,函数返回值类型用->指定。
这里的参数vec是一个Vec类型的集合。Vec<>你可以称他是向量或者集合或者数组集合,它是一个可变大小的集合类型,实现原理有点像Java的ArrayList<>
返回类型就是我们上面定义的枚举类。我们要在传入的参数中寻找最小的元素返回,如果集合是空的就返回枚举中的第二个成员Nothing。所以我们的函数体大致类似:

fn vec_min(vec: Vec<i32>) -> IntOrNothing {
    let mut min = IntOrNothing::Nothing;
	for el in vec {
		// 寻找最小值赋给min
	}
	return min;
}

首先定义一个变量min,定义变量必须使用关键字mut,不然就会是常量。它的初始值赋值成Nothing,并在最后return。为啥我要说return? 因为rust中不需要在最后使用return。rust是表达式语言,和Java是语句声明语言不一样。表达式语言的意思是任意一个大括号保住的块都是一个表达式,最后返回的是表达式块中的最后一个对象(或者说经过计算的整个块)。


举个例子,定义一个比较两个数大小的函数,大概是这样的:

fn max(i: i32, j: i32) -> i32 { if i >= j { i } else { j } }

这里实际是省略了if前面的return。但是如果用Java写,需要把return写到每个if的分支里面才行。


函数也可以定义在其他函数内部,这样外面的函数就不能访问到他了。
如果集合有元素,我们就来遍历它。

for

和其他语言一样,rust也是用for循环。for循环的语法没啥说的,记着就是for-in就好了。
然后对于循环变量el我们来判断,如果它比现在的min小,就更新它。但是min有可能没存储整数,第一轮循环必然是直接赋值而非更新。你可以使用if进行判断,这里我们使用模式匹配。

match

模式匹配在rust中使用也非常广泛,和erlang有点像(比不上erlang那种程度)。在for循环体中写入如下代码:

        match min {
            Int(n) => {min = Int(if el < n {el} else { n })}
            Nothing() => {min = Int(el)}
        }

和其他语言的switch-case有点像。如果min是Nothing(下面的分支),就给他赋值成Int并携带整数el;如果已经是Int了,用变量n捕获其中的整数,并经过和el对比大小生成新的变量赋值给min。可以看到Int的参数可以传入一整个表达式,和传了一个值或者函数效果是一样的。
接下来我们执行一下这个程序看看。

main

和其他多数语言一样,rust也使用main函数作为程序入口。cargo在创建main.rs文件的时候已经创建了main函数,我们修改一下他:

fn main() {
    let v = vec![18,5,7,1,9,27];
    let min = vec_min(v);
    match min {
	Nothing => println!("The number is: <nothing>"),
        Int(n) => println!("The number is: {}", n),
    }
}

我们先用vec!这个宏生成了一个集合,然后用上面的函数计算它里面的最小值,最后打印一句话出来。打印的时候需要用到match来判断,因为rust中println!这个宏只能打印Display这个trait(目前就这样理解就好了),而我们定义的IntOrNothing并没有实现它。但是i32是实现了的,可以打印。

宏是rust中定义生成代码的逻辑规则,我们也可以自定义宏。

你可以多试几个case看一下输出效果。

休息一下,我们再回头来看我们的代码。如果你头脑休息好了,可能会提出这个问题:格式化输出应该属于对象自身,这样我们拿到最小值枚举,直接调用它的输出方法就可以了。
我们来给上面这个枚举增加方法。

impl

impl关键字可以给任何对象增加方法和函数,不论这个对象是自定义的还是rust中已经存在的。
在枚举定义的代码下面增加(当然增加到其他文件里去也可以,只要你能修复遇到的问题)

impl IntOrNothing {
    fn print(self) {
        match self {
            Nothing => println!("The number is: <nothing>"),
            Int(n) => println!("The number is: {}", n),
        };
    }
}

终于用到self了。这是一个方法,不接受任何参数。当自身是Nothing的时候打印第一句,当是整数的时候打印第二句。
现在可以直接调用这个方法了:

fn main() {
    let v = vec![18,5,7,1,9,27];
    let min = vec_min(v);
    min.print();
}

然后作为程序员,开发完整数版本的求最值,那应该想要其他版本的。
前面写的枚举,当有值的时候会携带一个整数,如果想要携带各种类型,我们当然可以开发对于的版本。但是你大概率是从其他语言过来的,肯定想我之前用过泛型,rust可以用吗?

泛型(generic type)

rust也支持泛型,或者说是多态。我们来修改一下前面的枚举,让他支持泛型:

pub enum SomethingOrNothing<T>  {
    Something(T),
    Nothing,
}

很好理解,有数据就放到Something里面,没有就使用Nothing表示。
如何用它支持前面的整数场景呢?

type

不用说也知道,可以用SomethingOrNothing<i32>
此外,rust提供了type关键字将其绑定为新类型:

type IntOrNothing = SomethingOrNothing<i32>;

接下来就可以跟使用IntOrNothing了,跟之前完全一样。
类似的,我们可以定义SomethingOrNothing<bool>或者SomethingOrNothing<SomethingOrNothing<i32>>甚至更复杂的形式。

实际上rust已经提供这个枚举了,叫Option<T>:
image

可以看到里面也是两部分,一个空和一个内容。我们给我们上面自定义的枚举增加两个方法,让他和Option能互相转化。添加方法或函数还记得吗,使用impl

impl<T> SomethingOrNothing<T> {
    fn new(o: Option<T>) -> Self {
        match o { None => Nothing, Some(t) => Something(t) }
    }

    fn to_option(self) -> Option<T> {
        match self { Nothing => None, Something(t) => Some(t) }
    }
}

不看代码自己上手可能会有点惶恐,无从下手;看了代码你会发现非常简单。
这里定义了一个函数和一个方法,函数new接受一个Option对象,转成SomethingOrNothing:这里使用的Self类型,也就是self的类型。

new在rust中不是保留字,创建对象实例用不到它,不像Java。不过人们习惯用new创建对象,所以生成函数一般命名成这样。

还定义了一个方法to_option,出入参没啥说的。

rust提供了完整详细的官方文档,见 https://doc.rust-lang.org/stable/std/option/index.html

既然生成函数是静态的,那就可以不适用impl来分派,我们可以定义其他函数来生成对象:

fn call_constructor(x: i32) -> SomethingOrNothing<i32> {
    SomethingOrNothing::new(Some(x))
}

为了演示,这里的生成流程很长。先使用了Option中的Some,然后传给了new函数,整个作为call_constructor的函数体。

泛型枚举定义好了。现在你可以闭目养神一会,回来后我们继续完成目标:计算任意类型的集合最小值。


要想求任意类型的集合最值,就要求集合元素支持比较大小。前面我们使用的整数,当然是支持的。其他类型如果天然不知道怎么定义比较方法呢?聪明的你一定想到了:使用接口。

trait

rust中的接口称为trait,也就是特征。这个单词我觉得比Java中的interface或C++的template更形象。Java编程中有一个原则叫“基于接口编程”,实际上就是基于行为特征编程。

我们先来定义一个trait,里面有一个方法compare_get_min

pub trait Minimum  {
    fn compare_get_min(self, s: Self) -> Self;
}

这个方法接收一个跟自身相同类型的参数,比较厚返回小的(留意一下参数定义)。
接下来就修改vec_min函数。之前的定义是fn vec_min(vec: Vec<i32>) -> IntOrNothing,只接受整数,也最多返回整数。现在改成这样:

pub fn vec_min<T: Minimum>(v: Vec<T>) -> SomethingOrNothing<T> {}

在函数名称后面写<>,里面定义泛型参数T,这样参数列表中和返回类型中就可以使用T了。同时通过冒号:限制泛型边界,必须实现了traitMinimum。函数体修改如下:

    let mut min = Nothing;
    for e in v {
        min = Something(match min {
            Nothing => e,
            Something(n) => {
                e.compare_get_min(n)
            }
        });
    }
    min

注意看我们用到的trait方法compare_get_min

要让我们之前的代码运行,还有最后一步:让i32实现Minimum。因为只有实现这个trait的元素才能在集合中被拿到最值。

impl Minimum for i32 {
    fn compare_get_min(self, b: Self) -> Self {
        if self < b { self } else { b }
    }
}

再一次,注意表达式语言的应用。

写在最后

这篇文章的最后剧透一个枚举用法。因为枚举中的数据是保存在枚举的某一个分支中的,比如SomethingOrNothing中的SomethingOption中的Someresult::Result中的两个分支OKErr。要拿到其中的数据,除了使用模式匹配,一般会定义一个方法unwrap。这个方法的返回就是枚举中存储的数据:当确有数据时,就拿到数据;没有数据时,会报出异常。所以当我们确定(或要求)其中必须有值的时候可以调用这个方法,否则还是使用模式匹配分别处理。

posted @ 2024-03-01 11:15  大卫小东(Sheldon)  阅读(38)  评论(0编辑  收藏  举报