Rust 学习笔记 - 数据类型、函数、控制流

| 笔记 | 6292 | 16分钟 | 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 u128u128 最大能表示 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 中循环,loopwhileforloop 是其他语言没有的。

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 循环用法:

  1. 遍历范围:
for i in 0..5 {
    println!("{}", i);
}

上面的代码将输出 0 到 4。

  1. 遍历集合:
let arr = [10, 20, 30, 40, 50];

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

使用 iter 方法遍历数组。

  1. 遍历可变集合:
let mut vec = vec![1, 2, 3, 4, 5];

for element in vec.iter_mut() {
    *element *= 2;
}
println!("{:?}", vec);

使用 iter_mut 方法遍历和修改向量中的元素。

  1. 遍历字符串字符:
let s = "hello";

for c in s.chars() {
    println!("{}", c);
}

使用 chars 方法遍历字符串中的字符。

  1. 遍历字节:
let s = "hello";

for b in s.bytes() {
    println!("{}", b);
}

使用 bytes 方法遍历字符串中的字节。

  1. 遍历 Option 类型:
let maybe_value = Some(42);

for value in maybe_value {
    println!("{}", value);
}

如果 OptionSome,则 for 循环会遍历其中的值。

  1. 遍历 Result 类型:
let result: Result<i32, &str> = Ok(42);

for value in result {
    println!("{}", value);
}

如果 ResultOk,则 for 循环会遍历其中的值。

  1. 结合 enumerate 方法:
let arr = [10, 20, 30, 40, 50];

for (index, value) in arr.iter().enumerate() {
    println!("Index: {}, Value: {}", index, value);
}

使用 enumerate 方法获取索引和值对。

  1. 使用 into_iter 方法:
let vec = vec![1, 2, 3, 4, 5];

for element in vec.into_iter() {
    println!("{}", element);
}

使用 into_iter 方法将向量所有权移动到迭代器中。

  1. 多重循环:
for i in 1..3 {
    for j in 1..3 {
        println!("i: {}, j: {}", i, j);
    }
}

嵌套循环。

在 Rust 中,for 循环的范围 (range) 默认步长为 1,无法直接通过语法来控制步长。不过,你可以通过使用迭代器的 step_by 方法来控制步长。以下是一些示例:

  1. 使用 step_by 控制步长:
for i in (0..10).step_by(2) {
    println!("{}", i);
}

上面的代码将以步长 2 遍历范围,输出 0、2、4、6、8。

  1. 步长为负数(降序循环):

Rust 的 Range 类型不支持直接创建降序范围。你可以通过自定义迭代器来实现:

for i in (0..10).rev().step_by(2) {
    println!("{}", i);
}

上面的代码将以步长 2 逆序遍历范围,输出 9、7、5、3、1。

  1. 自定义范围和步长:

你也可以使用 while 循环来更灵活地控制范围和步长:

let mut i = 0;
while i < 10 {
    println!("{}", i);
    i += 2;
}

上面的代码同样以步长 2 遍历范围,输出 0、2、4、6、8。

  1. 遍历数组或向量时使用步长:
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 的报错提示也非常的清晰,很优雅。