Rust 学习 01

archive time: 2021-06-05

今天开始在博客记录自己学习Rust的笔记

对于Rust这门语言及其特点,相信大家或多或少都听说过,比如拥有 最智能的编译器良好的包管理器 什么的,还有 函数式编程,这也是其一大特色

然而我为什么要学Rust呢?纯粹是因为我喜欢他的图标,没错,就是那只大螃蟹;其次就是他的编译器检查,真的是太智能了,能够精确定位到是哪一行代码出现了问题,并且能给出一定的修改提示

我这个博客,很明显,是一个系列,我会依照The Rust Programming Language来学习

Rust 的安装什么的我就跳过了,太基础了

猜数游戏

由于本系列的博客是类似与总结一样的存在,所以这里我不会按照书上的顺序一步一步来教,而是直接讲最后的代码,并且将要点总结一下,仅此而已

所以,如果真的是一点基础也没有的话,建议还是看书来学,这样比较稳当

最终的代码

下面展示的,就是这章过后你写出的代码

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

看起来还是很长的,但每个部分的功能还是很清晰的

库 (crate) 以及 Cargo

首先是头部那一堆use

这个就是Rust中导包的语句,使用某个库,类比于Java中的import,和CCpp#include还是不太一样的1,但也可以类比

导入的库分别是

std,即 standard,也就是标准库的意思,说明前两个使用都是导入标准库中的iocmp,也就是输入输出模块 (Module) 及比较 (compare) 模块

其中,std::cmp::Ordering导入的其实是cmp这个模块中的一个Enums,一个枚举类型

有关类型的事情,后面的章节会详细讲解,这里就不仔细说了,大致就是有很多个选项的这样一个类型,可以类比C语言中的枚举类型

之后,rand,这是一个外部库,而Rng则是这个库中的一个特征 (Trait),类比其他编程语言接口

Rust的标准库里,没有能够生成 伪随机数 的库,所以这样需要用到rand这样一个库来为我们生成一个随机数,也就是我们要猜的那个数

要使用rand这个库,我们只需要在Cargo.toml里写入

[dependencies]
rand = "0.8"

就可以了,cargo在编译的时候会自动下载0,7这个版本号中最新的版本,目前最新版是0.8.3

关于cargo的使用,初学者只需要记住

cargo new <project-name>  # 创建工程
cargo build               # 编译项目
cargo run                 # 运行项目

这几个比较基础的命令就可以了

之后,fn main()一句标志着程序的入口,和其他语言是一样的

println!()这里要注意了,这个并不是个函数 (method, funtuon),而是一个宏 (Macro),作用是输出内容到屏幕上

至于宏和函数的区别,之后会提到,现在只需要记住,宏的名称后面会有一个!,而函数没有

变量声明

let secret_number一句,首先,let用来声明了一个“变量“,为什么要打引号呢?因为Rust中的变量 (Variable) 默认是 immutable的,不可变的,如果要定义一个可变变量,则需要使用let mut <var-name>的形式

同时注意到这里并没有声明变量的类型,而Rust作为一门强类型的静态语言,每个变量都会有确定的类型

这是因为Rust的编译器 足够智能,可以自动进行类型推断,而不需要手动声明类型,但是如果要声明类型也是可以的,可以采用下面这种方式

let a: i32;

let <var-name>: <type>这样的方式就是声明类型了,具体RSust中有哪些类型可以参考std库中的描写

rand::thread_rng::gen_range(1..101),这句调用了定义在rand库 (crate) 的函数thread_rng,生成了一个rand::rngs::ThreadRng类型,定义在这个类型上有一个从rang::Rng中实现的方法gen_range,也就是在一个范围内生成随机数,当然,这个随机数是整形的,参数格式是

gen_range(<lower>..<upper>)  // [lower, upper)
gen_range(<lower>..=<upper>) // [lower, upper]

也就是一个是不包括右边界的,一个是包括右边界的,所以这行代码还可以写成

let secret_number = rand::thread_rng().gen_range(1..=100); // 在[1, 100]生成随机数

之后的loop { ... }实现了一个无限循环,在花括号内的所有代码都会无限制循环,除非使用break;跳出

