Rust 学习笔记(持续更新中…)

一、 编译和运行是单独的两步

  • 运行 Rust 程序之前必须先编译,命令为:rustc 源文件名 - rustc main.rs
  • 编译成功之后,会生成一个二进制文件 - 在 Windows 上还会生产一个 .pdb 文件 ,里面包含调试信息
  • Rust 是 ahead-of-time 编译的语言 - 可以先编译程序,然后把可执行文件交给别人运行(无需安装 Rust )
  • rustc 只适合简单的 Rust 程序

二、Cargo

1. 简介

  • Cargo 是 Rust 的构建系统和包管理工具 - 它可以:构建代码、下载依赖的库、构建这些库…
  • 安装 Rust 的时候会自动把 Cargo 也安装上,不需要再另外安装
  • 检查是否安装成功:cargo --version 

2.  使用 Cargo 创建项目

  • 创建项目命令:cargo new hello_cargo - 项目名称也是 hello_cargo,会创建一个新的目录 hello_cargo:Cargo.tomlsrc 目录(main.rs)、初始化了一个新的 Git 仓库.gitignore - 可以使用其它的 VCS 或不使用 VCS:cargo new 的时候使用 --vcs 这个flag)

        VCS:版本控制系统(JvJv注)

3. Cargo.toml

  •  TOML(Tom's Obvious,Minimal Language)格式,是 Cargo 的配置文件的格式

  • [ package ]:是一个区域标题,表示下方内容是用来配置包(package)的
    1. name:项目名
    2. version:项目版本
    3. authors:项目作者
    4. edition:使用的 Rust 的版本
  • [ dependencies ]:另一个区域的开始,它下面会列出项目的依赖项
  • 在 Rust 中,代码的包称作 crate

4. src / main.rs

  •  cargo 生成的 main.rssrc 目录下
  • Cargo.toml 在项目顶层下
  • 顶层目录可放置:README、许可信息、配置文件和其它与程序源码无关的文件
  • 如果创建项目时没有使用 cargo,也可以按照以下步骤把项目转化为使用 cargo 的形式:
    1. 把源代码文件移动到 src
    2. 创建 Cargo.toml 并依次填写相应的配置

5. 构建 Cargo 项目:cargo build

  • 创建可执行文件:target / debug / hello_cargotarget \ debug \ hello_cargo.exe(Windows 下)
  • 运行可执行文件:./ target / debug / hello_cargo.\ target \ debug \ hello_cargo.exe(Windows 下)
  • 第一次运行 cargo build 会在顶层目录生产 cargo.lock 文件
    1. 该文件负责追踪项目依赖的精确版本
    2. 不需要手动修改该文件

6. 构建并运行 Cargo 项目:cargo run

  • 它是:编译 + 执行
  • 如果之前编译成功过,且源码没有改变,那么就会直接运行二进制文件

7. 检查代码:cargo check

  • 检查代码,确保代码能通过编译,但不产生任何可执行文件
  • cargo check 要比 cargo build 快得多
  • 好处:编写代码的时候,可以连续、反复的使用 cargo check 检查代码,提高效率

8. 为发布构建

  • cargo build --release
    1. 编译时会进行优化
    2. 代码会运行的更快,但是编译时间更长
    3. 会在 target / release 而不是 target / debug 生成可执行文件
  • 两种配置
    1. 开发时
    2. 正式发布时

三、猜数游戏

1. 游戏目标

  • 生成一个 1 到 100 之间的随机数
  • 提示玩家输入一个猜测
  • 猜完之后,程序提示:太大了 or 太小了
  • 如果猜测正常,那么打印出一个庆祝信息,程序退出

2. 实现步骤,写代码

  1. 新建1个文件夹,打开,地址栏输入 cmd 进入命令行窗口
  2. 在命令行窗口中输入:cargo new guessing_game 并回车,名为 guessing_game 的包就创建好了。
  3. 命令行继续输入:cd gue* 进入项目文件
  4. 命令行继续输入:code . (通过VS Code打开项目)
  5. 在VS Code中,打开 main.rs 的文件,可以看到里面有几行默认代码。
  6. 在命令行中输入:cargo run ,正常输出了一句 Hello, world! ,确认了一下没有问题
  7. 下面我们就开始在此文件内写代码:

use std::io;

fn main() {
    println!("来玩猜数游戏叭!");
    println!("请输入一个数:");

    let mut guess = String::new();
    io::stdin().read_line(&mut guess).expect("读取错误!");

    println!("你猜测的数字为:{}", guess);
}

  • use std::io; - 需要获取用户输入并打印用户输出,我们需要用到 io 这个库,而 io 这个库是在 std 这个标准库中。所以,我们使用 use 这个关键字进行预导入,类似于 Python 中的 import。
  • let mut guess: String = String::new ( ); - 用 let 创建一个字符串类型的变量 guess,要使它在后面是可修改的可变字符串,我们就要在中间再加一个 mut 关键字。等号后面的代码,表示将其初始化为空字符串。
  • io::stdin().read_line(&mut guess).expect("读取错误!"); - 这里使用标准库 io::stdin() 函数从标准输入读取一行字符串,并将其存储到之前定义的可变字符串变量 guess 中。&mut guess 表示传递 guess 的可变引用,以便可以将读取的字符串写入到它的内存地址中。expect 表示如果读取失败,程序将抛出一个错误信息 “读取错误!”。

  • println!("你猜测的数字为:{}", guess); - 这里就是看下中间的花括号,表示占位符,输出的时候就会被逗号后面的变量替换。如果有2个或者多个花括号,那么就会依次从左到右替换。

运行上面的代码,我们根据程序提示输入 18 这个数字,效果如下:

程序正常运行,没有问题,但只能猜一次数,没有完全达到我们的目的,所以我们继续完善上面的代码。

3. 生成随机数

        既然是猜数游戏,肯定少不了随机数这一环。相比其它语言,在 Rust 中生成随机数还是有点麻烦的。

  • 导入一个名为 rand 的库,如下图所示,我们在 Cargo.toml 这个文件中的 [ dependencies ] 下写 rand = "0.8" ,等号前面是我们要导入的库的名称,后面双引号中则是它的版本号。你可以随便指定哪个版本号,只要这个版本的库能满足你的需要,并且网上确实有发布过。

  • 我们回到 main.rs 文件中,写入如下几行代码:

        红框中就是我们新增的代码,use rand::Rng; - 是 Rust 编程语言中的一个导入语句,它用于在代码中使用 rand crate 中的 Rng trait。Rust 是一种系统级编程语言,rand crate 是 Rust 社区中常用的生成随机数的库。

        Rng trait 提供了生成伪随机数的方法,可以用于各种随机性需求,如游戏中的随机地图生成、密码学中的密钥生成等。通过 use rand::Rng;,可以在代码中直接使用 Rng trait 中定义的方法来生成随机数。

        例如,可以使用 Rng traitgen_range 方法来生成指定范围内的随机整数:

let mut rng = rand::thread_rng();

let random_number = rng.gen_range(1, 101);

        上述代码中,我们通过 rand::thread_rng() 函数创建了一个随机数生成器 rng,并使用其 gen_range 方法生成一个介于1 - 100之间的随机整数。注意:(1,101)是包括1,不包括101。

  • 通过上面的讲解,相信你应该不难看懂中间4行代码所代表的含义了。但是你可能会觉得图片中我写的代码好像很复杂?这是因为我装了一些关于 Rust 的插件,而这4行代码中的灰色内容是编译器自动帮我生成的,所以你在自己动手写的时候,只需要参照上面举例的2行代码即可。
  • 我使用的插件截图放在下面了,具体功能我就不细说了,你们可以在网上查一下。

  • 生成随机数的代码写完了,我们来运行一下看看效果。首先我们打开编译器底部的终端,直接输入 cargo build 命令先构建一下,然后再输入 cargo run 来运行程序。

  • 有的小伙伴可能误删了编辑器下方的页面,可以单击顶部菜单栏中的 “终端” 再新建一个终端页面出来,也可以直接按 Ctrl键 + Shift键 + ~键(ESC正下方的带小波浪号的那个键)。

4. 比大小

        前面我们已经实现了用户输入输出,和生成随机数,那么接下来我们就需要将用户输入的值和 rand 生成的随机数进行比较,如果两个等于,那么就打印 “You win!”,如果比随机数大,那么就打印 “Too big!”,如果比随机数小,就打印 “Too small!” 。

        这里先贴上代码,然后我们再一句句分析。

 

红框中第1行代码解析:

  1.  let guess:u32   - 声明一个名为 guess 的变量,并指定它的类型为 u32,即无符号32位整数。

  2. guess.trim()  - 对之前声明的 guess 变量进行方法调用。

  3.  trim()   - 方法用于去除字符串首尾的空白字符,返回一个新的字符串。

  4.  parse()   - 对上一步骤返回的字符串进行解析,将其转换为目标类型(在这里是 u32 )。此处使用.parse()方法对字符串进行解析,将其转换为 u32 类型。

  5.  expect("Please enter a number!"  - 在解析过程中,如果字符串无法被正确解析为 u32 类型,会产生一个错误。.expect() 方法用于处理这个错误,并打印自定义的错误信息。在这里,如果解析失败,会输出 Please enter a number! 。

红框中第2行代码解析:

  1.  guess.cmp ( &random_number   - 这是一个方法调用,使用了 .cmp() 方法来比较 guessrandom_number 两个值。
  2. &random_number   - 通过使用&符号获取random_number的引用。这是因为.cmp()方法要求传递引用作为参数。
  3. { }  - 此处是match表达式的主体部分,包含了不同情况的模式匹配和对应的执行代码块。

红框中第3、4、5行代码解析: 

        这3行代码是在 Rust 中使用 match 语句对比较的结果进行模式匹配,并根据不同的情况执行相应的代码块。

1.  Ordering::Greater => println!("Too big!")  - 当比较结果为 Ordering::Greater 时,也就是 guess 大于 random_number ,执行这个代码块并打印 "Too big!" 。

2.  Ordering::Equal => println!("You win!")  - 当比较结果为 Ordering::Equal 时,也就是guess 等于 random_number,执行这个代码块并打印 "You win!" 。

3.  Ordering::Less => println!("Too small!")  - 当比较结果为 Ordering::Less 时,也就是guess 小于 random_number,执行这个代码块并打印 "Too small!" 。 这里使用了 Ordering 枚举类型,它是 Rust 标准库中定义的一种表示比较结果的类型。通过使用 match 语句和模式匹配来处理不同的比较结果,可以根据情况执行相应的逻辑代码块。在这个例子中,根据猜测与随机数的大小关系,打印不同的提示信息以给出反馈。

5. 允许多次猜测

        为什么要允许用户多次猜测呢?因为大概率用户一次是猜不准随机数的,而且之前我们写的代码只能实现让用户猜一次,而且这一次不管猜大了还是猜小了,程序都会直接退出,不会给用户第二次机会,当再次运行程序时,随机数刷新,又是新的一轮了。

        所以,这第5小节,我们就来实现一下允许用户多次猜测,直到猜对后程序自动退出。

        多次,那么我们就需要写一个循环了,那 Rust 中循环怎么写呢?我们直接贴出写好的代码,大家先看一下。

        对照之前写的代码,我们只是将之前写的一段判断直接扔进了 loop{} 这个关键字后面的花括号里,这样就实现了循环了,是不是很简单?

        下面我们来运行一下这段修改后的代码:

         可以看到,当我们猜了很多次之后终于猜对了这个随机数,但是这个程序仍然让我们继续猜,这是不符合逻辑的,因为我们已经知道这轮的随机数是 31 了,所以这完全没有意义。因此,我们还需要对这段代码进行优化。

        怎么优化呢?是不是只要我们猜对了,程序打印了 You win!就可以直接退出了,而不是这样一直循环下去。然后我们还有一点不能忘记了,就是当用户如果输入的不是数字,而是别的内容,比如 abc、二十一这样的值,程序就会崩溃了。

        在之前我们写的代码中,用到了 parse() 这个关键字,它的作用是对上一步骤返回的字符串进行解析,将其转换为目标类型,这里是转换为 u32 类型,也就是无符号32位整数。但字母和汉字这些是不可能被转换的,所以此时程序就会崩溃。

        根据上面的分析与思考,我优化了一下代码,如下图:

        在第1个红框处,我删掉了之前的 expect,而是在外面套了一个 match ,它是 Rust中的控制流运算符,而 parse 的返回值是枚举类型,即:OkErr 。所以,我们可以通过 match 来分开执行结果是 Ok 下的情况,和 结果是 Err 下的情况。

        在这段实际代码中,它实现了当用户输入值转换成功时,程序正常运行,当用户输入值转换不成功时,程序跳过错误,继续执行。

        需要提一下的是,Err 后面括号中的下划线,是一个通配符,表示我们不关心里面的错误信息,忽略的意思。

        在第2个红框处,我们在 Equal 后面套了1个花括号,将打印 You win 的代码放了进去,然后再加了 break 关键字,表示当用户猜对了数字,程序打印了 You win 之后就自动退出了,这个和其他语言是一样的。

        至此,我们的猜数游戏就做完了。可能有很多小伙伴看到这里有些云里雾里,但不用担心哈,我们暂时只需要大概了解一下这些概念即可,后面会逐一细讲。

四、Rust 通用的编程概念

1. 变量与可变性

1.1 声明变量使用 let 关键字

举个🌰

        这里的 i32 是在我输入 let x = 5; 之后编译器自动添加进去的,它能自动推断出你声明的变量类型,这点我认为还是非常不错的。 

1.2 默认情况下,变量是不可变的(immutable)

举个🌰

        当我们再给 x 赋值为 6 时,运行程序就会报一个 cannot assign twice to immutable variable 的错误,意思就是:不能为不可变的变量赋值两次。

        如果我们在第一次声明 x 的时候就在它前面加一个 mut ,表示它是可变的,那么程序就不会报错了。

2. 常量

2.1 常量(constant),常量在绑定值以后是不可变的,它和变量的区别如下:

  • 不可以使用 mut 关键字来使它变成可变的,要记住:常量永远是不可变的
  • 声明常量应使用 const 关键字,它的类型必须被标注
  • 常量可以在任何作用域内进行声明,包括全局作用域
  • 常量只可以绑定到常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算出的值

2.2 在程序运行期间,常量在其声明的作用域内一直有效

2.3 命名规范:Rust 里常量应使用全大写字母,名称较长时,每个单词之间应使用下划线分开,例如:MAX_POINTS

举个🌰

        可以看到,u32 的字体颜色是黄色,而不像之前我们声明变量的时候是灰色,这是因为声明常量的时候,编译器就不会再给你自动推断并补全数据类型了,你需要自己去指定,否则就会报错。

        还有一点是,我这里给 MAX_POINTS 这个常量赋的值是 10000,数字中间的下划线只是方便查看, 类似于 Excel 中的千分位字符。

        那这个下划线是必须每隔三位数才能给一个下划线吗?那倒也不是,这个你想放哪就放哪。现在我把这个下滑线换个位置,你看程序依然是正常运行的,并且正确地打印出了 10000。

3. Shadowing(隐藏)

        在 Rust 中,可以使用相同的名字声明新的变量,新的变量就会 shadow(隐藏)之前的同名变量。

举个🌰

        可以看到,这个名为 x 的变量我用 let 关键字声明了2次,但程序并没有报错,而且 x 的值也是最后声明的那个同名变量的值为 6,这个就是 Shadowing

        shadow 和把变量标记为 mut 是不一样的:如果不使用 let 关键字,那么重新给 非mut 的变量赋值会导致编译时报错。

        而使用 let 声明的同名新变量,也是不可变的。

        使用 let 声明的同名新变量,它的类型可以与之前不同。

举个🌰

        这里我们用 let 关键字声明了一个变量 spaces 的值为 4个空格,编译器自动推断为 &str 类型。然后我们又给 spaces 声明为 上一个 spaces 的长度,编译器自动推断为 usize 类型。

        程序成功运行,并正确打印出了 4。可能 shadow 不好理解,你可以将这个概念理解为覆盖,意思就是如果程序中出现同名变量,总是以最新的赋值为准,而上一个同名的变量就会被覆盖掉。

4. 数据类型 - 标量类型

        一个标量类型代表一个单个的值。

        Rust 有4个主要的标量类型,分别是:整数类型、浮点类型、布尔类型、字符类型。

        Rust 是静态编译语言,在编译时必须知道所有变量的类型。基于使用的值,编译器通常能够推断出它的具体类型。

        但如果可能得类型比较多,例如将 String 转换为整数的 parse 方法,就必须提前添加标注,否则编译就会报错。

举个🌰

         这里黄色的 u32 数据类型是我们自己写上去的,而不是编译器推断出来的,这样程序就能正常运行,并正确打印出来了 42这个结果。如果我们删掉指定的 u32,程序就会报错。

        原因是,"42" 这个值可以被转换成多种整数类型,比如 u32 i32 等等,编译器无法推断你想要转换的到底是哪一种,程序就会报错了。

5. 整数类型

        整数类型没有小数部分。

        例如 u32 就是一个无符号的整数类型,占据 32位的空间。

        无符号整数类型以 u 开头(可以把这个 u 想象成一个没有盖子的口袋,空的,就是无符号~)

        有符号的整数类型以 i 开头。

        Rust 的整数类型列表如下:

Length(长度)Signed(有符号)Unsigned(无符号)
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

        每种都分 i 和 u,以及固定的位数。

        有符号范围:-(2的n次方 - 1)~ 2的n次方 -1。

        无符号范围:0 ~ 2的n次方 -1。符号指的是负号,无符号即没有负号,只有正值部分。

isize usize 类型:

        isize usize 类型的位数由程序运行的计算机的架构所决定,如果是64位计算机,那就是64位的,……

        使用 isize usize 的主要场景是对某种集合进行索引操作。

整数字面值

        下面表格里的下划线也是为了增强可读性。

Number LiteralsExample
Decomal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

        除了 byte 类型外,所有数值字面值都允许使用类型后缀,例如:57u8

        如果你不太清楚应该使用哪种类型,可以使用 Rust 相应的默认类型。

        整数的默认类型就是 i32,总体上来说速度很快,即使在64位系统中。

整数溢出

         例如:u8 的范围是 0 ~ 255,如果你把一个 u8 变量的值设为 256,那么:

        - 调试模式下编译:Rust 会检查整数溢出,如果发生溢出,程序在运行时就会 panic

        - 发布模式下(--release)编译:Rust 不会检查可能导致 panic 的整数溢出,如果溢出发生,Rust 会执行“环绕”操作,即:256变成0,257变成1…… 但程序不会 panic

6. 浮点类型 

        Rust 有两种基础的浮点类型,也就是含有小数部分的类型。

        f32,32位,单精度。

        f64,64位,双精度。

        Rust 的浮点类型使用了 IEEE-754 标准来表述。

        f64 是默认类型,因为在现代 CPU 上 f64 f32 的速度差不多,而且精度更高。

数值操作

        加减乘除余等。

 举个🌰

7. 布尔类型

        Rust 的布尔类型也有2个值:true false

        一个字节大小。

        符号是 bool

 举个🌰

8. 字符类型

         Rust 语言中 char 类型被用来描述语言中最基础的单个字符。

        字符类型的字面值使用单引号。

        占用 4字节 大小。

        是 Unicode 标量值,可以表示比 ASCII 多得多的字符内容,比如:拼音、中日韩文、零长度空白字符、emoji表情等(- U+0000 ~ U+D7FF,- U+E000 ~ U+10FFFF

        但 Unicode 中并没有 “字符” 的概念,所以直觉上认为的字符也许与 Rust 中的概念并不相符。

举个🌰

9. 复合类型

        复合类型可以将多个值放在一个类型里。

        Rust 提供了两种基础的复合类型:元祖(Tuple)、数组。

9.1 元组(Tuple

        Tuple 可以将多个类型的多个值放在一个类型里。

        Tuple 的长度是固定的,一旦声明就不能改变。

创建 Tuple

        在小括号里,将值用逗号分开。

        Tuple 中的每个位置都对应一个类型,Tuple 中各元素的类型不必相同。

举个🌰

         Tip:在创建元祖的时候,元祖里每个值的类型不用定义,输完等号后面具体的值之后,编译器会自动推断并补全。

获取 Tuple 的元素值

        可以使用模式匹配来解构(destructure)一个 Tuple 来获取元素的值。 

 举个🌰

访问 Tuple 的元素 

        在 Tuple 变量使用 点标记法,后接元素的索引号。 

 举个🌰

9.2 数组

        数组也可以将多个值放在一个类型里。

        数组中每个元素的类型必须相同。

        数组的长度也是固定的。

声明一个数组

        在中括号里,各值用逗号分开。 

 举个🌰

数组的用处

         如果想让你的数据存放在 stack(栈)上而不是 heap (堆)上,或者想保证有固定数量的元素,这时使用数组更有好处。

        数组没有 Vector 灵活(后面细讲)。

        Vector 和数组类似,它由标准库提供。

        Vector 的长度可以改变。

        如果你不确定应该用数组还是 Vector,那么估计你应该用 Vector

JvJv注:

        Stack 即栈内存方式存储,数据先进后出(FILO),操作系统只需要分配栈顶空间。

        所在在 Stack 上存储的内容必须有已知的固定大小。

        编译时大小未知或运行时大小发生变化的数据必须存放在 heap 上。

        Heap 堆内存:

        把数据放入 heap 时,你会先请求一定数量的空间,操作系统会在 heap 找到一块足够大的空间,标记为在用,并返回一个指针,也就是这个空间的地址。这个过程叫做内存的分配,有时候叫分配。

        注意:把数据压入(push)Stack 上要比在 heap 上分配快得多,因为操作系统不需要寻找用来存储新数据的空间,那个位置永远是 stack 的顶端。

        stack heap 如何访问数据比较:

        访问 heap 中的数据要比访问 stack 中的数据慢的多,因为需要先通过指针才能找到 heap 中的数据。

        对于现代处理器来说,因为存在多级缓存的缘故,如果指令在内存中多次跳转,速度就会大幅下降。

数组的类型

        数字的类型以这种形式表示:[ 类型; 长度 ] ,中间是个分号。

另一种声明数组的方法

        如果数组的每个元素值都相同,那么可以在中括号里指定初始值,然后是一个分号,最后是数组的长度。

        例如:let a = [3; 5]; 它就相当于:let a = [3, 3, 3, 3, 3]; 

访问数组的元素

        数组是 Stack 上分配的单个块的内存。

        可以使用索引来访问数组的元素。

        如果访问的索引超过了数组的范围,那么:编译会通过,但运行会报错(runtime 时会 panic)。Rust 不允许其继续访问相应地址的内存。

        比如,如果直接写一个显式的索引值,那么编译器会直接报错。但如果把索引值写的稍微 “绕”一点,那么编译器就不会直接报错,而是在运行时才会报错。

 举个🌰

10. 结构体(struct)

        结构体与元组类似,区别在于结构体的每一部分可以是不同类型。

        结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,使得结构体比元组更灵活,需要依赖顺序来指定或访问实例中的值。

如何定义?

        定义结构体需要使用 struct 关键字并为整个结构体提供一个名字。结构体的名字需要描述它组合的数据的意义。

        接着,在花括号中定义每一部分数据的名称以及字段类型。

        注意!结构体的名字要使用驼峰命名法(CamelCase name)。

        定义结构体内部的字段类型时,因为还没有输入具体值,所以编译器不能自动推断出你想要的类型,这里你需要自己一个个输入。

 

        

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喵呜角角

如果对你有所帮助,哪怕1毛钱~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值