数据类型
1. 标量类型(Scalar Types)
标量类型代表一个单一的值。
A. 整数类型 (Integers)
Rust 提供了非常精细的整数控制,分为有符号(i)和无符号(u)。
| 长度 | 有符号 (Signed) | 无符号 (Unsigned) | 范围 (n 为位数) |
|---|---|---|---|
| 8-bit | i8 | u8 | i8: -(2^(8-1)) ~ 2^(8-1)-1,u8: 0 ~ 2^8-1 |
| 16-bit | i16 | u16 | i16: -(2^(16-1)) ~2^(16-1)-1,u16: 0 ~2^16-1 |
| 32-bit | i32 (默认) | u32 | i32: -(2^(32-1)) ~2^(32-1)-1,u32: 0 ~ 2^32-1 |
| 64-bit | i64 | u64 | i64: -(2^(64-1)) ~2^(64-1)-1,u64: 0 ~ 2^64-1 |
| 128-bit | i128 | u128 | i128:-(2^(128-1)) ~2^(128-1)-1,u128: 0 ~ 2^128-1 |
| arch (平台相关) | isize | usize | 取决于计算机架构 (64位或32位) |
-
usize/isize的用途:常用于集合索引、切片范围、长度(如len())、以及与内存地址大小相关的场景。 -
整数字面值:可以使用
_分隔增强可读性,如1_000_000。支持0x(十六进制)、0o(八进制)、0b(二进制)。 -
类型后缀:可在字面量后加后缀明确类型,如
10u8、20i64。 -
溢出行为:
- debug 构建:整数溢出会触发
panic! - release 构建:默认按补码进行回绕(wrapping)
- 常见策略方法:
wrapping_add、checked_add、overflowing_add、saturating_add
- debug 构建:整数溢出会触发
fn main() {
let a: u8 = 250;
assert_eq!(a.wrapping_add(10), 4);
assert_eq!(a.checked_add(10), None);
}
B. 浮点类型 (Floating-Point)
Rust 遵循 IEEE-754 标准:
f32:单精度。f64:双精度(默认,因为在现代 CPU 上速度几乎与f32一样快,但精度更高)。
浮点数相关注意点:
- 精度误差:尽量避免直接用
==比较业务浮点值,常用误差范围比较。 - NaN:
NaN != NaN,这会影响比较与排序逻辑。
fn main() {
let x = 0.1f64 + 0.2;
assert!((x - 0.3).abs() < 1e-10);
}
C. 布尔与字符
bool:true和false。通常占用 1 个字节。char:占用 4 个字节,代表一个 Unicode 标量值,可以表示中文、日文、表情符号 (Emoji) 等。char不是 UTF-8 的“一个字节”,也不等同于字符串的长度单位。
2. 复合类型 (Compound Types)
将多个值组合成一个类型。
A. 元组 (Tuple)
- 特点:长度固定,各元素类型可以不同。
- 定义:
let tup: (i32, f64, u8) = (500, 6.4, 1); - 访问:使用点号,如
tup.0。 - 解构:可用模式匹配直接拆开。
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
}
- 单元类型
():不包含任何值的元组,常作为表达式的默认返回值或占位类型。
B. 数组 (Array)
- 特点:长度固定,各元素类型必须相同。
- 定义:
let a = [1, 2, 3, 4, 5]; - 类型与长度声明:
let a: [i32; 5] = [1, 2, 3, 4, 5]; - 重复初始化:
let a = [0u8; 1024]; - 存储:数组是固定大小的值类型,作为局部变量时通常位于栈上;若被
Box等包裹,则数据会位于堆上。
数组访问与边界:
a[i]:越界会panic!a.get(i):返回Option<&T>,更安全
fn main() {
let a = [1, 2, 3];
assert_eq!(a.get(10), None);
}
3. 序列与字符串 (Sequences & Strings)
这一部分常见困惑点集中在 UTF-8 与内存分配方式。
A. 字符串 (Strings)
Rust 核心语言层面有 str(动态大小类型,通常以引用切片 &str 出现),标准库提供可增长的 String。
String:拥有所有权,数据在堆上,可增长。内部包含指针、长度、容量等信息。&str:字符串切片,是对一段 UTF-8 字节序列的借用视图。引用本身在栈上,实际数据可能来自只读区(字符串字面量)、栈或堆。
常见转换:
fn main() {
let s1: String = "hello".to_string();
let s2: &str = &s1; // &String 自动解引用成 &str
let s3: String = s2.to_owned(); // 或 s2.to_string()
}
字符串索引与切片规则:
- Rust 不支持
s[0]直接索引字符,因为 UTF-8 下“字符边界”与字节下标不总一致。 &s[a..b]必须落在 UTF-8 字符边界,否则运行时会panic!。
fn main() {
let s = "中文";
let ok = &s[0..3]; // "中" 占 3 个字节
}
B. 切片 (Slices)
切片引用连续的一段序列,而不是整个集合。
- 数组/向量切片:
&[T] - 字符串切片:
&str(本质上也是切片)
示例:
fn main() {
let a = [10, 20, 30, 40, 50];
let slice = &a[1..3]; // &[20, 30]
}
切片是一种“胖指针”,通常包含地址与长度信息,因此可以安全地携带边界。
4. 标准库集合 (Standard Collections)
虽然属于标准库,但它们在实际开发中几乎被当作基础类型使用。
A. Vec<T> (Vector)
- 动态数组,在堆上分配,可扩容。
- 常见 API:
push、pop、len、capacity、get、切片&v[a..b]等。 - 预分配容量:
Vec::with_capacity(n)可减少扩容次数。
fn main() {
let mut v = Vec::new();
v.push(1);
v.extend([2, 3, 4]);
println!("{:?}", v);
let first = v.get(0); // Option<&i32>
let part = &v[1..3]; // &[i32]
println!("{:?}", part);
}
B. HashMap<K, V>
- 键值对映射结构。
- 常用
entry模式在“插入或更新”时更方便。
fn main() {
use std::collections::HashMap;
let mut m = HashMap::new();
m.insert("a", 1);
println!("{:?}", m);
*m.entry("a").or_insert(0) += 1;
println!("{:?}", m);
}
5. 特殊/底层类型
A. 枚举 (Enums)
枚举用于表示“一组有限的可能取值”。Rust 的枚举非常强大:每个变体(variant)不仅能表示不同分支,还能携带不同类型的数据,因此很适合用来建模状态机、协议消息、错误类型等。
- 基本定义与使用
enum Direction {
Up,
Down,
Left,
Right,
}
fn move_step(d: Direction) {
match d {
Direction::Up => println!("up"),
Direction::Down => println!("down"),
Direction::Left => println!("left"),
Direction::Right => println!("right"),
}
}
fn main() {
let d = Direction::Up;
move_step(d);
}
2)变体携带数据
枚举变体可以携带数据,且不同变体携带的数据类型可以不同:
enum Message {
Quit,
Move { x: i32, y: i32 }, // 结构体风格
Write(String), // 元组风格
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move { x: 3, y: 4 };
let m3 = Message::Write("hi".to_string());
}
B. 结构体 (Structs)
结构体用于把多个字段组合成一个自定义类型,是“组织数据”的核心方式之一。
- 具名字段结构体(最常用)
struct User {
name: String,
age: u8,
active: bool,
}
fn main() {
let u = User {
name: "Alice".to_string(),
age: 18,
active: true,
};
//访问字段用点号:
println!("{:?}", u.name);
}
2)结构体与所有权的直观规则
字段类型如果是 String、Vec <T> 等“拥有型”,把结构体赋值给新变量时默认会发生移动(move)。
想继续使用旧值通常需要借用(&User)或让字段可复制(如 u32)或显式 clone()。
struct User {
name: String,
age: u8,
active: bool,
}
fn main() {
let u1 = User { name: "A".to_string(), age: 1, active: true };
let u2 = u1; // u1 被 move
// println!("{}", u1.age); // 不能用
println!("{}", u2.age);
}
C. 指针类型(Pointer Types)
根据底层的表现形式和抽象程度,可以将 Rust 的指针分为以下四大类:
1. 引用 (References) —— 最常用的指针
引用是 Rust 中最常见的指针形式,它们在底层表现为指向某个内存地址的指针。
&T(不可变引用) :指向类型为T的值,允许读取数据但不能修改。&mut T(可变引用) :允许读取并修改指向的数据。- 内存表现 :
- 普通引用 :对于已知大小的类型(如
i32),它是单字长的指针。 - 切片引用(胖指针) :对于动态大小类型(如
&str或&[T]),它由指针和长度组成,占用两个字长。
- 普通引用 :对于已知大小的类型(如
2. 原生指针 (Raw Pointers) —— 绕过安全的底层指针
原生指针与 C 语言的指针非常相似。它们在语法上定义为 *const T 和 *mut T。
- 特点 :
- 允许忽略借用规则,可以同时拥有多个指向同一位置的可变和不可变指针。
- 不保证指向有效的内存,且允许为
null。 - 安全性 :解引用原生指针是不安全的,必须放在
unsafe块中执行。
- 用途 :主要用于与 C 语言交互(FFI)或编写底层高性能驱动。
fn main() {
let mut num = 5;
// 1. 从引用创建原生指针
// 虽然创建原生指针是安全的,但后续使用是不安全的
let r1 = &num as *const i32; // 不可变原生指针
let r2 = &mut num as *mut i32; // 可变原生指针
// 2. 解引用原生指针
// 必须放在 unsafe 块中,否则编译器会报错
unsafe {
println!("r1 指向的值: {}", *r1);
// 修改原生指针指向的数据
*r2 = 10;
println!("修改后 r2 指向的值: {}", *r2);
}
// 3. 创建一个指向任意内存地址的指针(慎用!)
let address = 0x012345usize;
let _r3 = address as *const i32;
}
3. 函数指针 (fn Pointer) —— 代码地址的载体
函数指针指向的是代码段中的函数入口地址,而不是堆栈上的数据。
- 语法 :类型写作
fn(参数类型) -> 返回类型。 - 区别于闭包 :
- 函数指针不捕获环境变量。
- 它的长度始终是一个字长(存储地址)。
- 它可以作为参数传递给其他函数,或者存储在数据结构中。
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
// 像正常函数一样通过指针调用
f(arg) + f(arg)
}
fn main() {
// 将函数名 add_one 隐式转换为函数指针类型
let f: fn(i32) -> i32 = add_one;
let answer = do_twice(f, 5);
println!("计算结果为: {}", answer); // 输出: 12
// 函数指针的大小验证
println!("函数指针的大小: {} 字节", std::mem::size_of_val(&f));
}
4. 智能指针 (Smart Pointers) —— 携带元数据的指针
智能指针是拥有数据所有权的结构体,它们实现了 Deref 和 Drop 特性。
A. Box<T> (堆空间分配)
- 功能 :在堆上分配空间存储类型为
T的值,并在栈上保留指针。 - 场景 :当数据大小在编译时未知(如递归类型),或数据量巨大不适合在栈上拷贝时使用。
B. Rc<T> (引用计数指针)
- 功能 :全称 Reference Counting,允许多个变量通过增加计数来共享同一个堆数据的所有权。
- 场景 :用于单线程环境下,需要一个数据有多个所有者的复杂逻辑(如树或图的节点共享)。
C. Arc<T> (原子引用计数指针)
- 功能 :Atomic Reference Counting,是
Rc<T>的线程安全版本。 - 场景 :多线程并发环境下,安全地共享同一份数据的所有权。
总结对比表
| 指针类型 | 语法表示 | 内存位置 | 长度(64位) | 核心特性 |
|---|---|---|---|---|
| 引用 | &T/&mut T | 栈/堆 | 8 或 16 字节 | 安全借用,编译器检查生命周期 |
| 原生指针 | *const T/*mut T | 栈/堆 | 8 字节 | 不安全,类似 C 指针 |
| 函数指针 | fn(...) -> ... | 代码段 | 8 字节 | 指向函数地址 |
| 智能指针 | Box<T>/Rc<T> | 堆 | 8 字节 | 管理堆内存,提供自动清理逻辑 |
D. Option<T> 与 Result<T, E>
Rust 不提供 null,用 Option<T> 表示“可能不存在”。
fn main() {
let x: Option<i32> = Some(1);
let y: Option<i32> = None;
}
错误处理通常用 Result<T, E>:
fn parse(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
fn main() {
let result = parse("123");
println!("{:?}", result);
}
E. Never 类型 (!)
! 表示永远不会返回的类型,常见于 panic!、无限循环等。
#![allow(unused)]
fn main() {
fn forever() -> ! {
loop {}
}
}
6. 类型转换注意
Rust 不会进行隐式类型转换。不同整数类型之间的转换必须显式完成。
fn main() {
let a: u8 = 10;
let b: u32 = a as u32;
}
as 转换在整数之间可能发生截断或符号变化。需要“转换失败就返回错误/None”时可用 TryFrom/TryInto。
fn main() {
use std::convert::TryFrom;
let x: i32 = 300;
let y = u8::try_from(x); // Err(...)
}