Rust:变量、常量与数据类型

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
  1. 遮蔽:创建新变量,可以改变类型
  2. 可变性:修改同一变量的值,类型不能改变
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提供了两种机制:

  1. 类型推断:编译器智能推断类型
  2. 显式标注:程序员明确指定类型

类型推断

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位)

对于 isizeusize,它们占用的比特位取决于系统架构,系统是多少位,就占用多少位。


整数字面量与后缀

前缀

Rust 允许通过前缀不同,来决定一个字面量的进制。

  • 无前缀:十进制(如 42
  • 0x:十六进制(如 0x2A == 42)
  • 0o:八进制(如 0o52 == 42)
  • 0b:二进制(如 0b101010 == 42)
  • 大小写均可:0xFF0xff 等价
rust 复制代码
// 进制表示
let decimal = 98;    // 十进制
let hex = 0xff;      // 十六进制
let octal = 0o77;    // 八进制  
let binary = 0b111;  // 二进制

下划线分隔符

对于一个数值,允许通过 _ 进行分割,仅用于提升可读性,没有数值语义影响。例如 1_000_0001000000 完全相同;二进制/十六进制中也可用来分组位。

字节字面量
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字节,只能是truefalse,不能与数字隐式转换。


字符类型

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;

这样元组内的元素就会自动赋值到 abcxyz 上。

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 转换为 1
  • false 转换为 0
rust 复制代码
let t = true as u8;  // 1
let f = false as i32; // 0

相关推荐
十八旬20 分钟前
苍穹外卖项目实战(日记十)-记录实战教程及问题的解决方法-(day3-2)新增菜品功能完整版
java·开发语言·spring boot·mysql·idea·苍穹外卖
这周也會开心1 小时前
Java-多态
java·开发语言
Forward♞1 小时前
Qt——网络通信(UDP/TCP/HTTP)
开发语言·c++·qt
XH华1 小时前
C语言第十三章自定义类型:联合和枚举
c语言·开发语言
2401_858286111 小时前
OS26.【Linux】进程程序替换(下)
linux·运维·服务器·开发语言·算法·exec·进程
草莓熊Lotso1 小时前
【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day13
c语言·开发语言·刷题·强化训练
一尘之中3 小时前
在Python 2.7中安装SQLAlchemy的完整指南
开发语言·python·ai写作
黄贵根3 小时前
使用JDK11标准 实现 图数据结构的增删查改遍历 可视化程序
java·开发语言·数据结构
电商数据girl3 小时前
Python 爬虫获得淘宝商品详情 数据【淘宝商品API】
大数据·开发语言·人工智能·爬虫·python·json·php