rust

安装

linux

执行命令:

1
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

windows

下载安装包rustup-init.exe

rustup-init.exe

执行安装时一路回车就行

检查安装

执行命令:

1
2
3
4
5
6
# rust编译器
rustc --version
# 项目和包管理器
cargo --version
# rust文档生成工具
rustdoc --version

编译单个源文件:

1
rustc 1.rs

新建一个项目:

1
cargo new 项目名称

编译项目:

1
cargo build

编译并运行:

1
cargo run

检查代码是否可以通过编译(但没有编译结果), 运行速度比cargo build快得多

1
cargo check

查看rust文档

1
rustup doc

基础

变量和可变性

变量

rust使用let来声明一个变量, 默认情况下是不可变

1
2
3
let x = 5;
// 下面的代码会报错因为x不可变
x = 6;

在使用let的同时使用mut(mutable, 可变的)来标记变量可变

1
2
3
let mut x = 5;
// 下面的代码不会报错因为x是可变的
x = 6;

一般情况下rust会自动根据上下文推断变量类型, 如上面的x就是i32类型

指定变量类型时需要这样写:

1
let x : i32 = 5;

常量

rust使用const来声明一个常量, 在任何情况下常量都不可变, 常量的命名全大写

1
const PIE: f64 = 3.14;

声明常量时必须同时指定常量的类型和值, 因为在此之后常量的类型和值永不可变, 并在其作用域中永远可用

遮蔽

在使用let声明一个不可变变量后, 再次使用let声明一个同名的新变量, 原来的变量类型和数据将会被新的变量的类型和数据覆盖

1
2
3
let str = "hello";
// 使用一个新的变量类型和数据进行遮蔽
let str : i32 = str.len();

遮蔽实现了变量的重新使用, 在上面的例子中, 不需要为两个变量分别命名为strstr_len而是直接将原来的变量给遮蔽

数据类型

rust是静态类型语言, 即在编译期间, rust必须要知道所有变量的确切数据类型, 当数据的类型可能是多种情况时就必须指定变量的类型

1
2
let str1 = "hello world";
let str1: u32 = str1.trim().parse().expect("无法完成类型转换!");

在上面的代码中, 定义了一个str1变量, 没有特定指定类型, 因为rust会自动根据上下文将其解析成 &str类型

第二行使用&str.trim()方法将字符串中的空白字符去除, 返回的仍是&str

接着使用了parse()将数据进行解析, 然而parse()并不知道需要解析成什么类型的数据, 因此在第二行遮蔽str1时指定了数据类型为u32, 则parse()将会将其解析成u32, 否则报错

parse()的返回类型为Result, 是rust中的一种重要数据类型枚举, 其有两个变体

  • Ok(data): 当数据解析成功后会返回解析后的数据
  • Err: 当数据解析失败后会返回Err这个变体, 其内置了一些方法, 上面用到的expect()就是在发生错误时退出程序并将其括号内的内容打印出来

rust中的标量类型就是单个值的类型, 四个基本类型为整型, 浮点型, 布尔型, 字符

整型

rust中的整形型只有长度和有无符号之分:

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

isizeusize的长度取决于arch也就是当前使用的计算机的架构

64位系统则isize就是i64, 32位系统isize就是i32, usize同理

在任意整型字面量中可以使用_来进行分隔, 提高数据的可读性如:

数字字面量 示例
十进制 100_000
十六进制 0x50_4b
八进制 0o17_60
二进制 0b1010_1100
字节(即u8) b’A’

可能属于多种类型的数字字面量可以使用后缀来指定数据类型, 如57u82222u16

当然一般情况下可以不指定整型的具体类型, rust会自动把整型解析成i32

整型溢出

当一个值的大小超过这个某个变量的类型所能承载的最大值时会发生整型溢出

调试时rust会指出错误并发生恐慌, 而在发布时rust并不会发生恐慌而是进行环绕

如变量byte1的类型为u8, 可以承载的数据大小是0-255, 如果给btye1赋值为256时数据会进行环绕, 相当于对256取模

最终256会变成0, 257会变成1

浮点型

rust中的浮点型有两种, 单精度f32和双精度f64, rust的默认浮点类型为f64

所有的浮点型都有符号

数字运算

rust的数字类型与其他语言一样支持加减乘除和取模操作

1
2
3
4
5
6
let sum = 5 + 10; // 加法
let dif = 10h - 5; // 减法
let pro = 5 * 10; // 乘法
let div1 = 11 / 5; // 除法, 结果向下取整
let div2 = 10.1 / 5.2; // 除法同样需要保证数据类型相同
let rem = 10 % 5; // 取模

布尔类型

rust中的布尔类型占用一个字节长度, 分为truefalse, 声明时使用bool

[!note]

需要注意的是, 不同于python, php等语言, rust永远不会尝试将非布尔类型的数据转换为布尔类型

下面的代码在python中成立:

1
2
3
a = 5
if a:
print("if判断为真")

但在rust中同样功能的代码则会直接报错:

1
2
3
4
let a = 5;
if a {
println!("if判断为真")
}

因为if期望的是一个严格的bool类型, 并且不会像python一样自动把非空的值转换为true

字符类型

rust的字符类型使用char声明, 支持所有的unicode字符, 使用单引号包裹, 占用四个字节长度

