0%

Rust

rust开发

为什么要学习Rust?

Rust 语言的特点

  • 内存安全:Rust 的所有权系统在编译时防止空悬指针、数据竞争等内存错误,无需垃圾收集器。
  • 并发编程:Rust 提供了现代的语言特性来支持并发编程,如线程和消息传递,使得编写并发程序更加安全和容易。
  • 性能:Rust 编译为机器码,没有运行时或垃圾收集器,能够提供接近 C 和 C++ 的性能。
  • 类型系统:Rust 的类型系统和模式匹配提供了强大的抽象能力,有助于编写更安全、更可预测的代码。
  • 错误处理:Rust 的错误处理模型鼓励显式处理所有可能的错误情况。
  • 宏系统:Rust 提供了一个强大的宏系统,允许开发者在编译时编写和重用代码。
  • 包管理:Rust 的包管理器 Cargo 简化了依赖管理和构建过程。
  • 跨平台:Rust 支持多种操作系统和平台,包括 Windows、macOS、Linux、BSDs 等。
  • 社区支持:Rust 有一个活跃的社区,提供了大量的库和工具。
  • 工具链:Rust 拥有丰富的工具链,包括编译器、包管理器、文档生成器等。
  • 无段错误:Rust 的所有权和生命周期规则保证了引用的有效性,从而避免了段错误。
  • 迭代器和闭包:Rust 提供了强大的迭代器和闭包支持,简化了集合的处理。

Rust 基础语法

输出

rust有两个标准输出的函数,println!()print!(),前者和后者的区别仅在于前者会多输出一个换行符

占位符

C语言中使用%(字母)作为占位符,rust中使用的是{}进行占位

例如:

1
2
3
4
fn main() {
let a = 10;
println!("a is {}", a);
}

输出得到:a is 10

如果将a输出两遍,不需要在最后写两遍a,此时可以将后面的参数看成是一个数组,然后可以通过索引访问

例如:

1
2
3
4
fn main() {
let a = 10;
println!("a is {0}, a again is {0}", a);
}

输出得到:a is 10, a again is 10

转义字符

rust的转义字符除{}外都是前面加反斜杠(和C语言一样),{}的转义是{{`和`}}

例如:

1
2
3
fn main() {
println!("{{}}");
}

输出:{}

变量

声明

rust使用let声明变量,但是一般声明的变量是不可变的变量

1
2
let a = 10;
a = 5; // 这会报错

如果需要后面对a再进行赋值,需要添加mut关键字

1
2
let mut a = 10;
a = 5; // 这样是对的

使用mut关键字声明的叫可变变量,没有使用mut关键字声明的是不可变变量

不可变变量与常量

不可变变量是不能重新进行赋值,但是可以重新进行声明的变量,常量不能重新赋值,也不能重新声明

1
2
let a = 10;
let a = 20; // 这样是对的
1
2
const a = 10;
let a = 20; // 这样是错的

数据类型

rust是静态语言类型,在声明变量的时候可以显式指定类型

基本类型:

i32 32位有符号整数
u32 32位无符号整数
f64 64位浮点数
bool 布尔类型
char 字符

如果不显式指定,那么编译器会自动判断该变量的类型

整数型:

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

isize 和 usize 两种整数类型是用来衡量数据大小的,它们的位长度取决于所运行的目标平台,如果是 32 位架构的处理器将使用 32 位位长度整型。

整数的表述方法有以下几种:

进制
十进制 98_222
十六进制 0xff
八进制 0o77
二进制 0b1111_0000
字节(只能表示 u8 型) b’A’

在上面的例子中,可以看到数字中间添加了下划线,下划线本身没有任何意义,只是提高了代码的可读性,1_2341234是一样的,前者更容易读出他是一个“千”位的数字

浮点数型(Floating-Point)

默认情况下,64.0 将表示 64 位浮点数,因为现代计算机处理器对两种浮点数计算的速度几乎相同,但 64 位浮点数精度更高。

字符型

字符型用 char 表示。

Rust的 char 类型大小为 4 个字节,代表 Unicode标量值,这意味着它可以支持中文,日文和韩文字符等非英文字符甚至表情符号和零宽度空格在 Rust 中都是有效的 char 值。

Unicode 值的范围从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF (包括两端)。 但是,”字符”这个概念并不存在于 Unicode 中,因此您对”字符”是什么的直觉可能与Rust中的字符概念不匹配。所以一般推荐使用字符串储存 UTF-8 文字(非英文字符尽可能地出现在字符串中)。

