(三)Rust语法基础

259 阅读10分钟

注意:本教程面向于有语言基础的人员,重点对一些陌生概念和一些差距大的语法进行解释。

关联知识介绍

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 在这里不可用,因为所有权已经转移
    
  • whileloop 循环中,变量的所有权取决于变量的使用方式。如果变量被移动到循环体中,那么所有权会转移。

    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
    }
    
  • 所有权传递:参数通过值传递,函数获得所有权,调用后原始变量失效,适合需要获取所有权的情况。

  • 不可变借用:参数通过不可变引用传递,函数只能读取数据,允许多个引用同时存在,原始变量仍然有效。

  • 可变借用:参数通过可变引用传递,函数可以修改数据,但只能有一个可变引用,原始变量仍然有效。