参考书籍:《Rust权威指南》第2版 Steve Klabnik Carol Nichols 著 毛靖凯 译

一. Cargo

二. 变量、常量和隐藏

1. 变量

  • Rust 中的变量(let x = 2)默认是不可变的,这是其安全设计的重要部分。要声明一个可变变量,需在声明时加上 mut关键字,例如 let mut x = 5;。这使得变量 x的值在后续可以被修改。

  • 不可变是默认行为:这能防止值被意外更改导致的 Bug,尤其在并发场景下,并能使代码逻辑更清晰。

  • 显式可变性:通过 mut关键字显式声明可变性,是一种“深思熟虑的可变性”,在提供灵活性的同时,维持了代码的安全性与清晰意图。

2. 常量

  • 声明与不可变性:常量必须用 const关键字声明,且必须显式标注类型。它不仅在默认状态下不可变,而且是永远不可变的,因此不允许使用 mut关键字。

  • 作用域:常量可以被声明在包括全局作用域在内的任何作用域,这便于程序的不同部分共享数据。

  • 值的约束:常量只能绑定到一个在编译时就能确定的常量表达式,不能绑定函数返回值或其他需要在运行时计算的值。

  • 命名约定:Rust 的命名约定是使用全大写字母,并用下划线分隔单词来命名常量。

  • 例如,const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;就是一个常量声明。编译器会在编译时计算出表达式 60 * 60 * 3的结果。将程序中的硬编码值定义为常量,能提高代码的可读性与可维护性,便于后续的理解和统一修改。

3. 隐藏

  • 变量遮蔽是 Rust 中的一项特性,它允许你通过再次使用 let关键字来重新声明一个已有的同名变量,从而创建全新的变量实例。这个新变量会“遮蔽”前一个,后续对变量名的引用都将指向最新的声明。

  • 变量遮蔽与将变量声明为 mut有本质区别。变量遮蔽能让你在保持原变量不可变性的同时,在后续重新绑定一个完全不同类型的新值。例如,你可以先用变量名 spaces存储一个字符串,之后在另一行用同名 spaces存储这个字符串的长度(一个数值)。

  • 而如果声明为可变变量(mut),虽然其值可变,但类型一旦确定就无法更改。尝试为可变变量赋一个不同类型的值(例如,从字符串改为整数)会导致编译错误,因为 Rust 是静态类型语言。

三. 数据类型

  • Rust 是静态类型语言,这意味着它在编译时需要知道所有变量的具体类型。编译器在多数情况下能根据绑定和使用方式自动推导类型,但在特定场景下则必须显式标注类型。

  • 例如,当使用 parse()方法将 String类型(如 "42")转换为数值类型时,编译器无法自动推断目标数值类型(如 i32u32),必须像 let guess: u32 = ...一样给出明确标注。如果省略标注,编译时将提示“type annotations needed”错误,并建议为变量指定一个类型。后续我们将介绍不同数据类型的标注方式。

1. 标量类型

  • Rust 中的标量类型是代表单个值的类型统称。其内置了四种基础标量类型:整数、浮点数、布尔值和字符。

①整数

  • Rust 的整数类型分为有符号(以 i开头,如 i8i32)和无符号(以 u开头,如 u8u32)两大类。有符号整数可表示负数,无符号整数则只能表示非负数。每种类型都有明确的位长度(如8、16、32、64、128位),以及两种平台相关类型:isizeusize,其长度(32位或64位)由程序运行的计算机架构决定。一个 n位的有符号整数(如 i8)可表示的范围是从 -(2ⁿ⁻¹) 到 2ⁿ⁻¹ - 1,而无符号整数(如 u8)的表示范围则是从 0 到 2ⁿ - 1。

  • 在代码中书写整数字面量时,可以添加类型后缀(如 57u8)并使用下划线 _作为分隔符以提高可读性(如 1_000)。如果无法确定使用哪种类型,i32通常是良好的默认选择,因为它在多数情况下运算速度最快。isizeusize则主要用于集合的索引。

  • 当尝试为整数变量赋予一个超出其类型表示范围的值时,就会发生整数溢出。Rust 对此的处理方式取决于编译模式:在调试(debug)模式下,溢出会触发程序 panic(即因错误而终止);在发布(release)模式下,则会执行二进制补码环绕,即超出最大值的数值会“环绕”到该类型的最小值重新开始(例如,对于 u8类型,256 会变为 0)。

  • 如果需要显式处理溢出,标准库为数值类型提供了多种方法:wrapping_*系列方法会执行环绕操作;checked_*方法在溢出时返回 Noneoverflowing_*方法会返回结果和一个指示是否溢出的布尔值;saturating_*方法则会将结果限制在该类型可表示的最小值或最大值。

②浮点数

  • Rust 提供了两种浮点数(即带小数的数字)类型:f32(单精度,占 32 位)和 f64(双精度,占 64 位)。在现代 CPU 上,两者的运算效率相近,但由于 f64拥有更高的精度,Rust 默认会将浮点数字面量(如 2.0)的类型推导为 f64。这两种类型均遵循 IEEE-754​ 标准。

③数值运算

  • Rust 支持所有数值类型进行加、减、乘、除和取余这五种基本数学运算。整数除法有一个重要特性:它会向零截断,即直接舍弃小数部分。例如,-5 / 3的结果是 -1,而不是向下取整的 -2

  • 在代码中,每个使用运算符的表达式都会计算出结果,然后被绑定到赋值语句(let)左侧的变量上。Rust 所支持的所有运算符可在附录 B 中查阅。

