Rust:变量、常量与数据类型
变量绑定与可变性
在大部分编程语言中,我们说"声明变量",但在Rust中,我们更准确地说是变量绑定。这种说法更能体现Rust的核心思想:将值绑定到一个标识符上。
在Rust中,变量名本身并不拥有值,值可能在栈上也可能在堆上,let
做的是把这个名字与一个具体的值绑定在一起。之所以默认不可变,是为了让并发和优化更容易:当编译器知道某个绑定不会被改写时,就能更大胆地做优化,也能在并发场景下避免数据竞争。
rust
let x = 5; // 将值5绑定到标识符x上
println!("The value of x is: {}", x);
不可变性
Rust中的变量默认是不可变的(immutable),这是Rust安全性和并发性的基础之一。
不可变并不是说值永远不能变化,而是说这个名字一旦绑定到某个值后,就不能再指向别的值。这样可以避免"谁在何时修改了状态"的隐蔽错误,阅读和调试也更直观。
rust
let x = 5;
x = 6; // 错误:cannot assign twice to immutable variable
以上代码会报错,因为 x
默认是不可变的。
可变性
当确实需要修改变量的值时,可以使用mut
关键字:
把 mut
放在变量名之前,表示允许对这个绑定指向的内存位置进行原地修改。可变会增加负担和并发风险,优先使用不可变,只有在确实需要原地更新(如计数器、缓冲区写入)时再使用可变。
rust
let mut x = 5;
x = 6; // 现在可以修改了
mut
关键字必须在变量绑定时就声明,不能后续添加
变量遮蔽
Rust允许用相同的名字声明新变量,新变量会遮蔽(shadow)之前的变量:
遮蔽是重新绑定,不是修改原变量。它常用在更小的作用域里用计算后的新值而不污染外层作用域。与可变不同,遮蔽可以改变类型。
rust
let x = 5;
let x = x + 1; // 遮蔽前一个x,创建新变量
println!("{}", x); // 6
{
let x = x * 2; // 在内部作用域中遮蔽
println!("{}", x); // 12
}
println!("{}", x); // 6,外层作用域的x
- 遮蔽:创建新变量,可以改变类型
- 可变性:修改同一变量的值,类型不能改变
rust
// 遮蔽:可以改变类型
let spaces = " "; // &str 类型
let spaces = 10; // usize 类型
// 可变性:不能改变类型
let mut spaces = " ";
spaces = 10; // 错误:类型不匹配
遮蔽 vs 可变性的区别:
特性 | 遮蔽(Shadowing) | 可变性(Mutability) |
---|---|---|
机制 | 创建新变量 | 修改同一变量 |
类型 | 可以改变 | 不能改变 |
内存 | 新分配 | 原地修改 |
作用域 | 受作用域影响 | 不受影响 |
变量初始化机制
初始化一个变量与大多数语言一致,在声明时通过赋值 =
给变量一个初始值。
rust
let x = 1;
延迟初始化
Rust
还允许用户延迟初始化一个变量。延迟初始化常用于先声明、后在分支或计算结果确定后再赋值的场景。
rust
let x; // 只声明,不初始化
x = 42; // 稍后初始化
println!("x = {}", x); // 合法
- 即使变量是不可变的 (没有
mut
),延迟初始化仍然合法 - 变量的不可变性 指的是初始化后不能重新赋值
简单区别一下三个概念:
概念 | 说明 | 可变变量 | 不可变变量 |
---|---|---|---|
声明 | 告诉编译器变量存在 | ✅ | ✅ |
初始化 | 第一次赋值 | ✅ | ✅ |
重赋值 | 修改已初始化的值 | ✅ | ❌ |
rust
// 不可变变量的延迟初始化
let x; // 声明
x = 10; // 初始化
x = 20; // 重赋值 ❌ 不可变变量不允许
// 可变变量的延迟初始化
let mut y; // 声明
y = 10; // 初始化 ✅
y = 20; // 重赋值 ✅ 可变变量允许
编译时安全保证
Rust编译器确保变量在使用前必须被初始化:
rust
let y;
println!("y = {}", y); // 编译错误:use of possibly-uninitialized variable
y = 1;
println!("y = {}", y); // 初始化完后才能使用
此外,还要保证每一个分支都有初始化结果,并且各个分支的初始化类型一致:
rust
let y;
if some_condition {
y = 10;
} else {
y = 20;
}
println!("y = {}", y);
此处不论走哪一个 if
分支,都可以确保使用 y
之前被正确初始化。
类型系统与类型标注
Rust是静态类型语言,编译时必须知道所有变量的类型。
静态类型不等于"处处手写类型"。Rust 借助局部类型推断让代码保持简洁,同时把类型错误尽早暴露到编译期。相比动态类型,静态类型能在大型代码库中降低运行时错误;相比一些拥有运行时反射的语言,Rust 的类型系统强调零成本抽象。
Rust
提供了两种机制:
- 类型推断:编译器智能推断类型
- 显式标注:程序员明确指定类型
类型推断
Rust 的推断是局部的:不会跨函数、跨模块进行全局推断;当上下文不足时,必须显式标注类型或使用类型后缀。
编译器根据值 和使用上下文推断类型:
rust
let x = 5; // 推断为 i32(整数默认类型)
let y = 3.14; // 推断为 f64(浮点数默认类型)
let z = true; // 推断为 bool
let s = "hello"; // 推断为 &str
// 延迟初始化时的类型推断
let number; // 类型未知
number = 42; // 推断为 i32
let data; // 类型未知
data = String::from("hello"); // 推断为 String
这种推断语法,可以大部分类型简单的场景下简化编码,不用为每一个变量指明类型。
显式类型标注语法
rust
let 变量名: 类型 = 值;
当编译器无法推断或存在歧义时,必须使用显式类型标注:
rust
let x: i32 = 5; // 明确指定为 i32
let y: f32 = 3.14; // 明确指定为 f32
常量与静态变量
常量
常量在程序运行期间值永远不变,使用 const
声明:
rust
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159;
const MESSAGE: &str = "Hello";
常量必须进行类型标注,指明类型。
与 let
的区别:常量必须是编译期可求值的表达式,通常会被内联到使用处,没有固定内存地址;可在任意作用域声明(包括全局),且总是不可变。
rust
let mut num = 1;
const NUM: i32 = num; // 错误
比如以上代码就是错误的,因为常量 NUM
依赖了一个变量 num
,变量在运行时才能确定值,导致 NUM
无法在编译期得到确定值,因此报错。
静态变量
静态变量具有'static
生命周期,在程序整个运行期间有效:
rust
static GLOBAL_COUNT: i32 = 0;
static mut COUNTER: i32 = 0;
static
具有固定内存地址,可通过引用取地址。static
也要求必须进行类型标注。
标量类型
整数类型
Rust提供了丰富的整数类型,每种都明确指定位数和符号性:
长度 | 有符号 | 无符号 | 范围 |
---|---|---|---|
8-bit | i8 |
u8 |
-128~127 / 0~255 |
16-bit | i16 |
u16 |
-32,768~32,767 / 0~65,535 |
32-bit | i32 |
u32 |
约±21亿 / 0~43亿 |
64-bit | i64 |
u64 |
约±922万万亿 |
128-bit | i128 |
u128 |
超大范围 |
arch | isize |
usize |
取决于架构(32/64位) |
对于 isize
和 usize
,它们占用的比特位取决于系统架构,系统是多少位,就占用多少位。
整数字面量与后缀
前缀
Rust
允许通过前缀不同,来决定一个字面量的进制。
- 无前缀:十进制(如
42
) 0x
:十六进制(如0x2A
== 42)0o
:八进制(如0o52
== 42)0b
:二进制(如0b101010
== 42)- 大小写均可:
0xFF
与0xff
等价
rust
// 进制表示
let decimal = 98; // 十进制
let hex = 0xff; // 十六进制
let octal = 0o77; // 八进制
let binary = 0b111; // 二进制
下划线分隔符:
对于一个数值,允许通过 _
进行分割,仅用于提升可读性,没有数值语义影响。例如 1_000_000
与 1000000
完全相同;二进制/十六进制中也可用来分组位。
字节字面量
rust
let byte = b'A'; // 字节字面量(u8)
字节字面量 b'A'
:表示一个 u8
(0~255)的字节值,必须是 ASCII 范围内的单字符。示例:b'A' == 65u8
。非 ASCII 字符(如 b'中'
、b'国'
)是非法的。
类型后缀
对于整数,可以通过修改后缀来决定字面量的类型:
rust
// 类型后缀
let typed = 123i64; // i64 类型
let unsigned = 456u32; // u32 类型
let long_num = 789i128; // i128 类型
类型后缀把"字面量本身"的类型在语法层面固定为某个具体整型。它发生在编译期,只影响该字面量节点的静态类型。
默认:整数字面量默认 i32
,除非通过上下文或后缀指定为其他类型。
整数溢出
Rust
的程序构建时,分为两种模式,debug
调试构建 和 release
发布构建。调
在 debug
模式下,会有更多的调试信息输出,一般用于开发环境。而 release
下会对代码进行更大幅度的优化,运行效率更高,一般用语正式发布环境。
当一个整数发生溢出的时候,在两个环境下效果也不同。
在 debug
模式下,如果整数溢出,此时会直接报错,在 Rust
中称为 panic
。这会导致整个程序直接终止。
但是在 release
下,如果溢出会发生环绕。
比如:
rust
let mut x: u8 = 255;
x += 1;
println!("x: {}", x);
这段代码在 debug
模式下直接报错退出,但是在 release
下输出 x: 0
。因为 255 已经是 u8
的最大值了,当再加一个数,就会重新变回最小的值,这个过程称为环绕。
其实环绕大部分情况下是一个非常危险的操作,在开发的时候,Rust
倾向于直接把这个行为作为一个错误报告给开发者,让开发者可以修改代码逻辑,或者更改更大的类型。
但是在实际发布环境,程序崩溃往往会给用户带来不好的体验,那么Rust
就不再把它当做一个错误处理了。
浮点数类型
Rust有两种浮点数类型:f32
(单精度)和f64
(双精度),它们都遵从 IEEE-754 标准。
rust
let x = 2.0; // f64(默认,推荐)
let y: f32 = 3.0; // f32(显式标注)
// 科学记数法
let large = 1e6; // 1,000,000.0 (f64)
let small = 1e-6; // 0.000001 (f64)
一个浮点数类型的字面量默认为 f64
,与现代CPU性能相近但精度更高。
布尔类型
布尔类型只表示两种真值,不与数字互转,这能防止很多隐式转换陷阱(例如把 0 误当做 false)。与控制流(if
/while
)结合时,编译器要求条件表达式必须是 bool
,从而让代码更清晰、更安全。
rust
let t = true; // bool类型
let f: bool = false; // 显式标注
// 布尔运算
let and_result = t && f; // false
let or_result = t || f; // true
let not_result = !t; // false
特点 :占用1字节,只能是true
或false
,不能与数字隐式转换。
字符类型
char
表示一个 Unicode 标量值,它可以存储英文字母,中文字符,拉丁文等等。Rust的char类型占4字节,使用单引号''
来声明。
rust
let c = 'z'; // ASCII字符
let unicode = 'ℤ'; // Unicode字符
let chinese = '中'; // 中文
复合类型
元组类型
元组可以存储多个不同类型的值,长度固定,元组适合把少量相关但类型不同的数据打包传递(如函数返回多个值),使用小括号()
进行定义。
rust
let tup: (i32, f64, u8) = (500, 6.4, 1);
一个元组的类型为 (type1, type2, ...)
,其类型也可以通过编译器自行推断,大部分时候不用手写类型标注。
访问元组的元素通过下标进行访问,从0
开始:
rust
let first = tup.0;
let second = tup.1;
let third = tup.2;
也可以通过解构赋值,把元组的元素直接赋值到变量上:
rust
let a;
let b;
let c;
(a, b, c) = tup;
let (x, y, z) = tup;
这样元组内的元素就会自动赋值到 abc
和 xyz
上。
Rust
还允许空元组:
rust
let unit: () = ();
空元组也称为单元类型
,其类型为 ()
值也为 ()
,可以理解为一个空值。如果一个函数没有返回值,默认就返回这个单元类型。
数组类型
数组用于存储相同类型的多个值,长度固定,适合在编译期已知大小的场景,使用方括号[]
进行定义。
rust
let arr: [i32; 5] = [1, 2, 3, 4, 5];
一个数组的类型写作 [T; N]
,其中 T
是元素类型,N
是长度。大多数情况下类型可由上下文推断,无需显式标注。
访问数组的元素通过下标进行访问,从0
开始:
rust
let first = arr[0];
let len = arr.len();
也可以通过解构赋值,把数组的元素直接赋值到变量上:
rust
let [a, b, c, d, e] = arr;
Rust会在运行时检查数组边界,越界访问会panic
类型转换
Rust要求显式类型转换,不允许进行隐式转换,使用 as
关键字。
整数间转换
- 扩大转换(如 i32 → i64):安全无数据丢失
- 缩小转换(如 i32 → u8):高位截断(取模运算)
rust
let num: i32 = 300;
let safe: i64 = num as i64; // 安全扩大
let truncated: u8 = num as u8; // 44 (300 % 256)
浮点转整数
- 直接丢弃小数部分(向零截断)
- 浮点值超出目标整数范围时行为未定义
rust
let pi = 3.99f32;
let int_pi = pi as i32; // 3
字符转整数
- 转换为字符的Unicode码值
rust
let star = '*';
let star_code = star as u32; // 42
布尔值转整数
true
转换为 1false
转换为 0
rust
let t = true as u8; // 1
let f = false as i32; // 0