rust学习记录

记录一下Rust的学习历程

第一章:安装和hello world

安装

1
$ curl https://sh.rustup.rs -sSf | sh

这条命令会下载并执行一个脚本来安装rustup工具,进而安装最新的Rust稳定版本。该脚本可能会在执行过程中请求输入你的密码。一旦安装成功,你将能够看到如下所示的输出

1
Rust is installed now. Great!

上面的安装过程会自动将Rust工具链添加到环境变量PATH中,并在下一次登录终端时生效。假如你想要立即开始使用Rust而不用重新启动终端,那么你可以在终端中运行如下所示的命令来让配置立即生效:

1
$ source $HOME/.cargo/env

或者,你也可以向~/.bash_profile 文件中添加下面的语句,手动将Rust添加到环境变量PATH中:

1
$ export PATH="$HOME/.cargo/bin:$PATH

Hello , world

创建一个文件夹

先,我们需要创建一个文件夹来存储编写的Rust代码。通常而言,Rust不会限制我们存储代码的位置,但是针对本书中的各种练习和项目,我们建议你创建一个可以集合所有项目的根文件夹,然后将本书中所有的项目放在里面。

现在,你可以打开终端并输入相应命令,来创建我们的文件夹及第一个“Hello, world!”项目了。

对于Linux系统、macOS系统,以及Windows系统的PowerShell终端来说,输入的命令如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ mkdir ~/projects


$ cd ~/projects


$ mkdir hello_world
Hello, World!
现在,你应该已经成功安装好了Rust。让我们遵从传统,从编写一个可以打印出“Hello, world!”的小程序开始正式的学习旅程。

注意
本书假定你已经熟悉了基本的终端操作与常用命令。开发Rust程序并不会对你所使用的编辑工具有任何的要求,如果你喜欢使用某个IDE(Integrated Development Environment,集成开发环境),那么就用你喜欢的IDE好了。许多常用的IDE都已经针对Rust实现了某种程度上的支持,你可以通过相应的IDE文档来了解更多的细节。值得高兴的是,Rust开发团队在集中精力提供流畅、舒适的IDE支持,不断优化编码体验!

创建一个文件夹
首先,我们需要创建一个文件夹来存储编写的Rust代码。通常而言,Rust不会限制我们存储代码的位置,但是针对本书中的各种练习和项目,我们建议你创建一个可以集合所有项目的根文件夹,然后将本书中所有的项目放在里面。

现在,你可以打开终端并输入相应命令,来创建我们的文件夹及第一个“Hello, world!”项目了。

对于Linux系统、macOS系统,以及Windows系统的PowerShell终端来说,输入的命令如下所示:

$ mkdir ~/projects


$ cd ~/projects


$ mkdir hello_world


$ cd hello_world

对于Windows系统的CMD终端,输入的命令如下所示:


> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world

现在,你可以打开刚刚创建的main.rs 文件,并键入示例1-1中的代码。

main.rs

1
2
3
4
5
6
fn main() {
println!("Hello, world!");
}
// 这部分代码定义了Rust中的一个函数。这里的main函数会比较特殊:当你运行一个可执行Rust程序的时候,所有的代码都会从这个入口函数开始运行。这段代码的第一行声明了一个名为main的、没有任何参数和返回值的函数。如果某天你需要给函数声明参数的话,那么就必须把它们放置在圆括号()中。
// 首先,标准Rust风格使用4个空格而不是Tab来实现缩进
// 最后,我们使用了一个分号(; )作为这一行的结尾,它表明当前的表达式已经结束,而下一个表达式将要开始。大部分的Rust代码行都会以分号来结尾。

编译与运行是两个不同的步骤

你应该已经运行过刚刚编写的程序了,让我们来详细地讨论一下这个过程中的每一个步骤。

在运行一段Rust程序之前,你必须输入rustc命令及附带的源文件名参数来编译它:

1
$ rustc main.rs

编译完之后我们ls看一下编译出来了什么东西

1
2
3
main.exe # Windows系统下的main.exe
main.pdb # 如果你使用的是Windows系统,那么你还会看到一个附带调试信息、以.pdb 为后缀的文件
main.rs # 以.rs 为后缀的源代码文件

假如你更加熟悉某种类似于Ruby、Python或JavaScript之类的动态语言,你可能还不太习惯在运行之前需要先进行编译。Rust是一种预编译语言,这意味着当你编译完Rust程序之后,便可以将可执行文件交付于其他人,并运行在没有安装Rust的环境中。而如果你交付给其他人的是一份.rb 、.py 或.js 文件,那么他们就必须要拥有对应的Ruby、Python或JavaScript实现来执行程序。当然,这些语言只需要用简单的一句命令就可以完成程序的编译和运行。这也算是语言设计上的权衡与取舍吧

hello cargo

是啥:

