参考书籍:《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")转换为数值类型时,编译器无法自动推断目标数值类型(如i32或u32),必须像let guess: u32 = ...一样给出明确标注。如果省略标注,编译时将提示“type annotations needed”错误,并建议为变量指定一个类型。后续我们将介绍不同数据类型的标注方式。
1. 标量类型
Rust 中的标量类型是代表单个值的类型统称。其内置了四种基础标量类型:整数、浮点数、布尔值和字符。
①整数


Rust 的整数类型分为有符号(以
i开头,如i8、i32)和无符号(以u开头,如u8、u32)两大类。有符号整数可表示负数,无符号整数则只能表示非负数。每种类型都有明确的位长度(如8、16、32、64、128位),以及两种平台相关类型:isize和usize,其长度(32位或64位)由程序运行的计算机架构决定。一个n位的有符号整数(如i8)可表示的范围是从 -(2ⁿ⁻¹) 到 2ⁿ⁻¹ - 1,而无符号整数(如u8)的表示范围则是从 0 到 2ⁿ - 1。在代码中书写整数字面量时,可以添加类型后缀(如
57u8)并使用下划线_作为分隔符以提高可读性(如1_000)。如果无法确定使用哪种类型,i32通常是良好的默认选择,因为它在多数情况下运算速度最快。isize和usize则主要用于集合的索引。当尝试为整数变量赋予一个超出其类型表示范围的值时,就会发生整数溢出。Rust 对此的处理方式取决于编译模式:在调试(debug)模式下,溢出会触发程序 panic(即因错误而终止);在发布(release)模式下,则会执行二进制补码环绕,即超出最大值的数值会“环绕”到该类型的最小值重新开始(例如,对于
u8类型,256 会变为 0)。如果需要显式处理溢出,标准库为数值类型提供了多种方法:
wrapping_*系列方法会执行环绕操作;checked_*方法在溢出时返回None;overflowing_*方法会返回结果和一个指示是否溢出的布尔值;saturating_*方法则会将结果限制在该类型可表示的最小值或最大值。
②浮点数
Rust 提供了两种浮点数(即带小数的数字)类型:f32(单精度,占 32 位)和 f64(双精度,占 64 位)。在现代 CPU 上,两者的运算效率相近,但由于
f64拥有更高的精度,Rust 默认会将浮点数字面量(如2.0)的类型推导为f64。这两种类型均遵循 IEEE-754 标准。
③数值运算
Rust 支持所有数值类型进行加、减、乘、除和取余这五种基本数学运算。整数除法有一个重要特性:它会向零截断,即直接舍弃小数部分。例如,
-5 / 3的结果是-1,而不是向下取整的-2。在代码中,每个使用运算符的表达式都会计算出结果,然后被绑定到赋值语句(
let)左侧的变量上。Rust 所支持的所有运算符可在附录 B 中查阅。
④布尔类型
Rust 的布尔类型(
bool)只有两个值:true和false,占用 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,并编写更健壮的代码。