1
2
3
let c = 'C';
let z = '∀';
let cat = '🐱';

元组类型

rust使用元组类型将多种类型的多个值组合在一起, 长度在声明时就固定, 之后不可改变

1
let tup: (u64, u128, u16) = (3, 34, 34);

可以通过模式匹配来对元组中的数据进行解构:

1
2
3
4
let tup: (u64, u128, u16) = (3, 34, 34);
let (x, y, z) = tup; // 创建一个匿名元组并获取tup中的数据
println!("z的值是{}", z);

也可以使用.接元素索引来直接访问元组的元素:

1
2
let tup: (u64, u128, u16) = (3, 34, 34);
printlin!("{}", tup.0)

有一类元组类型很特殊, 即空元组(), 被称为单元类型, 如果一个表达式或函数不返回任何值, 则隐式的返回单元值

数组类型

数组类型跟元组类似, 长度同样不可变, 但其中的元素类型必须相同

1
let a = [1, 3, 5, 7, 0];

需要指定类型时需要同时指定数组内元素的类型和个数:

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

如果需要创建一个包含同一个元素的数组可以这样创建:

1
2
3
let a : [3; 5];
// 等价于
let a = [3, 3, 3, 3, 3];

与元组不同, 数组访问元素是使用[]

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

println!("{}", a[0]);

数组越界

在其他语言中常常会发生数组越界的情况, 即访问的索引大于数组的最大索引

1
2
3
let a = [3, 3, 3, 3, 3];
// 索引5超出了数组的范围
println!("{}", a[5])

这段代码在编译时就会被rust发现并报错

如果代码中索引值在运行时才能确定, 则编译会通过, 但运行时仍会被rust发现, 终止程序并报错

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

fn main() {
let mut str = String::new();

io::stdin().read_line(&mut str).expect("读取错误!");

let index: usize = str.trim().parse().expect("类型转换失败!");

let a = [3, 3, 3, 3, 3];
// 编译时无法确定index的值
let content = a[index];

println!("{}", content);
}

这里代码中的索引值需要等待运行时的用户输入, 因此编译可以通过

函数

rust使用fn定义一个函数:

1
2
3
4
5
6
7
8
9
10
fn main() {
println!("这是主函数!");
// 函数调用
another_function();
}

// 函数定义
fn another_function() {
println!("这是另一个被调用的函数!");
}

rust中的函数定义可以在main函数之后, 只要定义的函数在作用域中, 就始终可用

参数

rust函数同样可以被定义为有参数, 向函数传入参数时同样需要指定参数的数据类型:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
println!("这是主函数!");
// 函数调用
another_function(50);
}

// 函数定义
fn another_function(x : i32) {
println!("这是另一个被调用的函数!");
println!("传入的参数x的值是: {}", x);
}

这里在定义时使用的临时占位参数x被称为形参, 即形式上的参数, 而在调用时传入的参数50被称为实参, 即实际传入的参数

同样的, 函数可以有多个参数, 每个参数之间使用,隔开:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
println!("这是主函数!");
// 函数调用
another_function("tom", 19);
}

// 函数定义
fn another_function(name : &str, age : i32) {
println!("这是另一个被调用的函数!");
println!("姓名: {}, 年龄: {}", name, age);
}

语句和表达式

rust函数由一系列语句和表达式组成, 语句和表达式的区别如下:

  • 语句是执行一些操作而不返回值的指令
  • 表达式会计算并产生一个值
1
2
3
4
5
fn just_function() {
// let a = 6;是一个语句, 它执行赋值操作而不返回值
// 等号右边的6是表达式, 它计算的结果是6
let a = 6;
}

表达式与语句不同, 在表达式结束时, 结尾没有;, 如果在表达式末尾加上;则会将其转化为语句

带有返回值的函数

函数可以向调用此函数的代码返回值

返回值是匿名的, 不需要对其进行命名, 但需要在函数定义时预先声明它的数据类型:

1
2
3
4
5
6
7
8
9
10
// 在定义函数时声明其返回值的数据类型
fn retrun_ten() -> i32 {
10
}

fn main() {
let x = five();

println!("x的值是: {}", x);
}

在上面的代码中, 10是函数return_ten的唯一表达式, 也是最后的表达式

它的计算结果也就是10, 将会被作为返回值返回给调用它的代码也就是five()

再交由let语句, 将其值绑定到变量x, 最后打印出来

函数体的最后一个表达式的计算结果会默认作为函数的返回值返回(如果函数有返回值的话)

上面的代码中:

1
2
3
fn return_ten() -> i32 {
10
}

等价于:

1
2
3
fn retrun_ten() -> i32 {
return 10;
}

但一般情况下会省略return, 如果想要提前返回, 写return也是可以的

注释

rust中使用//及其变体来代表注释

单行注释:

1
2
3
fn main() {
// 这是一个单行注释
}

多行注释:

1
2
3
4
5
6
fn main() {
// 这是
// 一个
// 多行
// 注释
}

跨行注释:

1
2
3
4
5
6
fn main() {
/* 这是
一个
跨行
注释 */
}

控制流

控制语句与其他语言类似, 同样的, rust中也有条件语句, 循环控制语句等, 只不过实现方式稍有差别

if表达式