是Rust的包管理器,它可以处理众多诸如构建代码、下载编译依赖库等琐碎但重要的任务

1
2
$ cargo --version
cargo 1.77.2 (e52e36006 2024-03-26)

使用Cargo创建一个项目

1
$ cargo new hello_cargo

现在,让我们进入hello_cargo 文件夹,你可以看到Cargo刚刚生成的两个文件与一个目录:一个名为Cargo.toml 的文件,以及一个名为main.rs 的源代码文件,该源代码文件被放置在src 目录下。与此同时,Cargo还会初始化一个新的Git仓库并生成默认的.gitignore 文件。

Cargo.toml 中的内容如示例1-2所示,你可以使用文本编辑器打开它。

toml就是配置文件格式

Cargo.toml

1
2
3
4
5
6
7
8
[package] # 首行文本中的[package]是一个区域标签,它表明接下来的语句会被用于配置当前的程序包。随着我们在这个文件中增加更多的信息,你还会见识到更多其他的区域 (section)
name = "hello_cargo" # 项目名称
version = "0.1.0" # 版本
edition = "2021" # 日期

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

同样按照惯例,Cargo会默认把所有的源代码文件保存到src 目录下,而项目根目录只被用来存放诸如README文档、许可声明、配置文件等与源代码无关的文件。使用Cargo可以帮助你合理并一致地组织自己的项目文件,从而使一切井井有条。

使用Cargo构建和运行项目

那么使用Cargo来构建和运行项目与手动使用rustc相比又有哪些异同呢?在当前的hello_cargo 项目目录下,Cargo可以通过下面的命令来完成构建任务:

1
2
3
$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs

与之前不同,这个命令会将可执行程序生成在路径target/debug/hello_ cargo (或者Windows系统下的target\debug\hello_cargo.exe )下。你可以通过如下所示的命令运行这个可执行程序试试看

1
2
3
$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
# 如果是linux就直接运行这个二进制,如果是win就运行exe
Hello, world!

一切正常的话,Hello, world! 应该能够被打印到终端上。首次使用命令cargo build构建的时候,它还会在项目根目录下创建一个名为Cargo.lock 的新文件,这个文件记录了当前项目所有依赖库的具体版本号。由于当前的项目不存在任何依赖,所以这个文件中还没有太多东西。你最好不要手动编辑其中的内容,Cargo可以帮助你自动维护它。

当然,也可以简单的使用cargo run 命令来一次完成编译和运行任务

