Rust 学习记录

Rust 语言学习记录

Rust

一门赋予每个人 构建可靠且高效软件能力的语言。

Rust 官方文档 -> The Rust Programming Language - (rust-lang.org)

Rust 安装

参见官方文档安装 Rust - Rust 程序设计语言 (rust-lang.org)

或者 菜鸟教程Rust 环境搭建 | 菜鸟教程 (runoob.com)

1
2
3
4
$ rustc -V
#rustc 1.51.0
$ cargo -V
#cargo 1.51.0

使用 Cargo 建立第一个项目

参考官方:Getting started - Rust Programming Language (rust-lang.org)

使用Cargo.toml配置项目

1
2
3
4
5
6
7
8
9
10
11
[package]
name = "one"
version = "0.1.0"
authors = ["Steven Yan"]
edition = "2018"

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

[dependencies]
# 新引入了一个ferris-says包,显示螃蟹对话框
ferris-says = "0.2"

编写主函数

1
2
3
4
5
6
7
8
9
10
11
// 使用新引入的包
use ferris_says::say;
use std::io::{stdout, BufWriter};

fn main() {
let stdout = stdout();
let message = String::from("Hello fellow Rustaceans!");
let width = message.chars().count();
let mut writer = BufWriter::new(stdout.lock());
say(message.as_bytes(), width, &mut writer).unwrap();
}

构建编译项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cargo build
# cargo 工具将从crates.io下载依赖包
$ cargo run
# Compiling one v0.1.0 (/Users/steven/_Code/Rust/one)
# Finished dev [unoptimized + debuginfo] target(s) in 0.81s
# Running `/Users/steven/_Code/Rust/one/target/debug/one`
# __________________________
#< Hello fellow Rustaceans! >
# --------------------------
# \
# \
# _~^~^~_
# \) / o o \ (/
# '_ - _'
# / '-----' \

猜字符游戏

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
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;
}
}
}
}

变量

创建变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
// 使用let关键字创建变量,该变量默认是不可变的
let num1 = 1;
// num1 is not changeable
println!("num1 = {}", num1);
// 使用mut关键字将该变量声明为可变的
let mut num2 = 2;
// num2 is changeable
println!("num2 = {}", num2);
num2 += 1;
println!("num2 = {}", num2);
// 常量用关键字const声明,一般用大写,且需要标注值的类型
const MAX_NUM: u32 = 100_000;
println!("MAX_NUM = {} is constants", MAX_NUM);
// 使用let可以对一个变量名重新赋值
let num3 = 4;
println!("num3 = {}", num3);
// This declare a NEW variable named num3
let num3: String = (num3 * 2).to_string();
println!("NEW num3 = {}", num3);
}

数据类型

整形变量

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

字面值

数字字面值 例子
Decimal (十进制) 98_222
Hex (十六进制) 0xff
Octal (八进制) 0o77
Binary (二进制) 0b1111_0000
Byte (单字节字符)(仅限于u8) b'A'

浮点变量

长度 关键字
32-bit f32
64-bit *f64

布尔型变量

布尔值 关键字
true
false

字符类型

char z = 'Z';

char 大小为四个字节(four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value)

复合类型

1
2
3
4
5
6
7
8
9
10
11
12
// 元组:元组长度固定;元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
// x = 500; y = 6.4; z = 1
// tup.0 = 500; tup.1 = 6.4; tup.2 = 1;

// 数组: Rust 中的数组是固定长度的;数组中的每个元素的类型必须相同
let arr: [i32; 5] = [1, 2, 3, 4, 5];
// arr[0] = 1; arr[1] = 2; arr[5] = thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5'
// 初始化数组:5个为0的数组
let arr = [0; 5];
// arr = [0, 0, 0, 0, 0];

函数

声明函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let input = 5408;
show_input(input);
}

// 在函数签名中,必须声明每个参数的类型。
fn show_input(input: i32) {
println!("Your input is {}", input)
}

// 使用箭头 -> 指明返回值类型
fn x_times(x: i32) -> i32 {
// 不添加;表示一个表达式作为返回值
x * x
}

语句和表达式

语句Statements)是执行一些操作但不返回值的指令。表达式Expressions)计算并产生一个值。

let x = 10; 是一个语句,5 + 6 是一个表达式。

1
2
3
4
5
6
let m = {
let n = 5;
n * n
}
// n * n 是一个表达式,返回值是25
// m = 25;

控制流

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
39
40
41
42
43
44
45
fn main() {
for_fn();
}

// 如果控制流
fn if_condition(a: bool, b: bool) -> i32 {
if a && b {
1
} else if a {
2
} else if b {
3
} else {
0
}
}

// 循环控制流
fn loop_fn(times: u32) {
let mut counter = 0;
loop {
println!("counter = {}", counter);
if counter >= times {
break;
} else {
counter += 1
}
}
}

// while循环控制流
fn while_fn(times: u32) {
let mut counter = 0;
while counter <= times {
println!("counter = {}", counter);
counter += 1;
}
}

// for循环控制流
fn for_fn() {
for ele in (0..100) {
println!("value is {}", ele);
}
}

所有权

什么是所有权?

Rust 的核心功能(之一)是 所有权ownership)。虽然该功能很容易解释,但它对语言的其他部分有着深刻的影响。

Rust 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。

所有权规则

  1. Rust 中的每一个值都有一个被称为其 所有者owner)的变量。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

变量的作用域

1
2
3
4
5
6
7
fn main() {
// s 在这里无效, 它尚未声明
{
let s = "hello"; // 从此处起,s 是有效的
}
// 此作用域已结束,s 不再有效
}

