Rust let / static / const 完整详解
核心总览
let:栈/堆上普通变量,运行时,有所有权、可借用,可mut,作用域局部const:编译期常量,编译时直接展开,无内存地址,没有所有权,仅编译期求值static:全局静态变量,程序数据段(.data/.rodata),拥有固定内存地址,整个程序生命周期,可mut(unsafe)
三者本质完全不同,不要和C语言 const/static 直接等价。
一、let 普通绑定
let 创建局部变量绑定,作用域受代码块限制,栈或者堆,运行时生效。
基础语法
ini
// 不可变绑定,不能重新赋值
let a = 10;
// a = 20; // 编译报错,默认不可变
// 可变绑定 mut
let mut b = 20;
b = 30;
// 类型标注
let c: i32 = 100;
// 遮蔽 shadowing:同名let覆盖,不是修改,生成新绑定
let x = 5;
let x = x + 1; // 遮蔽,允许类型变化
let x = "hello";
let 关键特性
- 作用域:块级作用域 ,出
{}销毁,执行Drop; - 有所有权系统:可以move、借用
&、&mut; - 值可以是运行时计算结果:
css
let input = std::io::stdin().read_line(...); // 运行时才知道值
- 可遮蔽 shadowing,允许改变类型;
mut不能改变类型。 - 可以不初始化,但使用前必须赋值:
ini
let v;
v = 99;
println!("{}",v);
let 存储位置
- 简单标量:栈上;
String/Vec:栈存元数据指针,实际数据在堆;- let 不会放到全局数据段,只能局部。
❗let 不能写在函数外面,只能函数/内部块。
二、const 编译期常量
ini
const MAX_COUNT: u32 = 1024;
const:编译期求值,编译展开,没有内存地址,不占用运行时内存。
类似C语言
#define宏,但带强类型检查。
规则
- 可以写在全局,也可以写函数内部;
- 必须显式写类型,不能推导;
ini
const A = 10; // ❌编译错误,缺少类型
const A: i32 = 10; // ✅
- 只能是编译期可计算表达式,不能运行时值;
css
const X: i32 = 1 + 2 * 3; // ✅编译期计算
// const Y:i32 = std::time::SystemTime::now().elapsed().unwrap().as_secs() // ❌运行时值不行
- 没有内存地址 ,每次使用直接原地展开,取地址
&MAX_COUNT不一定得到同一个地址; - 不允许 mut,const永远不可变;
- 支持复杂编译期计算:数组、字符串、结构体(必须const构造);
- 泛型const:
const N:usize(const泛型,用于数组[T;N])
示例:
ini
const BUF_SIZE: usize = 4096;
const GREET: &str = "hello const";
#[derive(Debug)]
struct Conf { v: i32 }
const CFG: Conf = Conf {v: 100}; // const结构体
const 只是值的别名,不产生全局变量。
const 函数(const fn)
编译期可执行的函数,用于const求值:
rust
const fn add(a:i32,b:i32) -> i32 { a + b }
const SUM:i32 = add(1,2);
三、static 静态全局变量
ini
static G_NUM: i32 = 100;
static:分配在程序全局数据段(rodata只读 / data可读写),整个程序生命周期,拥有唯一固定内存地址。
等价C语言全局变量。
static 两种:只读static、static mut
1)只读 static(安全,无需unsafe)
ini
static GREET: &str = "hello static";
static MAX: u32 = 999;
fn main() {
println!("{}", GREET);
let p = &GREET; // ✅可以取地址,永远同一个地址
}
- 存放在
.rodata只读段; - 程序启动就存在,main之前就生效;
- 有确定唯一内存地址 ,多次取
&都是同一个指针; - 类型必须标注;
- 初始化必须编译期常量表达式;不能运行时赋值。
2)static mut 可变静态(⚠️极度危险,必须unsafe)
rust
static mut COUNTER: u32 = 0;
fn main() {
unsafe {
COUNTER +=1;
println!("{}", COUNTER);
}
}
static mut所有读写都必须包裹unsafe。
-
放在
.data可读写段; -
多线程同时读写同一个
static mut直接发生数据竞争,UB未定义行为,编译器完全不检查。工程实践:尽量不要用static mut ;多线程场景改用
lazy_static / once_cell+Arc<Mutex<T>>。
static 重要特性
- 必须显式标注类型;
- 初始化只能编译期常量,不能运行时计算初始化;
vbnet
// static S: String = String::from("abc"); // ❌ String::from是运行时,不能static直接初始化
String/Vec这种需要运行时堆分配的,不能直接作为static初始值。需要
once_cell::sync::Lazy延迟初始化。
- 拥有唯一内存地址;
&G_NUM得到稳定全局指针; - 生命周期
'static;&'static T就是指向static或者程序二进制段的引用。
static 存String/Vec(延迟初始化)
标准库没有内置延迟static,常用 once_cell:
ini
[dependencies]
once_cell = "1.19"
rust
use once_cell::sync::Lazy;
static G_STR: Lazy<String> = Lazy::new(|| String::from("global string"));
fn main(){
println!("{}", *G_STR);
}
第一次访问时运行闭包初始化,线程安全。
static vs const 核心对比(高频面试)
| const | static | |
|---|---|---|
| 内存 | 编译期展开,无固定内存地址 | 全局数据段,唯一固定内存地址 |
取地址&x |
每次可能不同地址 | 永远同一个地址 |
| 可mut | ❌不允许mut | ✅支持static mut(unsafe) |
| 生命周期 | 无 | 'static |
| 使用场景 | 数值常量、数组大小、魔法数字 | 全局共享状态、全局句柄 |
| 运行时初始化 | 不支持 | 不支持,需要Lazy延迟初始化 |
简单记忆:魔法数字用const;需要全局唯一内存地址用static。
四、let / const / static 完整对比表
| 项目 | let | const | static |
|---|---|---|---|
| 位置 | 函数/块内部 | 全局/函数内部 | 仅全局(模块级别) |
| 类型标注 | 可推导,可省略 | 必须写类型 | 必须写类型 |
| 可mut | ✅ let mut |
❌不可变 | ✅static mut(unsafe) |
| 内存位置 | 栈/堆 | 编译展开无独立存储 | 程序.rodata/.data全局段 |
| 生命周期 | 局部作用域,出块销毁 | 无,编译替换 | 整个程序 'static |
| 取地址& | 合法 | 地址不保证稳定 | 固定唯一地址 |
| 初始化值 | 运行时/编译期均可 | 仅编译期常量表达式 | 仅编译期常量表达式 |
| 所有权 | 完整所有权、借用系统 | 无所有权 | 全局静态所有权 |
| 多线程安全 | 遵循借用检查 | 无状态,安全 | static mut极易数据竞争UB |
典型错误示例
vbnet
// 错误1:let放全局
// let A:i32 =10; // ❌ let不能在函数外面
// 错误2:const省略类型
// const B = 10; // ❌必须写类型
// 错误3:static直接初始化String
// static S:String = String::from("123"); // ❌运行时堆分配,不允许
// 错误4:static mut不写unsafe
// COUNTER +=1; // ❌必须unsafe块包裹
五、'static 生命周期(容易混淆)
'static 不是 static 关键字,是生命周期标记:
-
&'static str:字符串字面量,放在程序二进制只读段;static变量的引用也是&'static T。
ini
static X:i32 = 10;
let r:&'static i32 = &X;
区分:
static X:T:定义全局变量;&'static T:引用生命周期,代表引用存活整个程序。
六、工程实践选型建议
- 魔法数字、缓冲区大小、常量数值 → 使用
const
ini
const MAX_CONN:usize = 1000;
- 需要全局唯一地址,全局只读配置 → 使用
static - 全局可变状态 :禁止直接写
static mut,优先用once_cell::sync::Lazy<Arc<Mutex<T>>>线程安全封装。 - 局部变量 :一律用
let / let mut。 - 需要运行时初始化全局对象(String、Vec、句柄):用
once_cell Lazy延迟初始化。
七、和C语言对比(FFI/bindgen场景)
- C
const int A≠ Rustconst A:i32,C const是带内存的全局变量,更接近Ruststatic; - C
#define VAL 10≈ Rustconst VAL:i32 =10; - C全局变量
int g;≈ Ruststatic mut G:i32(unsafe)。
bindgen导入C全局变量,生成Rust的
static,不是const。
八、易混点总结
const不分配内存,只是编译期值替换;static实实在在占用全局内存。static mut读写必须unsafe,多线程会UB,尽量避开。- static不能直接初始化需要堆分配的类型(String/Vec),要用Lazy延迟初始化。
- let只允许局部;const可全局可局部;static只能模块全局。
'static是生命周期,不等于static关键字。
如果你需要,我可以给你一段可直接复制的综合demo,包含const/static/once_cell示例。