写给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>
:
可以看到里面也是两部分,一个空和一个内容。我们给我们上面自定义的枚举增加两个方法,让他和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
。这个方法的返回就是枚举中存储的数据:当确有数据时,就拿到数据;没有数据时,会报出异常。所以当我们确定(或要求)其中必须有值的时候可以调用这个方法,否则还是使用模式匹配分别处理。
写给rust初学者的教程(一):枚举、特征、实现、模式匹配的更多相关文章
- 10篇写给Git初学者的最佳教程(转)
身为网页设计师或者网页开发者的你,可能已经听说过Git这个正快速成长的版本控制系统.它由GitHub维护:GitHub是一个开放性的.存储众人代码的网站.如果你想学习如何使用Git,请参考本文.在文章 ...
- Swift中文教程(六)--枚举和结构
原文:Swift中文教程(六)--枚举和结构 Enumerations 枚举 使用 enum 来创建一个枚举.跟Classes(类)和其他类型的命名方式一样,枚举也可以有Method(方法). enu ...
- Rust极简教程
目录 简介 特性 特征 用途 安装 核心组件 常用命令 基础语法 数据类型 标量类型 复合类型 示例 条件语句 循环 输出&输入 输出 输出花括号 输出非基础类型 输入 所有权 切片 结构体 ...
- 专为设计师而写的GitHub快速入门教程
专为设计师而写的GitHub快速入门教程 来源: 伯乐在线 作者:Kevin Li 原文出处: Kevin Li 在互联网行业工作的想必都多多少少听说过GitHub的大名,除了是最大的开源项目 ...
- [转]让你从零开始学会写爬虫的5个教程(Python)
让你从零开始学会写爬虫的5个教程(Python) 写爬虫总是非常吸引IT学习者,毕竟光听起来就很酷炫极客,我也知道很多人学完基础知识之后,第一个项目开发就是自己写一个爬虫玩玩. 其实懂了之后,写个 ...
- Java基础教程:枚举类型
Java基础教程:枚举类型 枚举类型 枚举是将一具有类似特性的值归纳在一起的方法.比如,我们可以将周一到周日设计为一个枚举类型.彩虹的七种颜色设计为一个枚举类型. 常量实现枚举 我们通过定义常量的方式 ...
- 写给 Linux 初学者的一封信
大家好,我是肖邦. 这篇文章是写给 Linux 初学者的,我会分享一些作为初学者应该知道的一些东西,这些内容都是本人从事 Linux 开发工作多年的心得体会,相信会对初学者有所帮助.如果你是 Linu ...
- 【译】Rust宏:教程与示例(二)
原文标题:Macros in Rust: A tutorial with examples 原文链接:https://blog.logrocket.com/macros-in-rust-a-tutor ...
- 让你从零开始学会写爬虫的5个教程(Python)
写爬虫总是非常吸引IT学习者,毕竟光听起来就很酷炫极客,我也知道很多人学完基础知识之后,第一个项目开发就是自己写一个爬虫玩玩. 其实懂了之后,写个爬虫脚本是很简单的,但是对于新手来说却并不是那么容易. ...
- JAVA写接口傻瓜(#)教程(四)
接上篇 7.sevlert 啊啊啊终于写到最重要的实现部分了.Servlet = Service + Applet,表示小服务程序.Servlet 是在服务器上运行的小程序.这个词是在 Java ap ...
随机推荐
- Linux中的find
find命令在硬盘上进行文件的查找,比起whereis与locate会比较耗时. 与时间有关的选项 在Linux当中一个文件有mtime,ctime,atime,find在搜索时可以配置这3种时间. ...
- Ubuntu 上安装 Docker
步骤 1:删除任何现有的 Docker 包 但在跳到安装部分之前,有必要删除所有以前安装的 Docker. 要 卸载以前的 Docker,请使用以下命令. sudo apt remove docker ...
- cesium教程7-官方示例翻译-模型要素选择
源代码示例: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UT ...
- 我的书《Unity3D动作游戏开发实战》出版了
首先感谢帮助和参与前期检阅的朋友们.本书是我经验积累的提炼,书中既有干货分享也有对基础内容的详解补充. 同时由于是第一次撰写书籍,许多地方仍有不足还请读者朋友们见谅. 在京东或当当等都可以购买到本书: ...
- nohup Command [ Arg … ] [ & ]
nohup 英文全称:no hang up 不挂断的意思.退出终端不挂断程序的运行.在默认的情况下,会输出一个名叫 nohup.out 的文件到当前目录下.nohup Command [ Arg - ...
- skipped: maximum number of running instances reached (1)
apscheduler定时任务报错skipped: maximum number of running instances reached (1) 原因是默认max_instances最大定时任务是1 ...
- CSS——阴影
<!DOCTYPE html> <html> <head> <style> p.one { text-shadow: 3px 5px 5px #FF00 ...
- Keil_MDK中无法打开map文件的解决办法
如果在MDK中打开map文件 我们在STM32的开发过程中,经常会查看.map文件 .map文件是MDK在编译过程中生成的一个包含镜像文件信息的重要文件,在程序编译后自动生成,比方这里我的工程下自动将 ...
- zabbix第一天 zabbix安装,添加监控项
1. zabbix 介绍 公司规模大,服务器众多,运维人员需要用到zabbix来监控整个服务器的运行状况,避免服务器故障后运维人员无法察觉. 清华zabbix源: https://mirrors.tu ...
- 「C++」深度分析C++中i++与++i的区别
大家好,我是Charzie.在C++编程中,i++和++i是两个常见的自增运算符,用于将变量的值增加1(有时与i+=1效果一样).然而,虽然它们的功能看似相似,但在实际使用中却存在显著的区别.本博客将 ...