变量与数据的交互

移动

1
2
3
4
5
6
7
8
9
let x = 5;
let y = x;
// 此时 x = y = 5
// Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上。如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。
let s1 = String::from("hello");
let s2 = s1;
// 此时应该是s1, s2 -> "hello"两个数据指针指向了同一个地址
// > 当 s2 和 s1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。
// 实际上是s2 -> "hello", s1被移除掉

有哪些类型是Copy的呢?

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32)Copy 的,但 (i32, String) 就不是

克隆

1
2
3
4
let s1 = String::from("hello");
let s2 = s1.clone();
// s1 -> "hello"(1) s2 -> "hello"(2)
// "hello" 被复制了

所有权与函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

引用与借用

引用

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
fn main() {
let mut s = String::from("hello");
// 可变引用
change(&mut s);
println!("{}", s); // hello world
// 只能有一个可变引用
// cannot borrow `s` as mutable more than once at a time
let r1 = &mut s;
let r2 = &mut s;
// 我们也不能在拥有不可变引用的同时拥有可变引用。
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
// 一个引用的作用域从声明的地方开始一直持续到最后一次使用为止
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 没问题
println!("{}", r3);
}
// 错误:
fn change(some_string: &String) {
// 错误!无法修改引用的值
some_string.push_str(", world");
}
// 正确:
fn change(str: &mut String) {
str.push_str(" world");
}

我们将获取引用作为函数参数称为 借用

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。

切片

另一个没有所有权的数据类型是 slice。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。

1
2
3
4
5
6
7
// 字符串切片
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
// 数字切片
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];

结构体

struct,或者 *structure,是一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合。

不需要依赖顺序来指定或访问实例中的值。

定义结构体,需要使用 struct 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 字段field)。

结构体的定义

1
2
3
4
5
struct User {
name: String,
account: String,
id: i32,
};

实例化结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let user1 = User {
name: String::from("Van"),
account: String::from("vansama"),
id: 123456,
extra: 0
};
// 可变结构体实例
let mut user2 = User {
name: String::from("Dark"),
account: String::from("darkholme"),
id: 1234567,
extra: 1
};
// 获取值、改变值可以用点"."号
print!(user1.name);
// 从已有结构体创建新结构体实例
let user3 = User {
name: String::from("Sam"),
account: String::from("sans"),
..user1
}

通过派生 trait 增加实用功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 增加结构体打印功能
// Rust 确实 包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上 #[derive(Debug)] 注解
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 is {:#?}", rect1);
}
// 输出
rect1 is Rectangle {
width: 30,
height: 50,
}

定义方法

方法 与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,将分别在第六章和第十七章讲解),并且它们第一个参数总是 self,它代表调用该方法的结构体实例。

impl块允许有多个。

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
struct Rectangle {
width: u32,
height: u32
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width >= other.width && self.height >= other.height
}
}

fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

关联函数

impl 块的另一个有用的功能是:允许在 impl 块中定义 self 作为参数的函数。这被称为 关联函数associated functions),因为它们与结构体相关联。它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。

1
2
3
4
5
6
// 例如轻松地创建一个正方形
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}

枚举与模式匹配

枚举enumerations),也被称作 enums。枚举允许你通过列举可能的 成员variants) 来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 Option,它代表一个值要么是某个值要么什么都不是。然后会讲到在 match 表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后会介绍 if let,另一个简洁方便处理代码中枚举的结构。

创建枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 举例:IP 地址的类型
enum IpAddrType {
v4,
v6,
}
// 创建实例
let ipv4 = IpAddrType:v4;
let ipv6 = IpAddrType:v6;

// 将 IP 数据放在结构体中
struct IpAddr {
type: IpAddrType,
addr: String,
}

更简洁的方式:将数据直接放进枚举成员中。

1
2
3
4
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

Option 枚举

Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>,而且它定义于标准库中

1
2
3
4
5
6
enum Option<T> {
Some(T),
None,
}
// Option<T> 枚举是如此有用以至于它甚至被包含在了 prelude 之中,不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 Option:: 前缀来直接使用 Some 和 None。即便如此 Option<T> 也仍是常规的枚举,Some(T) 和 None 仍是 Option<T> 的成员。
let some_number = Some(123);

为什么 Option<T> 比空值安全?

当在 Rust 中拥有一个像 i8 这样类型的值时,编译器确保它总是有一个有效的值。只有当使用 Option<i8>(或者任何用到的类型)的时候需要担心可能没有值,而编译器会确保在Option<i8>使用值之前处理了为空的情况。

控制流运算符

Rust 有一个叫做 match 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
Buck,
}

fn value_in_cents(coin: Coin) -> u8 {
// match 分支分为两部分:目标匹配模式和代码;分支之间使用 , 分隔
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
// 如果分支代码较短的话通常不使用大括号,如果想要在分支中运行多行代码,可以使用大括号。
Coin::Buck => {
println!("a buck equals 100 coins.");
100
}
}
}

Rust 中的匹配是 穷尽的exhaustive):必须穷举到最后的可能性来使代码有效。

更简洁的控制流

使用if let实现单个match选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let choice = 1;
match choice {
1 => println!("This is 1"),
_ => println!("{}", choice)
}
// 等同于
if let 1 = choice {
println!("This is 1.");
} else {
println!("{}.", choice);
}
}
打赏
  • 版权声明: 本博客采用 Apache License 2.0 许可协议。
    转载请注明出处: https://ryzenx.com/2021/05/Rust-learning/

请我喝杯咖啡吧~

支付宝
微信