let mut guess = String::new();

io::stdin().read_line(&mut guess)
    .expect("Failed to read line");

这两句连在一起看,首先是定义了一个可变变量,叫guess,为他 绑定 (bind) 了一个值

为什么强调是绑定,而不是说赋值呢?这就要涉及到Rust的变量机制了,之后会提到,感兴趣的可以翻阅标准库的资料2查看

绑定值的就是String::new()的结果,也就是String类型的一个对象,String类型在Rust中可以看作就是一个可变长度的字符串数组,支持utf-8编码

io::stdin().read_line(&mut guess),这句就是从标准输入流 (stdin) 中读取一行输入,并将读取到的内容赋给了guess

为什么要用&mut guess呢?我们要对guess的值进行操作,所以应该传入一个 引用,学过C++的应该对引用这个比较清楚了,所以这里应该传入一个&guess

但是为什么是&mut呢?这是因为Rust中,引用默认也是不可变的,而我们这里就是要改变引用的值,故需要使用mut来修饰

.expect()一句是对read_line()的返回值进行的一个操作,read_line()的返回值是一个Result类型的枚举,有OkErr两种状态,也就是读取成功和读取失败,expect说明了如果读取失败返回的值,而读取成功则不会执行

shadow 以及 match

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

这一句里有两个点要注意,一个是变量之间的shadow,另一个就是match语句

这里可以看到,我们又一次地声明了guess变量,在其他静态语言里,这是不允许的,而Rust可以做到,这是因为Rust中的shadow机制

简单来说就是,如果重复声明了一个变量,那么新的变量将会把旧的变量隐藏起来,无法使用,之后出现这个变量名都会指向新的这个变量

这个机制在进行变量类型转换 (cast) 的时候是十分有用的,就比如这里,我们通过parse()方法将guess由字符串解析为整形3

parse()函数的返回值也是一个 Result 类型,在这里,我们需要对这个枚举类型进行选择,即使用match语句

match语句的基本格式是

match <var> {
    arm1 => doSome1(),
    arm2 => doSome2(),
    ...
}

前面是要 匹配模板,而=>指向的就是如果模板与我们的变量匹配成功后要进行的操作,这样的一个 模式匹配 的功能还是很强大的,之后的章节会详细讲到

这里我们对Result类型进行匹配,我们之前说过,Result类型是一个枚举类型,枚举对象有OkErr,所以这里我们使用这两个对象来进行匹配,如果解析成功的话,也就是Ok,那么,我们就将解析到的值保存到一个叫 num 的变量里,之后返回出来,如果失败,那么这次循环就直接continue跳过了,进行下次循环,再次要求用户输入,直到解析成功,我们才会进行下一步操作

placeholder

println!("You guessed: {}", guess);

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => {
        println!("You win!");
        break;
    }
}

这里介绍了Rust里简单输出变量值的方法,即使用 Placeholder {},一对花括号对应一个值,这就有点像Pythonformat string了,但差别还是有的

之后是使用了std::cmp模块的cmp()函数,对比两个值,由于要传入值,所以使用了引用的方式,但我们不需要改变值,所以我们没有加上mut

cmp()的返回值是一个Ordering类型,一个枚举类型,有LessGreaterEqual三种结果,这里就是用match依次比较,如果是Equal的话,就输出“You win1“,同时跳出循环,至此,程序结束

小结

这个例子是一个十分基础且经典的例子,可以很好的从一个大的方面来看看Rust的一些特性和基本使用方法,还提到了Cargo这个优秀的包管理器

这个例子的重点,我认为应该是match语句的使用以及cargo的用法还有变量,需要注意


本次Rust学习笔记到此就结束了,谢谢大家的阅读,希望大家能从中有所收获

1

C 语言和 C++中的#include是一个预处理命令, 所做的事情通俗地说就是把你要用的那部分代码直接复制到你那句#include的位置,而JavaRust却不是这样

2

找了一下菜鸟教程的一篇教学, 如果不想啃英文的话可以稍微参考一下

3

我们自己声明guessu32类型,即无符号 32 位整形