rust中if的使用方法与其他语言类似:

1
2
3
4
5
6
7
let a = 6;

if a > 4 {
println!("小于4");
} else {
println!("不小于4");
}

同样的, rust中也有else if:

1
2
3
4
5
6
7
8
9
let a = 6;

if a > 4 {
println!("大于4");
} else if a > 5 {
println!("大于 5");
} else {
println!("不大于4");
}

布尔类型提到过, if期待的是严格的bool类型而不能python等语言中的if a来用一个非空值代表布尔值true

不光在if中, 整个rust中, 任何需要用到bool类型的地方, 其输入也一定是严格的bool类型

在let中使用if

由于if是一个表达式, 所以可以使用letif的计算结果赋值给变量:

1
2
3
let a = true;

let number = if a { 5 } else { 6 };

当a为true时, if表达式的计算结果为5, 最后number被赋予5, 另外的情况则是6

需要注意的是, 在let中使用if表达式时, 不同分支最后的返回值类型也一定需要相同

下面的代码是不可以的:

1
2
3
let a = true;

let result = if a { 5 } else { "six" };

因为rust在编译时需要知道result的确切数据类型, 而这里有两种可能: 整型字符串切片

循环

rust提供了三种方法来实现代码的循环执行, loop, whilefor

loop循环

rust中的loop相当一个死循环, loop中的语句会无限次执行直到触发某个条件明确要求停止

1
2
3
4
5
fn main() {
loop {
println!("正在执行loop循环!");
}
}

上面的代码中没有指明何时停止循环, 所以只有在运行时手动输入ctrl+c, 代码才会停止

和其他语言一样, rust使用break来跳出上一级循环, continue来跳出当次循环

1
2
3
4
5
6
7
8
let conut = 1;

loop {
let conut = count + 1;
if count == 10 {
break;
}
}

这里指明了跳出循环的条件, 即count == 10, 当count每次加1, 最终等于10时循环被打破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::io;

fn main() {
loop {
// 创建新String实例
let mut input = String::new();
// 读取用户输入并写入input
io::stdin().read_line(&mut input).expect("无法读取输入!");
// 去除空白符并将String类型转换为&str
let input = input.trim();
// 判断输入
if input == "quit" {
break;
} else {
println!("输入quit后退出!");
continue;
}
}
}

这段代码读取了用户输入并使用if来判断用户输入是否为quit, 当输入quit跳出循环并退出程序

输入其他内容则跳过当次循环, 继续进行下一次循环

循环标签

在正常情况下, breakcontinue跳出或跳过的循环都是最近的循环

rust提供了循环标签来实现内层循环的breakcontinue跳出外层特定循环

循环标签的写法如下:

1
2
3
4
5
6
7
'loop1: loop {
println!("这是第一个循环");
'loop2: loop {
println!("这是第二个循环");
break 'loop1;
}
}

在循环前加上'标签名:就能为循环打上标签, 在内层循环时只要使用break '标签名;就能打破指定标签的循环

上面的代码在loop2中使用了break 'loop1;跳出了loop1循环

continue同理

从循环返回

loop循环同样可以用于返回值:

1
2
3
4
5
6
7
8
9
10
let mut counter2 = 0;

let result = loop {
counter2 += 1;
if counter2 == 20 {
break counter2 + 10;
}
};

println!("result的值是: {}", result);

在停止循环的break表达式后添加想要返回的值, 这个值将会作为循环的返回值返回

while循环

rust提供while循环以便于在每次循环之前执行一些检查以确定是否要继续循环:

1
2
3
4
5
6
7
let mut counter = 0;

while counter < 10 {
counter += 1;
}

println!("conuter的值是: {}", counter);
for循环

可以使用while循环来遍历访问数组等类型中的所有元素:

1
2
3
4
5
6
7
8
let mut index = 0;

let a = [1, 3, 5, 7, 9];

while index < 5 {
println!("{}", a[index]);
index += 1;
}

这样使用有一个坏处, 那就是很容易写错index的范围导致数组越界或者没能完全遍历所有元素

rust提供更为简洁的方案for循环来对一个集合的每一个元素执行一些操作

写法类似python中的for i in

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

for i in a {
println!("{}", i);
}

同样的, rust中的for也有类似python中的for i in range()写法:

1
2
3
4
5
6
7
8
9
10
let a = [1, 4, 56, 7, 7];
// i的值在1到9, 也就是1到10左闭右开
for i in (1..10) {
println!("{}", i);
}

// 使用.rev()倒序输出范围
for i in (1..10).rev() {
println!("{}", i);
}

所有权

所有权是rust中最为与众不同的特性, 正是它让rust不需要垃圾回收器(GC)也能保证内存安全, 这部分是rust的重要一环

前置: 堆和栈

在很多语言中编程时并不需要考虑数据实在堆还是栈上, 但在rust中, 一个数据在堆上还是在栈上的影响极为重要

栈(stack)堆(heap)都是程序运行时可以使用的内存, 区别在于它们的结构:

  • 遵循先进后出规则, 就像叠起来的砖, 后放上去的需要先拿走, 所有的数据必须占用已知并且的大小
  • 则用来存储在编译时未知大小的数据, 需要在运行时向系统申请才能正常使用

一般情况下, 访问上的数据总是比访问上的数据要

