写给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
是一个VecVec<>
你可以称他是向量或者集合或者数组集合,它是一个可变大小的集合类型,实现原理有点像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>
:
可以看到里面也是两部分,一个空和一个内容。我们给我们上面自定义的枚举增加两个方法,让他和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
中的Something
,Option
中的Some
,result::Result
中的两个分支OK
和Err
。要拿到其中的数据,除了使用模式匹配,一般会定义一个方法unwrap
。这个方法的返回就是枚举中存储的数据:当确有数据时,就拿到数据;没有数据时,会报出异常。所以当我们确定(或要求)其中必须有值的时候可以调用这个方法,否则还是使用模式匹配分别处理。