注意:由于中文文字编码有两种(GBK 和 UTF-8),所以编程中使用中文字符串有可能导致乱码的出现,这是因为源程序与命令行的文字编码不一致,所以在 Rust 中字符串和字符都必须使用 UTF-8 编码,否则编译器会报错。

运算

rust支持自运算操作,如sum += 1 = sum = sum + 1

但是不支持自增和自减的++--

复合类型

元组是用一对 () 包括的一组数据,可以包含不同种类的数据:

1
2
3
4
5
6
let tup: (i32, f64, u8) = (500, 6.4, 1);
// tup.0 等于 500
// tup.1 等于 6.4
// tup.2 等于 1
let (x, y, z) = tup;
// y 等于 6.4

数组用一对 []包括的同类型数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let a = [1, 2, 3, 4, 5];
// a 是一个长度为 5 的整型数组

let b = ["January", "February", "March"];
// b 是一个长度为 3 的字符串数组

let c: [i32; 5] = [1, 2, 3, 4, 5];
// c 是一个长度为 5 的 i32 数组

let d = [3; 5];
// 等同于 let d = [3, 3, 3, 3, 3];

let first = a[0];
let second = a[1];
// 数组访问

a[0] = 123; // 错误:数组 a 不可变
let mut a = [1, 2, 3];
a[0] = 4; // 正确

函数

函数的基本格式:

1
fn <函数名> ( <参数> ) <函数体>

rust函数命名风格是小写字母加下划线分割

rust语言函数定义位置没有要求,可以在任意位置定义函数

定义函数如果有参数,则必须声明参数名称和类型:

1
2
3
4
5
6
7
8
fn main() {
another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
println!("x 的值为 : {}", x);
println!("y 的值为 : {}", y);
}

Rust 中可以在一个用 {}包括的块里编写一个较为复杂的表达式:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = 5;

let y = {
let x = 3;
x + 1
};

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

结果:

1
2
x 的值为 : 5
y 的值为 : 4

在上面的程序中,包含了一个表达式块:

1
2
3
4
{
let x = 3;
x + 1
};

而且在块中可以使用函数语句,最后一个步骤是表达式,此表达式的结果值是整个表达式块所代表的值。这种表达式块叫做函数体表达式。

注意:**x + 1**之后没有分号,否则它将变成一条语句

rust不支持自动判断返回值类型,需要在函数声明时定义返回值的类型

1
2
3
fn add(a: i32, b: i32) -> i32 {
return a + b;
}

如果没有明确声明函数返回值的类型,函数将被认为是”纯过程”,不允许产生返回值,return 后面不能有返回值表达式。

而函数体表达式和函数体不一样,函数体表达式不能使用return关键字。

条件语句

基础语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() { 
let a = 12;
let b;
if a > 0 {
b = 1;
}
else if a < 0 {
b = -1;
}
else {
b = 0;
}
println!("b is {}", b);
}

Rust 中的条件表达式必须是 bool 类型,例如下面的程序是错误的:

1
2
3
4
5
6
fn main() { 
let number = 3;
if number { // 报错,expected `bool`, found integerrustc(E0308)
println!("Yes");
}
}

虽然 C/C++ 语言中的条件表达式用整数表示,非 0 即真,但这个规则在很多注重代码安全性的语言中是被禁止的。

所以可以使用函数体表达式构造一个三元条件运算

1
2
3
4
5
fn main() { 
let a = 3;
let number = if a > 0 { 1 } else { -1 };
println!("number 为 {}", number);
}

结果:

1
number 为 1

循环

while循环

1
2
3
4
5
6
7
8
fn main() {
let mut number = 1;
while number != 4 {
println!("{}", number);
number += 1;
}
println!("EXIT");
}

for循环

for循环感觉和Python的for很像

以下例子中a.iter() 代表 a 的迭代器(iterator)

1
2
3
4
5
6
fn main() { 
let a = [10, 20, 30, 40, 50];
for i in a.iter() {
println!("值为 : {}", i);
}
}
1
2
3
4
5
6
fn main() { 
let a = [10, 20, 30, 40, 50];
for i in 0..5 {
println!("a[{}] = {}", i, a[i]);
}
}

loop循环

这是rust原生的无限循环结构

1
2
3
4
5
6
7
8
9
10
11
12
fn main() { 
let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
let location = loop {
let ch = s[i];
if ch == 'O' {
break i;
}
i += 1;
};
println!(" \'O\' 的索引为 {}", location);
}