在代码的运行过程中需要实时追踪堆上分配的无用内存是否被清理, 否则会占用大量内存并拖慢系统和程序运行, 这正是所有权需要解决的问题

所有权的规则

所有权的规则可以概括为三点:

  • rust中每一个值都有一个被称为它的所有者的变量
  • 一个值在任意时刻下有且只有一个所有者
  • 当所有者离开作用域时, 这个值就会被丢弃

作用域

下面是一个作用域的例子:

1
2
3
4
5
6
7
8
9
fn main() {
{
// 这里以及之前, 变量a无效, 因为尚未声明
let a = 10; // 从这里开始, a开始有效
//这之后都是a的作用域
} // 这个大括号结束后就离开了a的作用域
// 在这里a无效, 下面的代码会报错
println!("{}", a);
}

String类型

下面使用String类型来举例说明所有权的规则

创建一个String类型可以使用String下的new或者from函数, 区别在于new创建一个空实例, from基于一个字面量:

1
2
3
4
5
6
7
fn main() {
// a是空的
let a = String::new();

// b是"hello"
let b = String::from("hello");
}

同时, String类型还是可以修改的:

1
2
3
4
5
6
7
fn main() {
let mut s = String::from("hello");
// 使用pust_str在s之后追加内容
s.push_str(", world!");

pritnln!("{}", s);
}

为什么要单独说String是可变的:

在rust中, 普通的字符串都是指向栈上字符串的一个不可变引用&str:

1
let a = "hello world";

这里的a是一个始终不可变引用

即使使用let mut;

1
2
3
let mut a = "hello world";

a = "你好";

只是可以把a绑定到另一个&str类型的你好上, hello world这个字符串仍然没有改变

内存与分配

像上面说的:

1
let a = "hello world";

由于hello world这个字符串字面量在编译时就确定了内容和长度, 所以这段数据会被硬编码进最后的二进制可执行文件的可读数据段中, 这使得程序在读取和使用这些数据时的速度很快

但对于一些大小未知的文本, rust不可能将一块内存放进二进制文件中, 更何况它的大小还可能随着程序运行变化

对于rsut的String类型, 为了支持一个可变, 可增长的文本片段, 需要在堆上分配一个在编译时是位置大小的内存来存放数据, 而为了实现String的这些特性, 需要:

  • 在运行时向内存分配器请求一块内存
  • 处理完后需要将内存返回给分配器

rust中第一步由String::from()完成, 不同语言的不同之处在于第二步:在有垃圾回收机制的语言如python中, 垃圾回收器会记录并回收不使用的内存;在没有垃圾回收机制的语言中则需要显式地进行内存回收

rust采用的方法则是: 内存在拥有它的变量离开作用域后就被自动释放

下面是一个作用域例子的String版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
{
// a是一个局部变量,其值(整数10)通常存储在栈上
// 当a被声明时,它会在栈上分配一个空间来存储值10, a就是10
let a = 10;
} // a离开作用域时, a会被回收, 也就是栈上存储10的内存被回收

{
// 由于String可变, 会在堆上申请内存用于存放数据"hello"
// s会存放在栈上, 但只是内存中"hello"的引用
let s = String::from("hello");
} // s离开作用域时, 数据"hello"的拥有者s离开作用域, "hello"这块内存也就被释放
}

数据与变量的交互(移动)

使用整型进行数据移动:

1
2
3
let a = 5;
let b = a;
println!("{}, {}", a, b);

这里将数据5绑定到变量a, 然后将a绑定到b, 实际是在栈上复制了一块相同的内存

此时栈上有两个5, 原因是栈上的数据相对简单且固定, 所以复制起来快速且高效

使用String写同样的代码:

1
2
3
4
5
let a = String::from("hello");

let b = a;

println!("{}, {}", a, b);

这里的代码则会报错, 因为数据hello实际在堆中, 而ab只是栈上的引用, 尝试将a绑定到b时, 实际上是将内存中的hello的所有权进行移交, 数据的拥有者从a变成了b, 因此a不再可用

为什么不像整型可以直接复制, String只能转移所有权:

前面说过, a实际上存放在栈上, 它是一个引用, 指向堆上的数据hello,

a绑定到b上时, 如果直接复制一个栈上的引用, 那么数据hello就会有两个引用, 也就是两个拥有者

它们离开各自的作用域时都会导致内存中的hello被释放, 则另一个引用就会指向一块被释放的内存, 也就是悬垂引用, 这是不允许的

而当两个引用都离开各自的作用域时, 同一块内存就会进行两次内存释放(每个引用离开时释放一次), 也就是双重释放, 这也是不允许的

使用rust中的所有权机制来解决这个问题就是移交a的所有权至b, 同时a不再可用

数据与变量的交互(克隆)

如果确实需要将内存中的数据复制一份, rust也提供了clone()方法:

1
2
3
4
5
let a = String::from("hello");

let b = a.clone();

println!("{}, {}", a, b);

这里使用了clone方法对a及其指向的内存进行了克隆, 此时ab指向了不同的内存空间, 两块内存空间中存储形同的数据, 所有引用a和引用b都是可用的

只在栈上的数据(复制)

对于直接存放在栈上的数据, 像上面说的, 会直接进行复制, 因为这种复制是快速高效的:

