注意:本教程面向于有语言基础的人员,重点对一些陌生概念和一些差距大的语法进行解释。
关联知识介绍
Result 枚举类型:用于表示一个操作可能成功或失败的情况,一般用于错误处理。
enum Result<T, E> {
Ok(T), // 操作成功,返回结果
Err(E), // 操作失败,返回错误
}
Option 枚举类型:用于表示一个值可能存在或不存在的情况,一般用于越界行为判断。
enum Option<T> {
Some(T), // 值存在
None, // 值不存在
}
所有权:所有权是 Rust 内存管理的核心机制,它确保每个值只有一个所有者,可以通过赋值、函数参数传递或返回值转移所有权(原来的所有者会被销毁)。
借用:为了避免所有权转移,Rust 提供了借用机制。通过 &T 借用值,允许读取但不能修改,通过 &mut T 借用值,允许读取和修改。
数据绑定
变量和常量
- 不可变变量:
let a = 123;,值一旦定义后不可更改 - 可变变量:
let mut a = 123;,允许重新赋值 - 静态变量:
static [mut] a:u32 = 123;,在整个程序生命周期内可用,必须指定类型(可使用mut关键字设为可变,但不推荐) - 常量:
const XXX:u32 = 100;,全局可用且不可修改,必须指定类型
特性:Shadowing
-
允许新的变量遮蔽旧的变量,但旧的变量仍然存在。通常用于需要对旧变量进行处理的场景。这样子不用修改变量名,并且避免使用mut来修改变量,可以使得代码更加清晰。
fn main() { let input = "42"; { let input: u32 = input.parse().expect("Not a number!"); // 将字符串转换为整数 println!("内部 input = {}", input); } // 内部 x 离开作用域 println!("外部 input = {}", input); // 仍然可以访问外部的 input }
问题1:不可变变量、常量和静态变量有什么区别?
- 不可变变量(
let)是局部作用域内的不可变值,主要用于临时值或运行时计算的不可变数据。 - 常量(
const)是编译时确定的全局不可变值,没有固定内存地址,适用于配置项或数学常数等全局常量。 - 静态变量(
static)是具有固定内存地址的全局变量,适用于全局共享状态或单例模式。
问题2:Rust 中的变量是否必须初始化?为什么?
- 在 C/C++ 中可以使用未初始化变量,但 Rust 要求变量使用前必须初始化,否则编译器会报错。
- 原因:Rust 以内存安全为核心目标,使用未初始化变量可能导致程序读取随机内存数据。
数据类型
除了基本类型和复合类型,Rust还有自定义类型(结构体、枚举)和高级类型(泛型、Trait),这些后面会讲到。
基本类型
- 整数类型:i[8/16/32/64/128/size],u[8/16/32/64/128/size]
- 浮点类型:f32,f64(默认)
- 布尔类型:bool(true/false)
- 字符类型:char
复合类型
- 元组:支持不同值类型的组合,声明后不可更改。使用点标记法访问元素(如 tup.1、tup.2)
- 数组:固定长度且类型相同的集合,存储在栈内存中。使用中括号访问元素(如 arr[1]、arr[2])
// 元组
let tup: (i32, f64, char) = (500, 6.4, 'A');
let (x, y, z) = tup; // 解构元组
// 数组
let arr:[i32;5] = [1,2,3,4,5] // i32类型,长度为5
let arr = [3;5] // 创建长度为5且元素均为3的数组:[3,3,3,3,3]
arr[0] // 3
问题1:Rust 中的浮点数是否支持精确比较?为什么?
-
不支持精确比较。原因有下:
- 浮点数在计算机中是以二进制形式存储的,无法精确表示某些十进制小数(如 0.1)。
- 浮点数的精度有限,计算过程中可能会引入舍入误差。
- 浮点数中有特殊值
NaN(Not a Number)和无穷大(Infinity),它们的行为不符合常规的数学规则。
-
解决方法:判断允许一定的误差范围、使用
float-cmp库。
问题2:如何将一个 f64 类型的值四舍五入为 i32 类型?
-
可以使用Rust标准库中的
round方法,然后使用as关键字将其转化成i32类型let x: f64 = 3.7; let rounded_x = x.round() as i32; // 4
问题3:Rust 中的数组是否支持越界访问?如何避免越界?
-
不支持。如果越界在编译时可能会报错(索引为动态计算时不会报错),在运行时一定会panic。在C/C++中,则一切正常,可能会导致不可预料的BUG。
-
如何避免越界:使用
get/get_mut方法,会返回一个Option<T>枚举类型{Some,None},如果越界则返回None。除此之外还可以使用动态数组Vec,这个后面会讲到。let arr = [1, 2, 3]; let index = 3; match arr.get(index) { Some(&x) => println!("x:{}", x), None => println!("越界了!"), }
问题4:如何将一个 i32 类型的数组转换为 f64 类型的数组?
-
使用
map结合lambda表达式let i32_array: [i32; 5] = [1, 2, 3, 4, 5]; let f64_array: [f64; 5] = i32_array.map(|x| x as f64); println!("i32 array: {:?}", i32_array); println!("f64 array: {:?}", f64_array);
表达式
分支表达式
if 表达式
-
根据条件选择执行不同的代码块,并可以返回值。
let condition = true; let x = if condition { 5 } else { 6 }; println!("x = {}", x); // 输出: x = 5
match 表达式
-
用于匹配一个值,并根据匹配的模式执行相应的代码块。
let coin = "Penny"; let value = match coin { "Penny" => 1, "Nickel" => 5, "Dime" => 10, "Quarter" => 25, _ => 0, // 通配符,匹配其他情况 }; println!("The value is {}", value); // 输出: The value is 1 -
守卫条件:
match支持在模式匹配中添加守卫条件(if条件),以进一步过滤匹配let value = 7; match value { x if x < 0 => println!("Negative: {}", x), // 守卫条件:x < 0 x if x == 0 => println!("Zero"), // 守卫条件:x == 0 x if x % 2 == 0 => println!("Even: {}", x), // 守卫条件:x 是偶数 x => println!("Odd: {}", x), // 默认情况:x 是奇数 } -
可以匹配的类型:基本类型、复合类型、自定义类型(枚举、结构体)、引用和指针
-
如果
if表达式使用了多于两个else if,推荐使用match来替代。 -
match表达式必须覆盖所有可能的情况(穷尽性检查)。 -
每个分支的代码块可以是表达式,返回值类型必须一致。
if let 表达式
-
if let是match的简化形式,用于匹配单个模式并执行相应的代码块(它也可以匹配多个模式,但不建议,多个模式应该用match匹配)。通常用于处理Option类型和Result类型的某个变体(这两个类型后面会讲)// 当你只需要处理Result类型的 Ok 变体时,if let 可以简化代码。 let file = std::fs::File::open("example.txt"); if let Ok(f) = file { println!("File opened successfully"); } else { println!("Failed to open file"); }
问题1:Rust 的 match 表达式在性能上有什么特点?它是如何实现的?
- Rust 的
match表达式在性能上非常高效,通常会被编译为跳转表或条件判断 - 具体实现:对于枚举类型使用标签匹配,对于整数类型使用跳转表或二分查找来加速匹配。编译器会进行优化,尽可能生成高效的机器代码。
问题2:Rust 中的 if 是表达式还是语句?它与传统语言(如 C 或 Java)中的 if 有什么不同?
- 在 Rust 中,
if是表达式,而不是语句。这意味着它可以返回值。 - 与传统语言不同之处:Rust 的
if表达式必须返回相同类型的值,且所有分支的返回值类型必须一致。
循环表达式
loop 表达式
-
无限循环,直到显式使用
break退出。fn main() { let mut count = 0; let result = loop { count += 1; if count == 3 { break count * 2; // 退出循环并返回值(break后面可以跟返回值) } }; println!("Result: {}", result); // 输出: Result: 6 }
while 表达式
-
条件循环,当条件为
true时重复执行代码块。fn main() { let mut count = 0; while count < 3 { println!("Count: {}", count); count += 1; } }
while let 表达式
-
在模式匹配成功时重复执行代码块
fn main() { let mut stack = vec![1, 2, 3]; while let Some(top) = stack.pop() { println!("Popped: {}", top); } }
for..in 表达式
-
用于遍历集合(如数组、范围、迭代器)中的元素
fn main() { let arr = [10, 20, 30]; for value in arr.iter() { println!("Value: {}", value); } for i in 0..3 { // 遍历范围 println!("Index: {}", i); } }
问题1:在 Rust 的循环中,所有权是如何转移的?如何避免不必要的所有权转移?
-
在
for循环中,迭代器会逐个提供元素的引用或值。如果迭代器提供的是值,那么值的所有权会转移到循环体中。let v = vec![1, 2, 3]; for i in v { println!("{}", i); } // v 在这里不可用,因为所有权已经转移 -
在
while和loop循环中,变量的所有权取决于变量的使用方式。如果变量被移动到循环体中,那么所有权会转移。let mut x = 5; while x > 0 { println!("{}", x); x -= 1; } // x 在这里仍然可用 -
避免所有权转移:使用借用
&xx、使用迭代器方法iter/iter_mut、使用clone// 使用借用 let v = vec![1, 2, 3]; for i in &v { println!("{}", i); } // v 在这里仍然可用 // 使用迭代器方法 let mut v = vec![1, 2, 3]; for i in v.iter_mut() { *i += 1; } // v 在这里仍然可用 // 使用clone let v = vec![1, 2, 3]; for i in v.clone() { println!("{}", i); } // v 在这里仍然可用
函数
-
函数定义:使用snackcase命名规范,需要指定参数类型和返回值类型,支持多返回值。返回可以直接通过表达式返回(隐式返回)也可以通过
return返回(显式返回)fn plus_and_minus(x:i32,y:i32) -> (i32, i32) { (x + y , x - y) // 隐式返回不需要分号,因为它是一个表达式 } -
Rust 支持将函数作为参数传递、作为返回值返回,支持高阶函数和函数式编程
fn apply_function(f: fn(i32) -> i32, x: i32) -> i32 { f(x) } -
Rust支持匿名函数(也称为闭包)
let add = |x:i32,y:i32| -> { x + y }; // 简写版 let add = |x,y| x+y; -
Rust函数可以在声明之前调用,不像C/C++需要提前声明
问题1:Rust 中的函数和闭包有什么区别?闭包的优缺点?
- Rust 中的函数是命名的代码块、不可以捕获环境变量,而闭包是匿名的、可以捕获环境变量。
- 闭包的优点在于其灵活性,可以捕获和使用外部变量,支持多种调用方式,并且可以作为参数传递
- 闭包的缺点在于可能引入额外的性能开销(如堆分配)和复杂性(如生命周期和所有权问题),尤其是在捕获可变引用或所有权时。
问题2:Rust 函数传递参数根据传递特性分为哪几类?有什么区别?
-
三类:所有权传递、不可变借用传递、可变借用传递
// 所有权传递 fn take_ownership(s: String) { println!("{}", s); } fn main() { let s = String::from("hello"); take_ownership(s); // 所有权转移给函数 // println!("{}", s); // 错误:s 已失效 } // 不可变借用传递 fn borrow_mutable(s: &mut String) { s.push_str(" world"); } fn main() { let mut s = String::from("hello"); borrow_mutable(&mut s); // 可变借用 println!("{}", s); // 输出: hello world } // 可变借用传递 fn borrow_mutable(s: &mut String) { s.push_str(" world"); } fn main() { let mut s = String::from("hello"); borrow_mutable(&mut s); // 可变借用 println!("{}", s); // 输出: hello world } -
所有权传递:参数通过值传递,函数获得所有权,调用后原始变量失效,适合需要获取所有权的情况。
-
不可变借用:参数通过不可变引用传递,函数只能读取数据,允许多个引用同时存在,原始变量仍然有效。
-
可变借用:参数通过可变引用传递,函数可以修改数据,但只能有一个可变引用,原始变量仍然有效。