迭代器

创建迭代器

最常见的方式是通过集合的 .iter().iter_mut().into_iter() 方法来创建迭代器:

  • .iter():返回集合的不可变引用迭代器。
  • .iter_mut():返回集合的可变引用迭代器。
  • .into_iter():将集合转移所有权并生成值迭代器。

.iter()方法返回一个不可变引用的迭代器。可以遍历数组中的元素,但不能修改它们。

1
2
3
for element in arr.iter() {
println!("{}", element);
}

.iter_mut() 方法返回一个可变引用的迭代器。不仅可以遍历数组中的元素,还可以修改它们的值。

1
2
3
4
for element in arr.iter_mut() {
*element += 1; // 修改元素的值
println!("{}", element);
}

这里,element 是数组元素的可变引用,因此可以通过解引用运算符 * 来修改元素的值。

解引用运算符 * 在 Rust 中用于访问引用指向的值。引用在 Rust 中是一种指针类型,它允许你引用内存中的某个值而不需要拥有该值。解引用运算符 * 可以让你获取引用指向的实际值。

1
2
3
4
5
6
7
fn main() {
let a = 10;
let b = &a; // b 是 a 的引用

println!("a 的值是 {}", a);
println!("b 引用的值是 {}", *b); // 使用 *b 来获取 b 引用的值
}

.into_iter() 方法将数组的所有权转移给迭代器,并生成一个值迭代器。这意味着迭代器会直接生成数组中的元素,而不是它们的引用。在遍历过程中,数组的所有权会被移动到迭代器中。

1
2
3
for element in arr.into_iter() {
println!("{}", element);
}

这里,element 是数组中的元素本身。需要注意的是,使用 .into_iter() 之后,arr 就不再可用,因为它已经将所有权转移给了迭代器。

迭代器方法

Rust 的迭代器提供了丰富的方法来处理集合中的元素,其中一些常见的方法包括:

  • map():对每个元素应用给定的转换函数。

map() 方法对迭代器中的每个元素应用一个指定的函数,返回一个新的迭代器,其中包含应用该函数后的结果。

1
2
3
let arr = [1, 2, 3, 4];
let doubled: Vec<_> = arr.iter().map(|&x| x * 2).collect();
println!("{:?}", doubled); // 输出: [2, 4, 6, 8]

在这个例子中,map() 方法将数组中的每个元素乘以 2,返回一个新的迭代器,这个迭代器会被收集到一个向量中。

  • filter():根据给定的条件过滤集合中的元素。
1
2
3
let arr = [1, 2, 3, 4];
let evens: Vec<_> = arr.iter().filter(|&&x| x % 2 == 0).collect();
println!("{:?}", evens); // 输出: [2, 4]

在这个例子中,filter() 方法只保留了数组中能被 2 整除的元素。

  • fold():对集合中的元素进行累积处理。
1
2
3
let arr = [1, 2, 3, 4];
let sum: i32 = arr.iter().fold(0, |acc, &x| acc + x);
println!("{}", sum); // 输出: 10

在这个例子中,fold() 方法从初始值 0 开始,对数组中的每个元素进行累加,最终得到数组元素的总和。

  • skip():跳过指定数量的元素。
1
2
3
let arr = [1, 2, 3, 4];
let skipped: Vec<_> = arr.iter().skip(2).collect();
println!("{:?}", skipped); // 输出: [3, 4]

在这个例子中,skip(2) 方法跳过了数组中的前两个元素,返回了从第三个元素开始的新迭代器。

  • take():获取指定数量的元素。
1
2
3
let arr = [1, 2, 3, 4];
let taken: Vec<_> = arr.iter().take(2).collect();
println!("{:?}", taken); // 输出: [1, 2]

在这个例子中,take(2) 方法只获取了数组中的前两个元素。

  • enumerate():为每个元素提供索引。
1
2
3
4
5
6
7
8
9
let arr = [10, 20, 30, 40];
for (i, &value) in arr.iter().enumerate() {
println!("Element at index {} is {}", i, value);
}
// 输出:
// Element at index 0 is 10
// Element at index 1 is 20
// Element at index 2 is 30
// Element at index 3 is 40

在这个例子中,enumerate() 方法为数组中的每个元素提供了一个索引,你可以同时访问索引和元素的值。

迭代器内容好多啊。。。不想学了,用的时候再学,更多内容请看Rust 迭代器 | 菜鸟教程 (runoob.com)