1
2
3
4
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/hello_cargo`
Hello, world!

这里没有看到编译hello_cargo的相关信息,因为cargo发现源码并没有被修改,所以就直接运行了所生成的二进制,我们现在修改一下源代码再试试run

1
2
3
4
5
$ cargo run
Compiling hello_cargo v0.1.0 (/home/wang/code/rustStudy/hello/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.13s # 这里就是在重新编译
Running `target/debug/hello_cargo`
Hello, world!, test cargo run

还有一个命令,cargo check ,这个命令可以快速检查当前的代码是否可以通过编译,而不需要花费EAI的时间去真正生成可执行程序

1
2
$ cargo check
Finished dev [unoptimized + debuginfo] target(s) in 0.00s

总结:

cargo new project_name 创建一个Cargo项目目录

1
2
3
4
5
src --放项目代码的 source 的缩写
target --目标,也就是放构建完的东西的目录
Cargo.lock 这个文件记录了当前项目所有依赖库的具体版本号
Cargo.toml 存放Cargo的配置信息,
.gitignore git配置信息,这个里面目前的内容是/target,表示target这个目录不会上传到git上去

通过cargo build 和cargo check 来构建一个项目

通过cargo run 来构建并且运行这个项目,

构建所产生的结果会被Cargo 存储在target/debug目录下面,而非代码所处在的位置,

Cargo另一个优势是不区分操作系统(Linux\MacOS\Windows),在任何操作系统下都是一样的命令

以Release模式进行构建

当准备好发布自己的项目时,你可以使用命令cargo build –release在优化模式下构建并生成可执行程序。它生成的可执行文件会被放置在target/release 目录下,而不是之前的target/debug 目录下。这种模式会以更长的编译时间为代价来优化代码,从而使代码拥有更好的运行时性能。

第二章 编写一个猜数游戏

第三章 通用编程概念

src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::io;

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

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

let mut guess = String::new();

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

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

变量与可变性:

Rust中的变量默认是不可变的。Rust语言提供这一概念是为了能够让你安全且方便地写出复杂、甚至是并行的代码。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
println!("Hello, world!, test cargo run");
let x = 5; // 如果要对变量进行二次赋值,就要使用let mut x = 5;
println!("x is {}",x);
x = 6;
println!("x is {}",x);
}
// 这里运行会报错,看报错提示
/*
$ cargo run
Compiling hello_cargo v0.1.0 (/home/wang/code/rustStudy/hello/hello_cargo)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:5:5
|
3 | let x = 5;
| -
| |
| first assignment to `x` 第一个任务x
| help: consider making this binding mutable: `mut x` x是一个不变的类型
4 | println!("x is {}",x);
5 | x = 6;
| ^^^^^ cannot assign twice to immutable variable 不可以对不变类型变量进行二次赋值

For more information about this error, try `rustc --explain E0384`.
error: could not compile `hello_cargo` (bin "hello_cargo") due to 1 previous error
*/

如果需要定义可变变量,就必须let mut x = 5;

除了避免出现bug,设计一个变量的可变性还需要考量许多因素。例如当你在使用某些重型数据结构时,适当地使用可变性去修改一个实例,可能比赋值和重新返回一个新分配的实例要更有效率;而当数据结构较为轻量的时候,采用更偏向函数式的风格,通过创建新变量来进行赋值,可能会使代码更加易于理解。在类似这样的情形下,为了可读性而损失少许的性能也许是值得的。

常量

与不可变变量类似,常量(constant)是绑定到一个常量名且不允许更改的值,但是常量和变量之间是有差异的

1
2
3
4
#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

遮蔽

你可以声明和前面变量具有相同名称的新变量。这个是第一个变量被第二个变量遮蔽shadow

遮蔽和将变量标记为 mut 的方式不同,因为除非我们再次使用 let 关键字,否则若是我们不小心尝试重新赋值给这个变量,我们将得到一个编译错误。通过使用 let,我们可以对一个值进行一些转换,但在这些转换完成后,变量将是不可变的。

mut 和遮蔽之间的另一个区别是,因为我们在再次使用 let 关键字时有效地创建了一个新的变量,所以我们可以改变值的类型,但重复使用相同的名称。例如,假设我们程序要求用户输入空格字符来显示他们想要的空格数目,但我们实际上想要将该输入存储为一个数字:

变量

Rust是一种静态类型(statically typed)的语言,意味着它必须在编译期间就要知道所有变量的类型,、

整型

表 3-1: Rust 中的整型

长度 有符号类型 无符号类型
8 位 i8 u8
16 位 i16 u16
32 位 i32 u32
64 位 i64 u64
128 位 i128 u128
arch isize usize

有符号无符号表示数字能否取负数

每个有符号类型规定的数字范围是 -(2^(n - 1) ~ 2^(n - 1) - 1,其中 n 是该定义形式的位长度。所以 i8 可存储数字范围是 -(2^7) ~ 2^7 - 1,即 -128 ~ 127。

无符号类型可以存储的数字范围是 0 ~( 2^n )- 1,所以 u8 能够存储的数字为 0 ~ 2^(8 - 1),即 0 ~ 255。

此外,isizeusize 类型取决于程序运行的计算机体系结构,在表中表示为“arch”:若使用 64 位架构系统则为 64 位,若使用 32 位架构系统则为 32 位。

注意,可能属于多种数字类型的数字字面量允许使用类型后缀来指定类型,例如 57u8。数字字面量还可以使用 _ 作为可视分隔符以方便读数,如 1_000,此值和 1000 相同。

表 3-2: Rust 的整型字面量

数字字面量 示例
十进制 98_222
十六进制 0xff
八进制 0o77
二进制 0b1111_0000
字节 (仅限于 u8) b'A'

那么该使用哪种类型的整型呢?如果不确定,Rust 的默认形式通常是个不错的选择,整型默认是 i32isizeusize 的主要应用场景是用作某些集合的索引。

i32 就是 -2^31~2^31-1

整型溢出

比方说有一个u8 ,存放区间是什么?是0(2^8)-1=0255

如果当你将其修改为范围之外的值,比如说256,则会发生整型溢出(integer overflow),这会导致两种行为的其中一种:

1、当在调试(debug)模式编译时,Rust 会检查整型溢出,若存在这些问题则使程序在编译时 panic。Rust 使用 panic 这个术语来表明程序因错误而退出。第 9 章 [“panic! 与不可恢复的错误”](file:///opt/Koodo Reader/resources/app.asar/build/ch09-01-unrecoverable-errors-with-panic.xhtml)会详细介绍 panic。

2、在当使用 --release 参数进行发布(release)模式构建时,Rust 检测会导致 panic 的整型溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码包裹(two’s complement wrapping)的操作。简而言之,大于该类型最大值的数值会被“包裹”成该类型能够支持的对应数字的最小值。比如在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。

浮点型

浮点数是带有小数点的数字,Rust中有两种:

f32类型是单精度浮点型

f64为双精度浮点型。

1
2
3
4
5
fn main() {
let x = 2.0; // f64

let y: f32 = 3.0; // f32
}

数字运算

Rust 的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取模运算。整数除法会向下取整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

fn main() {
// addition
let sum = 5 + 10;

// subtraction
let difference = 95.5 - 4.3;

// multiplication
let product = 4 * 30;

// division
let quotient = 56.7 / 32.2;
let floored = 2 / 3; // Results in 0

// remainder
let remainder = 43 % 5;
}

布尔类型

和大多数编程语言一样,Rust 中的布尔类型也有两个可能的值:truefalse。布尔值的大小为 1 个字节。Rust 中的布尔类型使用 bool 声明。

1
2
3
4
5
6

fn main() {
let t = true;

let f: bool = false; // with explicit type annotation
}

字符类型

Rust 的 char(字符)类型是该语言最基本的字母类型,

1
2
3
4
5
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
}

注意,我们声明的 char 字面量采用单引号括起来,这与字符串字面量不同,字符串字面量是用双引号括起来。

Rust 的字符类型大小为 4 个字节,表示的是一个 Unicode 标量值,这意味着它可以表示的远远不止是 ASCII。

标音字母,中文/日文/韩文的文字,emoji,还有零宽空格(zero width space)在 Rust 中都是合法的字符类型。

Unicode 值的范围为 U+0000 ~ U+D7FFU+E000~`U+10FFFF`。

不过“字符”并不是 Unicode 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。

复合类型

复合类型compound type)可以将多个值组合成一个类型。Rust 有两种基本的复合类型:元组(tuple)和数组(array)

元组类型

元组是将多种类型的多个值组合到一个复合类型中的一种基本方式。元组的长度是固定的:声明后,它们就无法增长或缩小。

我们通过在小括号内写入以逗号分隔的值列表来创建一个元组。

元组中的每个位置都有一个类型,并且元组中不同值的类型不要求是相同的。

1
2
3
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);//变量 `tup` 绑定到整个元组,
}

因为元组被认作是单个复合元素。 想从元组中获取个别值,我们可以使用模式匹配来解构(destructure)元组的一个值,如下所示:

1
2
3
4
5
6
7
8
fn main() {
let tup = (500, 6.4, 1); //变量 `tup` 绑定到整个元组,

let (x, y, z) = tup; // 用对应的3个变量去接收它,

println!("The value of y is: {}", y);
// 这里注意报错提醒你x和z是未使用的变量,前面加_就可以
}

将这个元组分解成三个单独的变量x、y、z,这个过程称为解构

除了通过模式匹配进行结构外,还可以通过使用一个句点(.)连上要访问的值的索引来直接访问元组元素(类似Python下标索引取值),代码示例:

1
2
3
4
5
6
7
8
9
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0; // 下标索引为 0 的值

let six_point_four = x.1;

let one = x.2;
}

没有任何值的元组()是一种特俗的类型,只有一个值,也可以写成()。该类型被称为单元类型(unit type),该值也被称为单元值(unit value)。如果表达式不返回任何其他值,就隐式地返回单元值。

数组类型

数组:将多个值组合在一起的另一种方式就是数组(array)。与元组不同,数组内的每个元素必须是同样类型的元素,

长度:Rust语言的数组是固定长度的。

表达形式:方括号内逗号分隔

1
let a  = [1,2,3,4,5];

特点:

  • 当你希望将数据分配到栈(stack)而不是堆(heap)时,或者希望确保始终具有固定数量的元素时,数组就特别有用。

  • 但它不像vector(中文翻译为“向量”,Rust中的意义为“动态数据、可变数组”)类型那么灵活。

  • vector 类型类似于标准库中提供的集合类型,其大小允许增长或缩小。

  • 如果不确定是使用数组还是 vector,那就应该使用一个 vector。

  • 不过当你明确元素数量不需要改变时,数组会更有用。例如,如果你在程序中使用月份的名称,你很可能希望使用的是数组而不是 vector,因为你知道它始终包含 12 个元素:

1
2
3
4
5
#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
}

使用方括号编写数组的类型,其中包含每个元素的类型、分号,然后是数组中的元素数,如下所示:

1
2
3
4
#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5]; // i32整型,5个元素
}

以这种方式编写数组的类型看起来类似于初始化数组的另一种语法:如果要为每个元素创建包含相同值的数组,可以指定初始值,后跟分号,然后在方括号中指定数组的长度,

1
2
3
fn main() {
let a = [3; 5]; // 5个3 等于 [3,3,3,3,3]
}

访问数组内元素

数组是可以在栈上分配的已知固定大小的单个内存块。可以使用索引访问数组的元素,

1
2
3
4
5
6
fn main() {
let a = [1, 2, 3, 4, 5];

let first = a[0]; // 获取第一个值
let second = a[1];
}

函数


rust学习记录
http://example.com/2024/05/11/rust学习记录/
作者
Wangxiaowang
发布于
2024年5月11日
许可协议