跳到主要内容Rust 基础数据类型与变量机制 | 极客日志RustAI
Rust 基础数据类型与变量机制
Rust 的基础类型和变量系统主要解决三类问题:值怎么表示、变量能否修改、类型之间怎么安全转换。文章分别说明了整数、浮点数、布尔值、字符、元组、数组、切片、字符串以及 `let`、`mut`、作用域、Shadowing 的用法,并补充了 `as`、`From`、`Into` 的转换方式。最后用三个小例子把这些概念串起来,同时提醒了整数溢出、浮点精度和数组越界这几个最常见的坑。
字节跳动9 浏览 Rust 基础数据类型与变量机制
学习目标和重点
这部分内容看起来多,其实就围着三件事转:类型怎么放、变量能不能改、值怎么在类型之间转。Rust 在这几个地方都比很多语言更'较真',写起来前期会有点别扭,但一旦习惯,很多低级错误会在编译期就被挡住。
学习目标
- 掌握基础数据类型,包括整数、浮点数、布尔值和字符的定义、范围和字面量写法
- 能够使用元组、数组和切片处理常见的数据组合
- 理解变量声明、可变性、作用域和 Shadowing 的区别
- 知道 Rust 里常见的类型转换方式,尤其是
as、From、Into
- 能把这些基础类型放进简单场景里用起来,而不是只停留在语法层面
学习重点
- 整数类型里最容易出问题的是有符号和无符号的边界,以及溢出处理
- 切片本质上是引用,不是数据本身,这一点后面写字符串和集合时会反复碰到
mut 和 Shadowing 不是一回事,前者是原地修改,后者更像是'重新声明一个同名变量'
Rust 的基础数据类型
Rust 的数据类型可以先粗分成两类:标量类型和复合类型。前者表示单个值,后者把多个值放在一起。Rust 的静态类型很严格,变量在编译期通常都得说清楚自己是什么类型。
2.1 标量类型
2.1.1 整数类型
Rust 有 10 种整数类型,分成无符号整数和有符号整数两组。无符号类型以 u 开头,只能表示非负数;有符号类型以 i 开头,可以表示正负数。位数从 8 位到 128 位不等。
| 长度(位) | 无符号类型 | 有符号类型 | 最小值 | 最大值 |
|---|
| 8 | u8 | i8 | 0 | 255 |
| 16 | u16 | i16 | -32768 | 32767 |
| 32 | u32 | i32 | -2³¹ | 2³¹-1 |
| 64 | u64 | i64 | -2⁶³ | 2⁶³-1 |
| 128 | u128 | i128 | -2¹²⁷ | 2¹²⁷-1 |
| 平台相关 | usize | isize | 0 | 取决于 CPU 架构(x86 为 2³²,x86_64 为 2⁶⁴) |
let a = 10;
let b: u32 = 20;
= ;
: = ;
= ;
= ;
= ;
= ;
let
c
0xff
let
d
u8
0x1A
let
e
0o77
let
f
0b1010
let
g
b'A'
let
h
1_000_000
整数溢出这块,Debug 和 Release 的行为不一样。Debug 下会检查,出了问题直接 panic;Release 里通常会按两补数环绕处理。实际写代码时,别把'能跑'当成'没问题',溢出最好显式处理。
let x: u8 = 255;
match x.checked_add(1) {
Some(y) => println!("255 + 1 = {}", y),
None => println!("255 + 1 发生溢出"),
}
let y: u8 = 255;
let z = y.saturating_add(1);
let w: u8 = 255;
let v = w.wrapping_add(1);
2.1.2 浮点数类型
Rust 只有两种浮点数:f32 和 f64。默认一般是 f64。它们都遵循 IEEE-754 标准,只是精度不同。
f32:32 位单精度,约 6-7 位小数
f64:64 位双精度,约 15-17 位小数
let a = 3.14;
let b: f32 = 2.718;
let c = 1e5;
let d: f32 = 2.5e-3;
浮点数最常见的问题不是语法,而是精度。0.1 + 0.2 这类看上去很简单的表达式,结果不一定等于你直觉里的 0.3。
let x = 0.1 + 0.2;
println!("0.1 + 0.2 = {}", x);
println!("x == 0.3? {}", x == 0.3);
fn float_equals(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-9
}
println!("x == 0.3? {}", float_equals(x, 0.3));
2.1.3 布尔类型
Rust 的布尔类型只有 bool,值只能是 true 或 false。它占 1 字节,主要是为了对齐,不是因为逻辑上需要这么多空间。
let is_true = true;
let is_false: bool = false;
if is_true {
println!("这是真的");
} else {
println!("这是假的");
}
let true_as_u8 = is_true as u8;
let false_as_u8 = is_false as u8;
2.1.4 字符类型
Rust 的 char 是 Unicode 标量值,不是 C 里那种单字节字符。它占 4 字节,能表示中文、日文、表情符号这些内容。
let a = 'A';
let b: char = '中';
let c = '😀';
let a_as_u32 = a as u32;
println!("'A'的 Unicode 值是 U+{:X}", a_as_u32);
2.2 复合类型
2.2.1 元组类型
元组是固定长度、允许不同类型混在一起的组合。长度一旦定了,就不能再变。
let t1: (i32, f64, bool) = (10, 3.14, true);
let t2 = ("hello", 'R', 2024);
println!("t1 的第 0 个元素:{}", t1.0);
println!("t1 的第 1 个元素:{}", t1.1);
println!("t1 的第 2 个元素:{}", t1.2);
let (x, y, z) = t2;
println!("x = {}, y = {}, z = {}", x, y, z);
let t3 = (5,);
let t4 = (5);
let t5 = ();
元组很适合当函数返回值,尤其是'顺手带回两个结果'的时候。
fn calculate_rectangle(width: u32, height: u32) -> (u32, u32) {
let area = width * height;
let perimeter = (width + height) * 2;
(area, perimeter)
}
fn main() {
let (area, perimeter) = calculate_rectangle(10, 5);
println!("面积:{},周长:{}", area, perimeter);
}
2.2.2 数组类型
数组是固定长度、同一种类型的组合,存放在栈上。它适合长度已知、数据量不大的场景。
let a: [i32; 3] = [1, 2, 3];
let b = [10, 20, 30];
let c = [5; 4];
println!("a 的第 0 个元素:{}", a[0]);
println!("a 的第 2 个元素:{}", a[2]);
println!("c 的第 1 个元素:{}", c[1]);
println!("a 的长度:{}", a.len());
for element in a.iter() {
println!("{}", element);
}
let mut d = [1, 2, 3];
d[0] = 10;
println!("d 的第 0 个元素:{}", d[0]);
数组越界在 Rust 里不会悄悄吞掉,运行时会直接 panic。这个设计很硬,但对排查问题省事。
let a = [1, 2, 3];
println!("a 的第 3 个元素:{}", a[3]);
2.2.3 切片类型
切片可以理解成数组或 Vec 的一段视图,它自己不存数据,只带着指针和长度。用切片的时候,真正的数据还是在原数组或原 Vec 里。
- 指向数组/
Vec 的指针(*const T 或 *mut T)
- 切片长度(
usize)
let a = [1, 2, 3, 4, 5];
let slice1 = &a[1..3];
let slice2 = &a[..2];
let slice3 = &a[3..];
let slice4 = &a[..];
println!("slice1 的第 0 个元素:{}", slice1[0]);
println!("slice1 的长度:{}", slice1.len());
let mut b = [1, 2, 3, 4, 5];
let mut_slice = &mut b[1..3];
mut_slice[0] = 20;
println!("原数组 b:{:?}", b);
let mut vec = vec![10, 20, 30, 40, 50];
let vec_slice = &vec[2..4];
println!("Vec 切片:{:?}", vec_slice);
2.3 字符串类型
Rust 里字符串常见的就两种:&str 和 String。初学时最好先把它们分开记,不然后面所有权一上来就容易乱。
&str 是字符串切片,通常指向字符串字面量或别的字符串的一部分
String 是可变字符串,数据放在堆上
let s1: &str = "Hello, Rust!";
let s2 = "这是中文";
let s3 = String::new();
let s4 = String::from(s1);
let s5 = String::from("动态字符串");
let s6 = s4 + " " + s5.as_str();
println!("s6:{}", s6);
let s7 = format!("{} {} {}", s1, s5, 2024);
println!("s7:{}", s7);
let s8 = "Rust 语言开发";
println!("s8 的第 0-3 字节:{}", &s8[0..3]);
for c in s8.chars() {
println!("{}", c);
}
2.4 变量系统
2.4.1 变量声明
Rust 里声明变量用 let。默认情况下,变量是不可变的,这一点和很多语言正好相反。
let x = 10;
let y = "hello";
let z: u8 = 255;
let w: String = String::from("Rust");
2.4.2 变量的可变性
如果要改值,就在声明时加 mut。Rust 这里的态度很明确:能不改就别改。
let mut x = 10;
x = 20;
println!("x:{}", x);
let mut s = String::from("hello");
s.push_str(", Rust!");
println!("s:{}", s);
2.4.3 变量的作用域
变量从声明开始生效,到所在代码块结束为止。出了作用域,变量就不可用了。
fn main() {
let x = 10;
{
let y = 20;
println!("x + y = {}", x + y);
}
println!("x = {}", x);
}
2.4.4 Shadowing
Shadowing 是同名新变量把旧变量遮住了。它和 mut 最大的区别是:Shadowing 不是原地修改,而是重新绑定。
let x = 10;
let x = x + 5;
println!("x:{}", x);
let y = 20;
{
let y = "hello";
println!("内部 y:{}", y);
}
println!("外部 y:{}", y);
let z = "123";
let z = z.parse::<i32>().unwrap();
println!("z:{}", z);
| 特性 | Shadowing | mut |
|---|
| 变量名 | 可以相同 | 可以相同 |
| 变量类型 | 可以不同 | 必须相同 |
| 内存地址 | 可能不同 | 必须相同 |
| 作用域 | 同一或嵌套作用域 | 同一作用域 |
| 适用场景 | 需要类型转换或重新计算 | 需要修改值但类型不变 |
2.5 类型转换
Rust 对类型转换管得比较严,隐式转换很少。大多数时候都要你明确写出来。
2.5.1 as 关键字转换
as 适合基础类型之间的转换,写起来直接,但也正因为直接,风险也更容易被忽略。
let a: i32 = 100;
let b: u8 = a as u8;
let c: i8 = 255 as i8;
let d: i32 = 5;
let e: f64 = d as f64;
let f: f64 = 3.14;
let g: i32 = f as i32;
let h: char = 'A';
let i: u32 = h as u32;
- 不能拿它做非基础类型之间的转换,比如
String 和 &str
- 浮点转整数会直接截断小数部分
- 大整数转小整数可能会发生环绕,结果不一定符合直觉
2.5.2 From / Into
From 和 Into 更偏向'可预期'的转换方式。只要类型实现了 From,通常就能顺带拿到 Into。
let s1: &str = "hello";
let s2: String = String::from(s1);
let s3: String = s1.into();
use std::convert::TryFrom;
use std::convert::TryInto;
let a: i32 = 100;
let b: Result<u8, std::num::TryFromIntError> = u8::try_from(a);
match b {
Ok(x) => println!("a 转换为 u8:{}", x),
Err(e) => println!("转换失败:{}", e),
}
let c: Result<u8, std::num::TryFromIntError> = a.try_into();
struct Person {
name: String,
age: u32,
}
struct PersonInfo {
name: &'static str,
age: u32,
}
impl From<PersonInfo> for Person {
fn from(info: PersonInfo) -> Self {
Person {
name: String::from(info.name),
age: info.age,
}
}
}
let info = PersonInfo { name: "张三", age: 25 };
let person: Person = info.into();
println!("姓名:{},年龄:{}", person.name, person.age);
真实案例应用
3.1 计算多种几何图形的面积
这个例子主要是把枚举、模式匹配和基础数值类型串起来。场景很简单:不同图形的参数不同,但面积计算都返回 f64。
#[derive(Debug)]
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64),
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
Shape::Rectangle(width, height) => width * height,
Shape::Triangle(base, height) => base * height / 2.0,
}
}
}
fn main() {
let circle = Shape::Circle(5.0);
let rectangle = Shape::Rectangle(10.0, 5.0);
let triangle = Shape::Triangle(6.0, 4.0);
println!("圆形面积:{:.2}", circle.area());
println!("矩形面积:{:.2}", rectangle.area());
println!("三角形面积:{:.2}", triangle.area());
}
3.2 处理用户输入的成绩数据
这里用数组、循环和切片把一个小统计程序跑起来。它不算优雅,但处理输入、校验、统计这几个动作都在里面了,适合拿来理解基础类型怎么配合。
use std::io;
fn main() {
let mut scores = [0; 50];
let mut count = 0;
println!("请输入学生成绩(输入 -1 结束):");
loop {
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
let score: i32 = match input.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("请输入有效的整数");
continue;
}
};
if score == -1 {
break;
}
if score < 0 || score > 100 {
println!("成绩必须在 0-100 之间");
continue;
}
if count >= 50 {
println!("最多只能输入 50 个成绩");
break;
}
scores[count] = score;
count += 1;
}
if count == 0 {
println!("没有输入成绩");
return;
}
let sum: i32 = scores[0..count].iter().sum();
let average = sum as f64 / count as f64;
let max_score = scores[0..count].iter().max().unwrap();
let min_score = scores[0..count].iter().min().unwrap();
let mut grade_counts = [0; 5];
for &score in scores[0..count].iter() {
match score {
0..=59 => grade_counts[0] += 1,
60..=69 => grade_counts[1] += 1,
70..=79 => grade_counts[2] += 1,
80..=89 => grade_counts[3] += 1,
90..=100 => grade_counts[4] += 1,
_ => (),
}
}
println!("成绩统计结果:");
println!("----------------------");
println!("学生人数:{}", count);
println!("平均分:{:.2}", average);
println!("最高分:{}", max_score);
println!("最低分:{}", min_score);
println!("----------------------");
println!("成绩分布:");
println!("0-59 分:{}人", grade_counts[0]);
println!("60-69 分:{}人", grade_counts[1]);
println!("70-79 分:{}人", grade_counts[2]);
println!("80-89 分:{}人", grade_counts[3]);
println!("90-100 分:{}人", grade_counts[4]);
}
3.3 解析 CSV 格式的产品数据
这个例子展示了字符串切分、结构体、Vec 和过滤逻辑怎么一起工作。CSV 这里是简化版,只按逗号分隔,不处理转义和引号。
#[derive(Debug)]
struct Product {
name: String,
price: f64,
stock: u32,
}
fn parse_product_csv(line: &str) -> Option<Product> {
let fields: Vec<&str> = line.split(',').collect();
if fields.len() != 3 {
return None;
}
let name = fields[0].trim().to_string();
let price: f64 = match fields[1].trim().parse() {
Ok(num) => num,
Err(_) => return None,
};
let stock: u32 = match fields[2].trim().parse() {
Ok(num) => num,
Err(_) => return None,
};
Some(Product { name, price, stock })
}
fn main() {
let csv_data = " 苹果,5.99, 100\n香蕉,2.49, 200\n橙子,3.99, 150\n葡萄,9.99, 50\n西瓜,12.99, 30\n错误数据,abc, 10 ";
let mut products = Vec::new();
for line in csv_data.lines() {
let trimmed_line = line.trim();
if trimmed_line.is_empty() {
continue;
}
match parse_product_csv(trimmed_line) {
Some(product) => products.push(product),
None => println!("忽略无效行:{}", trimmed_line),
}
}
let filtered_products: Vec<&Product> = products
.iter()
.filter(|p| p.price >= 5.0 && p.price <= 10.0)
.collect();
println!("价格在 5-10 元之间的产品:");
println!("----------------------------------");
println!("产品名称\t价格\t库存");
println!("----------------------------------");
for product in filtered_products {
println!("{}\t{:.2}\t{}", product.name, product.price, product.stock);
}
}
常见问题和处理方式
4.1 整数溢出导致的崩溃
Debug 模式下,整数溢出会触发 panic。处理上还是那几个办法:checked_*、saturating_*、wrapping_*。别指望默认行为替你兜底。
4.2 浮点数精度导致的逻辑错误
0.1 + 0.2 != 0.3 不是 Rust 独有的问题,是浮点表示方式的老毛病。比较时通常改成差值阈值判断。
4.3 数组越界访问导致的崩溃
越界访问会 panic。更稳妥的方式是先检查范围,或者直接用 get() 拿 Option。
let a = [1, 2, 3];
if let Some(element) = a.get(3) {
println!("{}", element);
} else {
println!("索引无效");
}
4.4 类型不匹配导致的编译错误
如果参数类型对不上,Rust 不会帮你猜。通常就是三步:看类型注解、显式转换、必要时用 dbg!() 看值。
let x = 10;
dbg!(x);
let y = x as u8;
dbg!(y);
收尾
基础类型这块,学完以后最重要的不是背表,而是知道什么时候该用哪种类型。整数要盯住溢出,浮点数要避开精确相等比较,字符串别把 &str 和 String 搞混,数组和切片也别把'视图'和'数据'混为一谈。Rust 的规则多,但大多数都不是为了添麻烦,而是把问题尽量提前到编译期。
下一篇会接着讲函数和流程控制,重点放在参数、返回值、if、match 和几种循环写法上。
相关免费在线工具
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- 随机西班牙地址生成器
随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online