Rust 学习笔记 - 数据类型、函数、控制流
本文属于学习笔记,内容可能有误、可能不全面,仅代表个人在学习这一特性时的理解和总结
本文记录了对于 Rust 数据类型、函数、控制流相关的内容。并不详细记录所有细节,只记录和其他高级语言有区别的部分。
1 数据类型
再次强调 Rust 是一个静态类型语言,必须能在编译阶段知道所有变量的类型。不需要显式规定类型的必要前提是可以推导。
例如在进行类型转换时:
let guess: u32 = "42".parse().expect("Not a number!");
这里不能确定要将 42 解析成什么类型,可能是 i32 u32 f64……,所以必须指定 guess: u32
Rust 的数据类型分为两类,标量类型(scarlar)和组合类型(compound)。
1.1 标量类型
1.1.1 整数类型
变量名从表意的 int long 变成了可以清晰表示数字位数的 i32 i64,jiangly写算法就喜欢 using i64 = long long;。
并且最大提供了 i128 u128 ,u128 最大能表示 340282366920938463463374607431768211455 ,一般情况绝对够用了。
类似于 size_t Rust 有 isize usize。位数和系统的位数一致。
数字的字面量有以下特性:
- 默认类型为
i32 - 可以在后缀增加指定类型,例如
123u8 - 与其他语言相同,前缀指定进制,十六进制:
0x;八进制:0o;二进制0b - 可以用下划线作为分隔符,例如
1000_0000 - 字节:
b'A'表示一个u8类型的整数,即60,等价于C/C++中的unsigned char
1.1.2 字符类型
Rust 中的字符类型(char) 和 C/C++ 中的不同,占用空间4字节,表示的是 Unicode 编码而非 ASCII 编码。
总而言之,Rust 中的 char 类型可以表示任何键盘可以打出来的一个字符。这里的一个是直观感觉的一个字符,站在用户角度的一个字符,而非程序员习惯的一个字符。
1.1.3 浮点类型
基本和传统语言一样,但是字面量的默认类型为 f64 ,因为 Rust 认为现代计算机中双浮点数和单浮点数的计算效率已经差距不大。
1.1.4 布尔类型
基本和传统语言一样,占用空间1字节。
1.2 复合类型
分为元组(tuple)和数组(array)。
1.2.1 元组
元组中的元素类型可以不同。
下面一个例子包含了元组的所有基本用法:
fn main() {
let tup = (500, 6.4, 1);
let (_, y, _) = tup;
println!("The value of x is: {}", tup.0);
println!("The value of y is: {}", y);
println!("The value of z is: {}", tup.2);
let unit: () = ();
println!("The value of unit is: {:?}", unit);
}
-
使用
(type1, type2, ..., type_n)来定义元组类型 -
用模式匹配的方式解构元组
-
支持匿名变量
_ -
用句点
.索引访问变量 -
特殊的,空元组
()叫做单元类型(unit type),该类型只有一种值,即单元值。单元值和单元类型都写作()如果一个表达式不返回任何值,就隐式返回单元值。
单元类型就类似于 C 中的
void这里的表达式用编译原理语法分析过程中的状态来理解。见2.1节
1.2.2 数组
数组中的元素类型必须相同。
fn main() {
let a: [i32; 5] = [1,2,3,4,5];
println!("a has {} elements", a.len());
println!("{:?}", a);
println!("a[0] = {}", a[0]);
println!("a[1] = {}", a[1]);
let b: [i32; 5] = [1; 5];
println!("{:?}", b);
}
- 类型声明:[type; len]
- 数组的值有两种表示:
[num1, num2, ..., num_n][num; repeat]
- 使用方括号
[]索引
和大多数其他语言相同,Rust 的数组使用栈空间。同样也有 Vector 类型占用堆空间,这在后面再讨论。
最重要的一点是,Rust 的索引必须在 [0, len-1] 的范围里,即不可以访问未被分配的无效内存。在运行过程中,任何对无效内存的访问均会报错;在编译阶段,一些很明显的访问无效内存操作也会被检测到。
1.3 类型转换
暂时只讨论最简单的类型转换。还有很多使用了标准库中的一些 Trait 进行类型转换的方法。
Trait 是 Rust 中的一个重要概念,可以被简单的理解为接口。
不像 C/C++ 有很多隐式类型转换的情况(如整型提升等),Rust 中几乎所有类型转换都需要显式进行。
fn main() {
let x: i32 = 5;
let y: i64 = 5;
x + y; // 报错,类型不匹配
}
fn main() {
let x = 5; // 自动推导为i64
let y: i64 = 5;
x + y; // 不报错,类型为 i64
}
如果不指定 x 的类型为 i32 ,则会在类型推导过程中把 x 的类型推导为 i64,看似是隐式类型转换了,本质上还是定义过程的类型推导。
显示类型转换的方式类似于 Typescript:
fn main() {
let x: i32 = 5;
let y: i64 = 5;
let z = x as i64 + y;
}
使用 as type 的方式转换类型。
2 函数、语句和表达式
这一部分站在编译原理的角度理解。
2.1 表达式
表达式用于计算并返回值。表达式可以是常量、变量、算术运算、函数调用等。
3 + 4 * 2
add(y, 5)
用来创建新作用域的大括号(代码块) {} 也是一个表达式,返回值大括号里的最后一个表达式:
{
let x = 3;
x + 1
}
这里的返回值就是 4。需要注意的是,x + 1 的末尾没有 ; ,如果加上分号,则变成了一个语句,而语句没有返回值。
总结:
大括号表达式 ::= { <语句列表> [表达式] }
如果有表达式,大括号表达式的返回值为表达式的值;如果没有表达式,返回值为单元值()。
2.2 语句
语句用于执行某些操作,通常不返回值。常见的语句包括变量声明、赋值、表达式语句、控制流语句(如if、for、while)等。
let x = 5;
x = x + 1;
if x > 5 {
println!("x is greater than 5");
}
通常不返回值,也就是说有例外。见3.1节 if else 语句。
2.3 函数
函数是代码的基本组织单位,用于封装特定的功能。函数的定义包括函数名、参数列表、返回类型和函数体。
fn test(a: i32, b: i32) -> i32 {
if a > b {return a - b}
a + b
}
函数体有点像是一个大括号表达式,所以末尾的表达式可以不使用 return 进行返回。用 return 可以让函数提前返回。(但是大括号表达式里不能用 return 进行返回)
如果函数有返回值,必须指定返回值类型(否则返回值类型为单元类型 () )。
函数在使用前,并不需要先声明。例如
fn main() {
println!("The value of a is: {}", test(1, 2));
}
fn test(a: i32, b: i32) -> i32 {
a + b
}
3 控制流
外观上最显著的不同是,条件不需要加括号。
3.1 if 语句
基本用法与其他语言类似,不赘述。
但是 Rust 中,if-else 语句是可以有返回值的。
需要明确一下,这里的有返回值指的是返回值不是单元值
()
fn main() {
let x = 5;
let ret = if x > 5 {
println!("x is greater than 5");
0
} else {
println!("x is less than or equal to 5");
1
};
println!("ret is {}", ret);
}
- 必须有一个
else才可以有返回值。(否则可能没有返回值) - 所有大括号表达式的返回值类型必须相同。(否则类型不可推断)
再次强调,上面说的 没有返回值 指的是 返回值=()。如果显示指定变量的返回值就是 (),不需要 else 也可以。
fn main() {
let x = 5;
let ret: () = if x == 5 {
println!("x is five!");
};
}
当然,这种写法并没有什么意义,只是在反向理解 Rust 编译过程的实现。
通常的用法如下,目的是压行,代码更模块化、可读性更强,类似与 python 里的 x = 0 if condition else 1
fn main() {
let x = 5;
let ret = if x == 5 {true} else {false};
println!("ret is {}", ret);
}
3.2 loop
Rust 提供了 3 中循环,loop,while,for。loop 是其他语言没有的。
loop 就是一个 while true 的死循环。但是提供了一些语法糖:
3.2.1 嵌套循环跳出
break 语句用于跳出循环,同其他语言只能跳出最内层循环。
fn main() {
loop {
print!("1");
loop {
print!("2");
break;
}
}
}
// 结果是 1212121212...
但是可以给外层的 loop 添加一个标记:'label ,就可以直接跳出外层循环。
fn main() {
'out: loop {
print!("1");
loop {
print!("2");
break 'out;
}
}
}
// 结果是 12
相比于其他语言,想要直接跳出多层循环只有两种方法:
- 使用一个中间变量记录结果(
while (loop) {}) - 使用goto
3.2.2 返回值
loop 可以通过 break 传递返回值。太方便了。
fn main() {
let ret = 'out: loop {
print!("1");
loop {
print!("2");
break 'out 123;
}
};
println!("ret: {}", ret);
}
3.3 while循环
没什么区别,不讲了
3.4 for循环
没什么细节,让GPT写了一些常见的用法。
在 Rust 中,for 循环有多种用法,通常用于遍历集合或范围。以下是所有常见的 for 循环用法:
- 遍历范围:
for i in 0..5 {
println!("{}", i);
}
上面的代码将输出 0 到 4。
- 遍历集合:
let arr = [10, 20, 30, 40, 50];
for element in arr.iter() {
println!("{}", element);
}
使用 iter 方法遍历数组。
- 遍历可变集合:
let mut vec = vec![1, 2, 3, 4, 5];
for element in vec.iter_mut() {
*element *= 2;
}
println!("{:?}", vec);
使用 iter_mut 方法遍历和修改向量中的元素。
- 遍历字符串字符:
let s = "hello";
for c in s.chars() {
println!("{}", c);
}
使用 chars 方法遍历字符串中的字符。
- 遍历字节:
let s = "hello";
for b in s.bytes() {
println!("{}", b);
}
使用 bytes 方法遍历字符串中的字节。
- 遍历
Option类型:
let maybe_value = Some(42);
for value in maybe_value {
println!("{}", value);
}
如果 Option 是 Some,则 for 循环会遍历其中的值。
- 遍历
Result类型:
let result: Result<i32, &str> = Ok(42);
for value in result {
println!("{}", value);
}
如果 Result 是 Ok,则 for 循环会遍历其中的值。
- 结合
enumerate方法:
let arr = [10, 20, 30, 40, 50];
for (index, value) in arr.iter().enumerate() {
println!("Index: {}, Value: {}", index, value);
}
使用 enumerate 方法获取索引和值对。
- 使用
into_iter方法:
let vec = vec![1, 2, 3, 4, 5];
for element in vec.into_iter() {
println!("{}", element);
}
使用 into_iter 方法将向量所有权移动到迭代器中。
- 多重循环:
for i in 1..3 {
for j in 1..3 {
println!("i: {}, j: {}", i, j);
}
}
嵌套循环。
在 Rust 中,for 循环的范围 (range) 默认步长为 1,无法直接通过语法来控制步长。不过,你可以通过使用迭代器的 step_by 方法来控制步长。以下是一些示例:
- 使用
step_by控制步长:
for i in (0..10).step_by(2) {
println!("{}", i);
}
上面的代码将以步长 2 遍历范围,输出 0、2、4、6、8。
- 步长为负数(降序循环):
Rust 的 Range 类型不支持直接创建降序范围。你可以通过自定义迭代器来实现:
for i in (0..10).rev().step_by(2) {
println!("{}", i);
}
上面的代码将以步长 2 逆序遍历范围,输出 9、7、5、3、1。
- 自定义范围和步长:
你也可以使用 while 循环来更灵活地控制范围和步长:
let mut i = 0;
while i < 10 {
println!("{}", i);
i += 2;
}
上面的代码同样以步长 2 遍历范围,输出 0、2、4、6、8。
- 遍历数组或向量时使用步长:
let arr = [10, 20, 30, 40, 50, 60];
for (index, value) in arr.iter().enumerate().step_by(2) {
println!("Index: {}, Value: {}", index, value);
}
上面的代码以步长 2 遍历数组中的元素,输出 Index: 0, Value: 10 和 Index: 2, Value: 30 等。
这些方法可以让你在 Rust 中通过 for 循环控制步长。
其他
vscode 里的 Rust 插件真的是非常的智能。
cargo 的报错提示也非常的清晰,很优雅。