1
2
3
let a = 10;
let b = a;
println!("{}, {}", a, b);

可以直接像这样进行复制的数据类型还有:

  • 所有整数类型, i32, u32等
  • 布尔类型, true和flase
  • 所有浮点类型, f32和f64
  • 字符类型, char
  • 元组, 当其包含的元素都可以复制时, 元组也可以复制, (i32, char, f64)可以, (i32, char, String)不行

所有权和函数

向函数传递一个值与给变量赋值类似, 向函数传递的值也可能会被移动或者复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {

let s = String::from("hello"); // 这里开始s进入作用域
let a = 10; // 这里开始a进入作用域

take_ownership(s); // 这里s进入函数, 所有权被移交给函数内的参数
// 这之后s无效, 因为s已经不在作用域内

make_copy(a); // 这里传递a进入函数, a被复制一份进入函数内的参数
// 这之后a仍然有效, 因为进入函数的是一个复制
}

fn take_ownership(s: String) { // "hello"的所有权被移交给形参s
println!("{}", s);
} // s离开作用域, "hello"内存被释放

fn make_copy(a: i32) { // 整数10被复制了一份作为a进入函数
println!("{}", a);
} // a离开作用域, 栈上的内存被回收

返回值与作用域

返回值同样可以转移所有权, 在函数的最后, 数据如果作为返回值返回, 则所有权被移交至返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {

let s1 = String::from("hello"); // 这里开始s进入作用域

// 下面s1进入函数, 所有权被移交给函数内的对应参数
let s2 = take_ownership(s1); // 这里s被返回给s2, 数据所有权移交给s2, s2开始有效
// 这之后s1无效, 因为s1已经不在作用域内

}

fn take_ownership(s: String) { // "hello"的所有权被移交给形参s
println!("{}", s);
s // 这里将s返回, 并在上面的代码中与s2绑定
} // 这里由于s已经被返回, 所以不会发生任何事

变量的所有权总是遵循同样的模式: 将值赋给另一个变量时移动

可以使用元组返回多个值, 并使用模式匹配来解构并使用:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let s1 = String::from("hello");

let (len , s1) = cal_len(s1);

println!("{}的长度是{}", s1, len);
}

fn cal_len(s: String) -> (usize, String) {
let len = s.len();
(len, s)
}

上面的代码中, s1进入函数后, 计算了长度, 并返回了长度和其本身

但是这似乎确实有点形式主义了, 为什么s1本身一定要传入再传出呢

引用和借用

rust提供了引用来实现函数对值的使用而不获取其所有权:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = String::from("hello");

// 这里传入s1的不可变引用, 函数可以使用s1指向的值而不获取其所有权
let len = cal_len(&s1);
// 由于传入函数的是一个引用, 所以在这里s1仍然可用

println!("{}的长度是{}", s1, len);
}

fn cal_len(s: &String) -> usize { // s1的引用s作为参数进入函数
s.len() // 对s进行操作并返回一个usize
}

创建一个引用的行为就叫做借用

而根据rust的默认不可变原则, 创建的引用也是不可变的, 下面尝试修改借用到的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let mut s1 = String::from("hello");

// 这里传入s1的不可变引用, 函数可以使用s1指向的值而不获取其所有权
let s2 = cal_len(&s1);
// 由于传入函数的是一个引用, 所以在这里s1仍然可用

println!("{}的长度是{}", s1, len);
}

fn cal_len(s: &String) -> String { // s1的引用s作为参数进入函数
s.push_str(" world"); // 尝试在s后附加" world"
s // 返回修改后的字符串
}

实际上上面的代码会报错, 因为引用默认不可变, 也就是无法通过修改一个不可变引用来改变其指向的值

可变引用

和普通标量一样, 引用也有可以有可变引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() { 
let mut s1 = String::from("hello");

// 这里传入的是s1的可变引用,函数可以通过引用修改s1指向的数据而不获取其所有权
change_str(&mut s1); // 因为传入的是可变引用,s1的所有权未被转移
// 此处s1仍然可用,并且其指向的数据已被修改

println!("修改后的字符串: {}", s1);
}

fn change_str(s: &mut String) { // 接收一个可变引用作为参数
s.push_str(" world"); // 通过引用修改其指向的数据
}

可变引用允许通过修改引用的值来修改对应指向的数据, 前提是这个变量也是可变的mut

引用的规则:

  • 在同一作用域内, 一个变量可以同时有多个不可变引用
  • 在同一作用域内, 一个变量不能同时有不可变引用和可变引用
  • 在同一作用域内, 一个变量最多只能有一个可变引用

上面这些规则的目的是为了限制数据竞争, 数据竞争在以下三个条件同时发生时发生:

  • 两个以上的指针访问同一数据
  • 至少有一个指针用于写入数据
  • 系统没有同步数据访问的机制

rust可以使用{}来划定作用域, 来允许数据拥有多个可变引用, 但不是同时拥有

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello");

{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

let r2 = &mut s;
}

悬垂引用(悬垂指针)

在具有指针的应用中可能会发生指针存在, 而其指向的数据已经被错误的释放的情况, 这种情况下的指针就叫悬垂指针, 在rust中则一般不会发生这种问题(因为所有权的存在)

1
2
3
4
5
6
7
8
9
fn main() {
let n = dangle();
}