④布尔类型

  • Rust 的布尔类型(bool)只有两个值:truefalse,占用 1 个字节内存。在声明变量时,可使用 bool关键字进行显式类型标注。它最主要的用途是作为 if表达式中的条件判断依据。

⑤字符类型

  • Rust 的 char类型用于表示单个字符,占 4 个字节,是一个 Unicode 标量值。它可以表示字母(如 'z')、数字、象形文字(如中文),甚至表情符号(如 '😻')。在代码中,char字面量使用单引号定义,这与使用双引号的字符串字面量不同。

  • 需要特别注意的是,由于 Unicode 中并没有明确的“字符”概念,一个我们直觉上的“字形”(例如一个带重音符号的字母)在 Rust 中可能由多个 char值组合表示。这个主题将在后续关于字符串的章节中详细讨论。

2. 复合类型

  • 复合类型能将多个不同类型的值组合成一个类型。Rust 内置了两种基础的复合类型:元组 (tuple) 和数组 (array)。

①元组

  • Rust 中的元组是一种固定长度的复合类型,可用于组合多个不同类型的值。

// src/main.rs
// 元组是一种复合类型,可组合不同类型的值,长度固定。

// 示例1:创建元组,并可选择性地添加类型标注
fn main() {
    // 创建元组,包含i32、f64、u8三种类型的值
    let tup: (i32, f64, u8) = (500, 6.4, 1);

    // 示例2:通过模式匹配(解构)获取元组中的单个值
    let (x, y, z) = tup; // 将tup解构为x、y、z三个变量
    println!("The value of y is: {y}"); // 输出y的值:6.4

    // 示例3:通过索引(点号.)访问元组元素,索引从0开始
    let five_hundred = tup.0; // 访问第一个元素
    let six_point_four = tup.1; // 访问第二个元素
    let one = tup.2; // 访问第三个元素
    println!("Access by index: {}, {}, {}", five_hundred, six_point_four, one);

    // 示例4:单元元组,表示为(),用于空值或空的返回类型
    let unit: () = (); // 显式声明单元元组
    println!("Unit tuple: {:?}", unit); // 输出: ()

    // 表达式无返回值时隐式返回单元元组
    let implicit_unit = {
        let _ = 5; // 无返回值的表达式
    };
    println!("Implicit unit: {:?}", implicit_unit); // 输出: ()
}

②数组

// src/main.rs
// Rust数组是一种存储在栈上、长度固定、元素类型相同的集合。
fn main() {
    // 基本数组声明
    let a = [1, 2, 3, 4, 5]; // 类型推导为[i32; 5],包含5个i32类型元素

    // 数组类型标注格式:[元素类型; 元素数量]
    let b: [i32; 5] = [1, 2, 3, 4, 5]; // 显式指定类型

    // 简化初始化:创建所有元素值相同的数组
    let c = [3; 5]; // 等价于[3, 3, 3, 3, 3],类型为[i32; 5]

    // 示例:存储一年12个月份名称
    let months = ["January", "February", "March", "April", "May", "June",
                  "July", "August", "September", "October", "November", "December"];

    // 访问数组元素:通过索引(从0开始)
    let first = months[0]; // "January"
    let second = months[1]; // "February"
    
    println!("First month: {}", first);
    println!("Array a: {:?}", a);
    println!("Array c: {:?}", c);
}
// src/main.rs
// 演示尝试通过用户输入索引访问数组元素,并说明Rust对越界访问的运行时检查与安全处理。

use std::io; // 引入标准输入/输出库

fn main() {
    // 定义一个包含5个元素的数组
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    // 创建一个可变String来存储用户输入
    let mut index = String::new();

    // 从标准输入读取一行
    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    // 将输入字符串转换为 usize 类型(用于数组索引)
    let index: usize = index
        .trim()      // 去除首尾空白字符
        .parse()     // 解析为数值
        .expect("Index entered was not a number"); // 若非数字则报错

    // 【核心】尝试用用户输入的索引访问数组元素
    // 如果输入的索引在数组边界内(0到4),则能安全获取到该元素。
    // 如果索引 >= 数组长度(本例为5),程序会在**运行时** panic,
    // 并给出明确错误信息:"index out of bounds: the len is 5 but the index is ..."
    // 这体现了Rust的安全原则:立即中断程序,防止非法内存访问。
    let element = a[index];

    // 只有索引合法时,才会执行到这一行并打印结果
    println!("The value of the element at index {index} is: {element}");
}

// === 运行结果说明(对应图片内容) ===
// 1. 输入合法索引(0-4):程序正常输出对应元素值。
// 2. 输入越界索引(如10):程序 panic,输出错误信息并终止,最后的 println! 不会被执行。
//
// 错误示例:
// thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
//
// 这与其他一些底层语言(如C/C++)形成对比,后者在越界访问时往往会导致未定义行为。
// Rust 通过在运行时检查数组边界,确保了内存安全,即使这会以程序 panic 为代价。
// 第9章将讨论如何通过更灵活的错误处理来避免 panic,并编写更健壮的代码。

四. 函数

五. 控制流

六. 其他

1. 注释