闭包

一开始学没太明白这是个什么东西,他和函数差不多,所以先看一下它和函数的区别:

特性 闭包 函数
匿名性 是匿名的,可存储为变量 有固定名称
环境捕获 可以捕获外部变量 不能捕获外部变量
定义方式 ` 参数
类型推导 参数和返回值类型可以推导 必须显式指定
存储与传递 可以作为变量、参数、返回值 同样支持

闭包的声明

闭包的语法声明:

1
let closure_name = |参数列表| 表达式或语句块;

参数可以有类型注解,也可以省略,Rust 编译器会根据上下文推断它们。

1
let add_one = |x: i32| x + 1;

闭包的参数和返回值: 闭包可以有零个或多个参数,并且可以返回一个值。

1
let calculate = |a, b, c| a * b + c;

闭包的调用:闭包可以像函数一样被调用。

1
let result = calculate(1, 2, 3);

匿名函数

闭包在 Rust 中类似于匿名函数,可以在代码中以 {} 语法块的形式定义,使用 || 符号来表示参数列表,实例如下:

1
2
let add = |a, b| a + b;
println!("{}", add(2, 3)); // 输出: 5

在这个示例中,add 是一个闭包,接受两个参数 a 和 b,返回它们的和。

捕获外部变量

闭包可以捕获周围环境中的变量,这意味着它可以访问定义闭包时所在作用域中的变量。例如:

1
2
3
let x = 5;
let square = |num| num * x;
println!("{}", square(3)); // 输出: 15

以上代码中,闭包 square 捕获了外部变量 x,并在闭包体中使用了它。

闭包可以通过三种方式捕获外部变量:

  • 按引用捕获(默认行为,类似 &T
  • 按值捕获(类似 T
  • 可变借用捕获(类似 &mut T
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let mut num = 5;

// 按引用捕获
let print_num = || println!("num = {}", num);
print_num(); // 输出: num = 5

// 按值捕获
let take_num = move || println!("num taken = {}", num);
take_num(); // 输出: num taken = 5
// println!("{}", num); // 若取消注释,将报错,num 所有权被转移

// 可变借用捕获
let mut change_num = || num += 1;
change_num();
println!("num after closure = {}", num); // 输出: num after closure = 6
}

说明:

  • 闭包默认按引用捕获外部变量。
  • 使用 move 关键字可以强制按值捕获,将外部变量的所有权转移到闭包内。
  • 如果闭包需要修改外部变量,需显式声明为 mut 闭包。

移动与借用

闭包可以通过 move 关键字获取外部变量的所有权,或者通过借用的方式获取外部变量的引用。例如:

借用变量:默认情况下,闭包会借用它捕获的环境中的变量,这意味着闭包可以使用这些变量,但不能改变它们的所有权。这种情况下,闭包和外部作用域都可以使用这些变量。例如:

1
2
3
4
let x = 10;
let add_x = |y| x + y;
println!("{}", add_x(5)); // 输出 15
println!("{}", x); // 仍然可以使用 x

获取所有权:通过在闭包前添加 move 关键字,闭包会获取它捕获的环境变量的所有权。这意味着这些变量的所有权会从外部作用域转移到闭包内部,外部作用域将无法再使用这些变量。例如:

1
2
3
4
let s = String::from("hello");
let print_s = move || println!("{}", s);
print_s(); // 输出 "hello"
// println!("{}", s); // 这行代码将会报错,因为 s 的所有权已经被转移给了闭包

通过这两种方式,Rust 提供了灵活的机制来处理闭包与外部变量之间的关系,使得在编写并发、安全的代码时更加方便。

闭包也就先学到这里,更多请看Rust 闭包 | 菜鸟教程 (runoob.com)

所有权

所有权规则:

  • Rust 中的每个值都有一个变量,称为其所有者。
  • 一次只能有一个所有者。
  • 当所有者不在程序运行范围时,该值将被删除。

变量范围

1
2
3
4
5
6
{
// 在声明以前,变量 s 无效
let s = "runoob";
// 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效

变量范围是变量的一个属性,其代表变量的可行域,默认从声明变量开始有效直到变量所在域结束。

内存和分配

如在C语言中,假设有一个字符串Hello World,然后有以下程序

1
2
3
4
{
char *s = strdup("runoob");
free(s); // 释放 s 资源
}

在C语言程序中需要使用free手动释放内存,但是在rust中,不需要手动释放内存,当一个变量在其变量范围结束时rust会自动释放该变量。