fn dangle() -> &String {
let s = String::from("hello"); // s进入作用域
// s的作用域
&s // s的引用被返回
} // s被释放

上面的代码中, s离开函数后将会被回收, 而其引用将会被函数作为返回值返回给main函数中的n

也就是说, 在s离开函数, 被回收后, 其引用&s仍然存在, 在某些语言中这回导致很多问题, 如&s对应内存的二次释放, 通过&s对已经释放过的内存进行操作等, 但在rust中, 编译器会直接报错

切片

另一个没有所有权的数据类型是slice, 也就是切片, 切片允许你使用集合中一段连续的元素而不引用整个集合

尝试获取字符串中的第一个单词(获取第一个空格的索引):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let string = String::from("hello world");

let word_index = frist_word(string);
}

fn frist_word(string: String) -> usize {
// 使用as_bytes方法将字符串转换为字节数组
let bytes = string.as_bytes();
// 使用iter方法列出所有元素, enumerate方法包装成元组
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
// 如果循环结束没有找到空格, 则返回整个字符串的长度
s.len()
}

上面的(i, &item)代表每个元素的索引i和其内容的引用&item组成的元组

想要找到空格对应的索引, 只需判断&item是否等于空格, 若是则返回对应的i

在上面的代码中, 找到的i是一个和字符串string无关的一个值, 因为这个值是通过函数计算出来的, 而并未与string绑定, 也就是说在string被清理后仍然可用, 但不一定有意义

1
2
3
4
5
6
7
8
9
fn main() {
let string = String::from("hello world");

let word_index = frist_word(string); // 这里计算出word_index的值为5

string.clear(); // clear方法清空字符串

// word_index的值仍然可用并仍然是5, 但string已经变化
}

如果要按照上面的代码进行查找, 那么第二个单词对应函数应该是这样的:

1
fn second_word(s: String) -> (usize, usize) {}

这些值都与原来的字符串没有关系, 只是对应的数值, rust引入了slice来解决这个问题

字符串切片

字符串切片是对String中的一部分值的引用, 看起来长这样:

1
2
3
4
5
6
fn main() {
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..1];
}

很容易看出切片的写法&引用字符串变量[开始返回..结束范围], 开始和结束范围左闭右开

和python等语言类似, 范围的默认值是[字符串开头..字符串结尾], 所以当索引为开头或结尾时可以省略

有了字符串切片, 上面的代码就能进一步改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let string = String::from("hello world");

let word = frist_word(string);
}

fn frist_word(string: String) -> &String {
// 使用as_bytes方法将字符串转换为字节数组
let bytes = string.as_bytes();
// 使用iter方法列出所有元素, enumerate方法包装成元组
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
// 返回对应范围的切片
return &string[..i];
}
}
// 如果循环结束没有找到空格, 则返回整个字符串的切片
&string[..]
}

和上面获取的索引不同, 字符串切片是对源数据的一部分的不可变引用, 因此源数据不可能再拥有一个可变引用

也就是说, 源数据不再可修改:

1
2
3
4
5
6
7
fn main() {
let string = String::from("hello world");

let word = frist_word(string); // 这里获取了string的一个不可变引用

string.clear(); // 这里将会发生错误, 因为需要clear字符串需要一个可变引用来操作
}

字符串字面量就是slice

String类型中提到过, rust中的字符串字面量其实就是指向二进制程序中特定位置的切片, 其类型是&str

&str绝对不可变, 因为其数据在编译时就静态写入了二进制文件中, 进入内存时对应的内存也不可变

字符串slice作为参数

在知道了前面的内容后, 代码可以写成现在的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
let string = String::from("hello world");
// 获取字符串的完整切片(无论字符串是String还是&str都能用)
let string_slice = string[..];
let word = frist_word(string_slice);

// 或者
let word = frist_word(&string); // &string等同于string[..]
}

fn frist_word(s: &str) -> &str { // 将传入参数和返回值类型都修改为字符串切片

let bytes = string.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
// 返回对应范围的切片
return &string[..i];
}
}
// 如果循环结束没有找到空格, 则返回整个字符串的切片
&string[..]
}

其他类型的slice

与字符串类似, 数组类型也有切片:

1
2
3
let a = [1, 2, 3, 5];
// 数组的切片
let tup_slice = &a[1..3];

可以像字符串切片一样使用数组切片

结构体

结构体struct是rust中的一个重要类型, 它允许命名和包装多个相关的值组成一个有意义的集合

定义和实例化结构体

rust使用struct定义一个结构体:

1
2
3
4
5
struct User {
username: String,
email: String,
age: i32,
}

在大括号里写上数据名:数据类型来定义结构的的字段, 每个字段用,分隔

使用上面的定义实例化一个用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义结构体
struct User {
username: String,
email: String,
age: i32,
}

fn main() {
// 实例化结构体
let user1 = User {
username: String::from("tom"),
email: String::from("tom@gmail.com"),
age: 21,
};
}

可以通过结构体更新语法使用更少的代码达到更新结构体实例的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义结构体
struct User {
username: String,
email: String,
age: i32,
}

