Rust let / static / const 完整详解

Rust let / static / const 完整详解

核心总览

  1. let栈/堆上普通变量,运行时,有所有权、可借用,可mut,作用域局部
  2. const编译期常量,编译时直接展开,无内存地址,没有所有权,仅编译期求值
  3. 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 关键特性

  1. 作用域:块级作用域 ,出 {} 销毁,执行Drop;
  2. 有所有权系统:可以move、借用 &&mut
  3. 值可以是运行时计算结果
css 复制代码
let input = std::io::stdin().read_line(...); // 运行时才知道值
  1. 可遮蔽 shadowing,允许改变类型;mut 不能改变类型。
  2. 可以不初始化,但使用前必须赋值:
ini 复制代码
let v;
v = 99;
println!("{}",v);

let 存储位置

  • 简单标量:栈上;
  • String/Vec:栈存元数据指针,实际数据在堆;
  • let 不会放到全局数据段,只能局部。

❗let 不能写在函数外面,只能函数/内部块。


二、const 编译期常量

ini 复制代码
const MAX_COUNT: u32 = 1024;

const编译期求值,编译展开,没有内存地址,不占用运行时内存

类似C语言 #define 宏,但带强类型检查。

规则

  1. 可以写在全局,也可以写函数内部;
  2. 必须显式写类型,不能推导;
ini 复制代码
const A = 10; // ❌编译错误,缺少类型
const A: i32 = 10; // ✅
  1. 只能是编译期可计算表达式,不能运行时值;
css 复制代码
const X: i32 = 1 + 2 * 3; // ✅编译期计算
// const Y:i32 = std::time::SystemTime::now().elapsed().unwrap().as_secs() // ❌运行时值不行
  1. 没有内存地址 ,每次使用直接原地展开,取地址 &MAX_COUNT 不一定得到同一个地址;
  2. 不允许 mut,const永远不可变;
  3. 支持复杂编译期计算:数组、字符串、结构体(必须const构造);
  4. 泛型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 重要特性

  1. 必须显式标注类型;
  2. 初始化只能编译期常量,不能运行时计算初始化
vbnet 复制代码
// static S: String = String::from("abc"); // ❌ String::from是运行时,不能static直接初始化

String/Vec这种需要运行时堆分配的,不能直接作为static初始值。需要once_cell::sync::Lazy延迟初始化。

  1. 拥有唯一内存地址;&G_NUM 得到稳定全局指针;
  2. 生命周期 '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:引用生命周期,代表引用存活整个程序。

六、工程实践选型建议

  1. 魔法数字、缓冲区大小、常量数值 → 使用 const
ini 复制代码
const MAX_CONN:usize = 1000;
  1. 需要全局唯一地址,全局只读配置 → 使用 static
  2. 全局可变状态禁止直接写 static mut ,优先用 once_cell::sync::Lazy<Arc<Mutex<T>>> 线程安全封装。
  3. 局部变量 :一律用 let / let mut
  4. 需要运行时初始化全局对象(String、Vec、句柄):用once_cell Lazy延迟初始化。

七、和C语言对比(FFI/bindgen场景)

  • C const int A ≠ Rust const A:i32,C const是带内存的全局变量,更接近Rust static
  • C #define VAL 10 ≈ Rust const VAL:i32 =10
  • C全局变量 int g; ≈ Rust static mut G:i32(unsafe)。

bindgen导入C全局变量,生成Rust的static,不是const。

八、易混点总结

  1. const不分配内存,只是编译期值替换;static实实在在占用全局内存。
  2. static mut读写必须unsafe,多线程会UB,尽量避开。
  3. static不能直接初始化需要堆分配的类型(String/Vec),要用Lazy延迟初始化。
  4. let只允许局部;const可全局可局部;static只能模块全局。
  5. 'static是生命周期,不等于static关键字。

如果你需要,我可以给你一段可直接复制的综合demo,包含const/static/once_cell示例。

相关推荐
人间凡尔赛35 分钟前
2026 后端架构三驾马车:Wasm 容器上 K8s、存算分离与 AI 原生
后端·云原生·架构
凤山老林1 小时前
动态 i18n 体系落地:Spring Boot 多租户热加载与前后端协同实践
java·spring boot·后端·i18n
东风破_1 小时前
TypeScript 高级类型进阶:keyof、Exclude、Record 与类型组合思想
前端·后端·typescript
凤山老林2 小时前
数据库读写分离与动态路由实战:Spring Boot + ShardingSphere-JDBC 生产配置
数据库·spring boot·后端·分库分表·sharding-jdbc
Zane19942 小时前
调用了 async 函数却没执行?一文讲透协程、event loop 与 await
后端·python
Zane19942 小时前
HashMap 为什么要在长度16、容量必须是2的幂这些细节上较劲
java·后端
掘金者阿豪3 小时前
若依启动突然报 Redis MISCONF?一次从应用报错到磁盘爆满的完整排查记录
后端
boooooooom4 小时前
尝尝咸淡:从朴素 RAG 到 Graph RAG——一个烹饪问答系统的三层检索升级之路
前端·后端·llm
福大大架构师每日一题4 小时前
ragflow v0.27.0 发布:Go 后端全面对齐、智能体搜索与知识编译大升级,超全更新解析
开发语言·后端·golang
2601_963870185 小时前
【计算机毕业设计】基于Vue+Spring Boot的助农电商系统的设计与实现
spring boot·后端·课程设计