fn main() {
// 实例化结构体
let user1 = User {
username: String::from("tom"),
email: String::from("tom@gmail.com"),
age: 21,
};
// 实例化一个新的变量, 其只有username和user1不同
let user2 = User {
username: String::from("jerry"), // 与user1不同的地方
..user1 // 其他与user1相同的地方, 相当于直接复制user1的字段
}
}

上面的更新语句就像带有=的赋值语句

除了user2中的username是更新的, user1中的email将会被移动到user2(移交所有权), age则会直接复制一份

所以user最终可用的部分是user1.usernameuser1.age

访问结构体数据

和元组类似, 访问结构体数据可以使用.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义结构体
struct User {
username: String,
email: String,
age: i32,
}

fn main() {
// 实例化结构体
let user1 = User {
username: String::from("tom"),
email: String::from("tom@gmail.com"),
age: 21,
};

// 访问user1的username
let name1 = user1.username;
println!("{}", name1);
}

没有命名字段的结构体

rust还提供与元组类似的结构体, 结构体中只有类型而没有命名的就叫元组结构体, 通常用于给元组取一个名字:

1
2
3
4
5
6
7
8
9
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
// 定义一个颜色
let black = Color(0, 0, 0);
// 定义一个坐标
let orign = Point(0, 0, 0);
}

在上面的代码中blackorign并不是同一个类型, 而是ColorPoint类型, 只是在类别上属于结构体

没有任何字段的结构体

没有任何字段的结构体被称为类单元结构体, 它们类似于单元类型, 在结构体中的用途一般是为了实现一些方法而不存储数据

使用结构体的示例程序

计算长方形面积

编写一个代码尝试计算一个长方形的面积, 并逐步使用结构体来替代简单变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let h = 50;
let w = 30;

println!("长方体的形状是{} * {}", h, w);

let size = size(h, w);

println!("长方体的面积是{}", size);
}

fn size(h:i32, h:i32) -> i32 {
h * w
}

由于长方形长和宽是关联的, 上面的代码单独定义了两个变量, 传入size函数的也是两个, 这两者之间并无关联

使用元组重构

下面使用元组重构:

1
2
3
4
5
6
7
8
9
10
fn main() {
let s = (30, 50);
println!("长方形的形状是{} * {}", s.0, s.1);
let size = size(s);
println!("长方形的面积是{}", size);
}

fn size(s: (u32, u32)) -> u32 {
s.0 * s.1
}

问题又来了, 这里的s.0s.1在代码中很难体现长方体的性质, 在需要进行其他操作如绘图时很难体现宽和高的区别

使用结构体重构

下面使用结构体来重构代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
// 定义时很明显能体现长方形的特征
let rect1 = Rectangle{width: 30, height: 50};
println!("长方形的形状是{}*{}", rect1.width, rect1.height);
let size = size(rect1);
println!("长方形的面积是{}", size);
}

// 传入参数时只需要传入一个单独的长方形结构体
fn size(rect:Rectangle) -> u32 {
// 函数体中也能体现具体的意图
rect.width * rect.height
}

尝试打印实例

尝试使用println!打印rect1的值时, rust会报错:

1
2
3
4
5
6
7
8
9
10
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle{width: 30, height: 50};
// 尝试打印
println!("{}", rect1);
}

因为自定义的结构体并没有实现Display, rust不知道该以什么格式将数据打印出来

rust提供了debug来输出调试信息所以代码可以改成下面的模式:

1
2
3
4
5
6
7
8
9
10
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle{width: 30, height: 50};
// 使用Debug方式尝试打印
println!("{:?}", rect1);
}

代码仍然会报错, 因为Rectangle这个结构体并没有实现Debug方法, 需要显示的引入外部属性:

1
2
3
4
5
6
7
8
9
10
11
12
// 在定义的上方加入#[derive(Debug)]
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle{width: 30, height: 50};
// 使用Debug方式尝试打印
println!("{:?}", rect1);
}

现在可以成功打印出rect1的类型和值:

1
Rectangle { width: 30, height: 50 }

:?改成:#?可以增加打印结果的可读性:

1
2
3
4
Rectangle {
width: 30,
height: 50,
}

方法

方法函数类似, 同样使用fn定义, 一样可以传入参数和传出返回值

它们的第一个参数总是self, 意为包含这个方法的结构体或枚举类型实例本身

定义方法

使用上面Rectangle的定义, 为其定义一个size方法, 为结构体定义方法使用impl关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Rectangle {
width: u32,
height: u32,
}

// 使用impl实现方法
impl Rectangle {
// 以自身为参数
fn size(&self) -> u32 {
// 返回自身面积
self.width * self.height
}
}

fn main() {
let rect1 = Rectangle{
width: 30,
height: 50
};
// 使用.来调用结构体方法
let size = rect1.size();
println!("长方形的面积是: {}", size);
}

有多个参数的方法

实现一个判断长方体能否被另一个长方体完全覆盖的方法:

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

// 使用impl实现方法
impl Rectangle {
// 以自身为参数, 同时接收一个Rectangle类型的引用
fn can_hold(&self, another: &Rectangle) -> bool {
// 比较长和宽(假设不变方向)
if self.width > another.width && self.height > another.height {
true
} else {
false
}
}
}

fn main() {
let rect1 = Rectangle{
width: 30,
height: 50
};
let rect2 = Rectangle{
width: 20,
height: 45,
}
// 使用.来调用结构体方法
let hold = rect1.can_hold(&rect2);
println!("{}", hold);
}

关联函数

impl块中定义的函数叫关联函数, 其参数的第一个不是&self, 下面创建一个正方形关联函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

// 使用impl实现方法
impl Rectangle {
// 创建正方形关联函数, 传入边长, 返回正方形
fn square(len: u32) -> Rectangle {
Rectangle {
width: len,
height: len
}
}
}

fn main() {
// 使用::调用关联函数, 类似String::from
let squ1 = Rectangle::square(15);
println!("正方形的形状{:#?}", squ1);
}

多个impl块

同一个结构体的impl块可能有多个

将方法和关联函数分散写在多个impl块中或全写在一起在语法上都是可以的

枚举

枚举是rust中另一个重要的数据类型,允许通过列举可能的成员来定义一个类型

定义枚举

假设需要定义一个ip地址, 它要么是ipv4要么是ipv6, 而不能两者都是, 这时可以使用枚举类型来定义:

1
2
3
4
enum IpKind {
V4,
V6
}

在上面的代码中, 定义了一个IpKind枚举类型, v4v6是它的成员, 成员的命名方式是单词开头为大写

在类型上看, v4v6是同一个类型, 只是不同的变体

枚举值

创建枚举类型的两个不同实例:

1
2
3
4
5
6
7
8
9
10
fn main() {
// 定义枚举类型
enum IpKind {
V4,
V6
}
// 使用::访问成员
let ip4 = IpKind::V4;
let ip6 = IpKind::V6;
}

这里的ip4ip6是同一个类型IpKind, 但是其的不同变体

结合结构体创建新的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义枚举类型
enum IpKind {
V4,
V6,
}

//使用结构体, 添加枚举类型
struct IpAddr {
// ip类型为枚举类型, 有两种可能: v4和v6
kind: IpKind,
// 具体地址为String类型
address: String,
}

fn main() {
let localhost = IpAddr {
// 类型为ipv4
kind: IpKind::V4,
// 具体地址为127.0.0.1
address: String::from("127.0.0.1"),
};

}

但是在上面的代码中同时使用了两种数据类型结合才实现了一个数据的功能

枚举类型实际上可以直接关联对应的数据类型:

1
2
3
4
5
6
7
8
9
10
enum IpAddrKind {
// 在定义关联具体数据的类型
V4(String),
V6(String),
}

fn main() {
let localhost = IpAddrKind::V4(String::from("127.0.0.1"));

}

同一个枚举类型的不同变体的数据类型可以不同:

1
2
3
4
5
6
7
8
9
enum IpAddrKind {
// 对不同的变体使用不同的数据类型
V4(u8, u8, u8, u8),
V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"))

枚举与空值

在rust中并没有其他语言中的空值, 即所谓的null, 取而代之的是一个枚举类型option<T>

在其他语言中尝试像访问正常值一样使用一个空值将会引起一些不可预知的错误, 在rust中并没有null这样的东西, 但这样的概念存在于预导入模块的option<T>枚举变量中, 这里的<T>代表泛型, 意思是有多个数据类型都能使用option枚举

1
2
3
4
5
6
fn main() {
enum Option<T> {
Some(T),
None,
}
}

上面可以看到option枚举的变体只有两个, Some()None

这样使用None的优点在于, 在不使用option枚举时, 代码中的值不可能为None, 只有当使用一个option枚举类型的值时这个值才有变成None的可能, 缩小了可能出现None也就是null的范围

1
2
3
4
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = s + y;

match控制流

rust中match来进行分支运算, 对一个值进行匹配, 通过不同的匹配结果来进行不同的操作:

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

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

fn main() {}

上面的代码尝试对coin的具体变体进行匹配, 根据不同的变体类型返回不同的值或操作, =>的后面可以直接返回值也可以进行操作:

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

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

fn main() {}
匹配Option<T>

前面提到使用option<T>的初衷是防止null也就是None的泛滥, 当option<T>Some<T>时我们需要对其中的值进行提取, 出现None则不做操作, 这一步骤同样可以使用match来实现:

1
2
3
4
5
6
7
8
9
10
11
// 匹配一个变量, 如果有值则加一, 没有就返回
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
// 如果x是变体None则返回
None => None,
// 如果x是变体Some(T)则加一
Some(i) => Some(i + 1)
}
}
}
匹配一定穷尽

使用match进行匹配值, 需要列出匹配对象所有的可能性, 如:

1
2
3
4
5
6
7
8
fn mian() {
let x = Some(8);
match x {
Some(i) => {
println!("{}", i);
}
}
}

上面的代码是行不通的, 在进行匹配中, x的所有可能没有被列出来, 当x为None时, 编译器不知道该如何处理这个值或如何进行操作, 因此在写match就应当考虑所有的可能性并全部列出

通配符和_占位符

当需要完全覆盖所有可能, 但没有办法手动列出时, 可以使用占位符来替代某些特定的可能

1
2
3
4
5
6
7
8
9
10
11
//投骰子, 投到3获得帽子, 投到7失去帽子, 其余情况移动对应步数

fn main() {
let roll = 9;
match roll {
3 => println!("you gain a hat"),
7 => println!("you lost a hat"),
other => println!("move {} step", other),
}
}