rust 基础语法
1. Rust代码基本结构
fn main(){
... 代码块
}
2. use 关键字 – 导入模块或函数
2.1 功能作用
use 用于将模块、函数、结构体、枚举、trait 等引入当前作用域,避免每次使用时都写完整路径。它可以简化代码,提升可读性。
2.2 基本规则
// 导入单个模块或类型
use 路径::名称;
// 导入多个同级的项(使用花括号)
use 路径::{项1, 项2};
// 导入自身(使用 `self`)
use 路径::{self, 子模块};
// 为导入的项起别名(使用 `as`)
use 路径::名称 as 别名;
// 重新导出(使用 `pub use`)
pub use 路径::名称;
// 导入标准库常用模块
use std::io; // 导入 io 模块
use std::collections::HashMap; // 导入 HashMap 类型
use通常放在文件的顶部(mod声明之后)。use可以嵌套路径,例如use std::io::{self, Write};。- 使用
crate::表示当前 crate 的根路径,super::表示父模块。 - 外部 crate 需要在
Cargo.toml中先添加依赖,然后使用use crate_name::item;。
2.3 具体示例
// 导入标准库模块
use std::io;
use std::fmt::Result as FmtResult; // 别名,避免与 io::Result 冲突
// 导入多个项
use std::fs::{File, read_to_string};
// 导入自定义模块(假设有一个模块 utils)
mod utils {
pub fn helper() {}
}
use utils::helper;
fn main() {
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap(); // 因为导入了 io,可以简化路径
helper(); // 直接调用,无需 utils::helper
}
3. 变量
3.1 功能作用
在 Rust 中,变量用于存储数据。默认情况下,变量是不可变的,这有助于提升代码的安全性。当你需要修改变量的值时,必须显式声明为可变。
3.2 基本规则
// 定义不可变变量
let 变量名 = 值;
// 定义可变变量
let mut 变量名 = 值;
// 变量可以遮蔽(重新声明同名变量)
let 变量名 = 新值;
- 不可变变量不能被重新赋值。
- 使用
mut关键字使变量可变。 - 允许“遮蔽”(shadowing):用
let再次声明同名变量,新变量会覆盖之前的变量。
3.3 具体示例
fn main() {
let x = 5; // 不可变变量
// x = 6; // 编译错误!不能对不可变变量赋值
let mut y = 10; // 可变变量
y = 11; // 允许修改
println!("y = {}", y);
let z = 100;
let z = z + 1; // 遮蔽:基于旧值创建新值
let z = "hello"; // 遮蔽:z 的类型从整数变为字符串
println!("z = {}", z);
}
3.4 变量遮蔽(Shadowing)详解
变量遮蔽和 mut 是两种不同的机制,初学者容易混淆:
let mut | let 遮蔽 | |
|---|---|---|
| 本质 | 修改同一个变量的值 | 创建一个新变量,同名但独立 |
| 类型能否改变 | ❌ 不能改变类型 | ✅ 可以改变类型 |
| 旧变量 | 被覆盖,不存在了 | 被“藏起来“,新变量离开作用域后可能重新可见 |
是否需要 mut | 是 | 否(新变量默认不可变,也可以加 mut) |
典型使用场景:
fn main() {
// 场景1:类型转换(mut 做不到)
let value = "42";
let value: i32 = value.parse().unwrap(); // String → i32
// 后续直接用 value,不用起新名字
// 场景2:基于旧值逐步构造(避免可变中间变量)
let config = load_default_config();
let config = apply_env_overrides(config);
let config = apply_cli_args(config);
// 每一步都产生新的不可变 config,逻辑清晰
// 场景3:作用域内的临时遮蔽
let x = 5;
{
let x = x * 10; // 作用域内 x = 50
println!("内部 x = {}", x);
}
println!("外部 x = {}", x); // 回到 5,原变量还在
}
mut适合“反复修改同一个值“的场景(如循环计数器);遮蔽适合“加工转换、产生新值“的场景,且允许改变类型。两者各司其职,不必互斥——需要时也可以let mut x = ...然后后续遮蔽。
4. 类型别名 (type)
4.1 功能作用
type 关键字用于为现有类型创建一个新的名称(别名)。这可以简化复杂类型的书写,并增强代码的可读性。类型别名是同义词,不会创建新类型。
4.2 基本规则
type 别名 = 原类型;
- 别名可以用于任何类型,包括普通类型、泛型类型、函数指针等。
- 类型别名在编译时完全等同于原类型,没有运行时开销。
- 常用于简化长泛型类型(如
Result<Vec<u8>, Box<dyn Error>>)。
4.3 具体示例
// 为复杂类型起别名
type FileResult = Result<std::fs::File, std::io::Error>;
// 为函数指针起别名
type MathOp = fn(i32, i32) -> i32;
fn add(a: i32, b: i32) -> i32 { a + b }
fn sub(a: i32, b: i32) -> i32 { a - b }
// 为泛型类型起别名
type MyVec<T> = Vec<T>;
fn main() {
let mut f: FileResult = std::fs::File::open("test.txt");
let op: MathOp = add;
println!("{}", op(2, 3)); // 输出 5
}
5. 控制流
5.1 if-else 条件判断
5.1.1 功能作用
根据条件执行不同的代码分支。Rust 中的 if 是一个表达式,可以返回值(但所有分支返回值类型必须一致)。
5.1.2 基本规则
if 条件 {
// 条件为 true 时执行
} else if 另一个条件 {
// 条件为 true 时执行
} else {
// 所有条件都为 false 时执行
}
- 条件必须是
bool类型(不能像 C 语言那样用整数代替)。 - 可以用
if let简化针对某个模式的匹配。
5.1.3 具体示例
fn main() {
let number = 6;
if number % 2 == 0 {
println!("偶数");
} else {
println!("奇数");
}
// if 作为表达式赋值
let result = if number > 0 { "正数" } else { "非正数" };
println!("{}", result);
}
5.2 if let 简洁匹配
5.2.1 功能作用
当你只关心匹配某一种模式而不想写完整的 match 时,if let 提供更简洁的写法。这里只需了解基本用法即可,更深入的模式匹配将在第五章:返回值和错误处理中结合 Option 和 Result 详细展开。
5.2.2 基本规则
if let 模式 = 值 {
// 匹配成功时执行
} else {
// 可选:匹配失败时执行
}
5.2.3 具体示例
fn main() {
let some_value = Some(3);
if let Some(x) = some_value {
println!("值为 {}", x);
} else {
println!("没有值");
}
}
6. 循环
Rust 提供了三种循环结构:loop、while 和 for。它们都用于重复执行一段代码,但适用场景不同。
6.1 loop – 无限循环
6.1.1 功能作用
无限重复执行一段代码,直到遇到 break 显式退出。可以用 break 返回值。
6.1.2 基本规则
loop {
// 重复执行的代码
break; // 退出循环
}
// 带返回值的 loop
let 变量 = loop {
break 值;
};
6.1.3 具体示例
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // loop 表达式返回 20
}
};
println!("result = {}", result);
}
6.2 while – 条件循环
6.2.1 功能作用
每次循环前检查一个布尔条件,条件为 true 时执行,否则退出。
6.2.2 基本规则
while 条件 {
// 条件为 true 时重复执行
}
6.2.3 具体示例
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("发射!");
}
6.3 for – 遍历集合
6.3.1 功能作用
安全且高效地遍历集合(数组、区间、向量等)。Rust 推荐优先使用 for,因为它不易出错且性能良好。
6.3.2 基本规则
for 变量 in 迭代器 {
// 对每个元素执行
}
6.3.3 具体示例
fn main() {
// 遍历区间(左闭右开)
for i in 1..5 {
println!("{}", i); // 打印 1 2 3 4
}
// 遍历数组
let arr = [10, 20, 30];
for element in arr.iter() {
println!("{}", element);
}
// 使用 enumerate 获取索引
for (index, value) in arr.iter().enumerate() {
println!("索引 {} 的值是 {}", index, value);
}
}
6.4 三种循环的区别
| 循环类型 | 适用场景 | 退出方式 | 是否可返回值 |
|---|---|---|---|
loop | 需要无限循环,或需要在循环体中间退出并返回一个值 | break | 是 |
while | 循环次数未知,但有一个明确的停止条件 | 条件变为 false | 否 |
for | 遍历集合或区间,循环次数已知或可迭代 | 遍历完所有元素 | 否 |
6.5 范围语法 (Range)
Rust 中 .. 和 ..= 用来创建范围(Range),它们是一个轻量级的语法结构,生成一个表示区间的值。范围最常用于 for 循环和切片索引。
两种范围运算符
| 语法 | 含义 | 示例 | 生成的区间 |
|---|---|---|---|
start..end | 左闭右开,不包含 end | 1..5 | 1, 2, 3, 4 |
start..=end | 双闭区间,包含 end | 1..=5 | 1, 2, 3, 4, 5 |
..end | 从 0 到 end(不包含) | ..5 | 0, 1, 2, 3, 4 |
start.. | 从 start 到无穷(惰性) | 3.. | 3, 4, 5, ... |
..=end | 从 0 到 end(包含) | ..=5 | 0, 1, 2, 3, 4, 5 |
.. | 完整范围(起点到无穷) | .. | 0, 1, 2, ... |
常见使用场景
fn main() {
// 1. for 循环中使用范围
for i in 0..5 {
print!("{} ", i); // 0 1 2 3 4
}
println!();
for i in 1..=3 {
print!("{} ", i); // 1 2 3
}
println!();
// 2. 用于切片索引(详见[第 14 节](#14-切片-slice))
let arr = [10, 20, 30, 40, 50];
let slice = &arr[1..4]; // 索引 1~3 → [20, 30, 40]
let inclusive = &arr[1..=4]; // 索引 1~4 → [20, 30, 40, 50]
// 3. 配合 .rev() 反向迭代
for i in (1..=5).rev() {
print!("{} ", i); // 5 4 3 2 1
}
println!();
// 4. 范围配合 .step_by() 设置步长(需要稳定版,此处为示意)
for i in (0..10).step_by(3) {
print!("{} ", i); // 0 3 6 9
}
}
记忆技巧:
..(两点)= 不包含右边的数;..=(两点一横)= 包含右边的数。这个约定与切片索引(第 14 节)的规则一致,记住“两点不包右“就够了。
7. 函数
7.1 功能作用
函数用于封装可复用的代码块。Rust 代码使用 fn 关键字定义函数,主函数 main 是程序的入口。
7.2 基本规则
// 无返回值(实际上返回单元类型 ())
fn 函数名(参数名: 参数类型, ...) {
// 函数体
}
// 有返回值(必须用箭头 -> 指定返回类型)
fn 函数名(参数名: 参数类型, ...) -> 返回类型 {
// 函数体,最后一条表达式不带分号作为返回值
// 或者使用 return 语句提前返回
}
- 参数声明:每个参数必须注明类型,多个参数用逗号分隔。
- 返回值:在
->后声明类型。函数体的最后一个表达式的值会被隐式返回,也可以使用return提前返回。 - 语句与表达式:以分号结尾的是语句(不返回值);不以分号结尾的是表达式(返回值)。
7.3 具体示例
// 无返回值,打印 hello
fn say_hello(name: &str) {
println!("Hello, {}!", name);
}
// 有返回值,返回两个整数之和
fn add(a: i32, b: i32) -> i32 {
a + b // 注意没有分号,这是一个表达式
}
// 使用 return 提前返回
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
return None;
}
Some(numerator / denominator)
}
fn main() {
say_hello("Rust");
let sum = add(3, 5);
println!("sum = {}", sum);
}
8. Option、Result 和 match
说明:本节仅介绍
Option、Result和match的基本语法和大致功能。它们是 Rust 错误处理体系的核心,第五章:返回值和错误处理 会深入讲解unwrap/expect、?操作符、错误传播、自定义错误类型等完整内容。
8.1 Option 类型
8.1.1 功能作用
表示一个值可能存在也可能不存在,用于避免空指针问题。Option<T> 有两种变体:
Some(T):包含一个类型为T的值。None:表示没有值。
8.1.2 基本规则
// 创建
let some_val = Some(具体值);
let none_val: Option<类型> = None;
// 取值前通常需要用 match 或 if let 解构
8.1.3 具体示例
fn find_first_char(s: &str) -> Option<char> {
s.chars().next() // 如果 s 为空则返回 None
}
fn main() {
let text = "hello";
match find_first_char(text) {
Some(ch) => println!("第一个字符是 {}", ch),
None => println!("字符串为空"),
}
}
8.2 Result 类型
8.2.1 功能作用
表示一个操作可能成功也可能失败,用于错误处理。Result<T, E> 有两种变体:
Ok(T):操作成功,包含结果值T。Err(E):操作失败,包含错误信息E。
8.2.2 基本规则
// 通常由可能失败的函数返回
let result: Result<类型, 错误类型> = 可能失败的函数();
// 处理时使用 match 或 ? 操作符
8.2.3 具体示例
#![allow(unused)]
fn main() {
use std::fs::File;
fn open_file() {
let file_result = File::open("hello.txt");
match file_result {
Ok(file) => println!("文件打开成功: {:?}", file),
Err(e) => println!("打开文件失败: {}", e),
}
}
}
8.3 match 控制流运算符
8.3.1 功能作用
将一个值与多个模式逐一匹配,并执行匹配到的分支代码。match 是 Rust 中最强大的模式匹配工具,要求必须穷尽所有可能的情况。
8.3.2 基本规则
match 值 {
模式1 => 表达式1,
模式2 => 表达式2,
...
_ => 默认表达式, // 下划线匹配所有未列出的情况
}
- 每个分支由
模式 => 表达式组成,分支之间用逗号分隔。 match本身也是表达式,可以返回值(所有分支返回值类型必须一致)。
8.3.3 具体示例(结合 Option 和 Result)
fn divide(x: f64, y: f64) -> Option<f64> {
if y == 0.0 { None } else { Some(x / y) }
}
fn main() {
let result = divide(10.0, 2.0);
match result {
Some(value) => println!("结果是 {}", value),
None => println!("除数不能为零"),
}
let num_str = "42";
let parse_result: Result<i32, _> = num_str.parse();
match parse_result {
Ok(num) => println!("解析成功: {}", num),
Err(e) => println!("解析失败: {}", e),
}
}
9. 强制类型转换 (as)
9.1 功能作用
as 运算符用于在 Rust 中进行显式类型转换(又称“类型铸造”)。它可以将一种原始类型转换为另一种原始类型(如整数、浮点数、字符),也可以做某些指针相关转换。as 不会破坏内存安全,但不等于“语义上安全”:它可能截断整数、丢失浮点精度,或改变数值含义。
9.2 基本规则
值 as 目标类型
- 只能转换原始类型(
i8、u16、f32、char、bool等)以及某些指针/引用之间的转换。 - 整数截断:将大整数转换为小整数时,高位会被丢弃(相当于取模)。
- 浮点转整数会向零舍入(截断小数部分);超出目标整数范围时会饱和到边界值。
- 整数转浮点可能丢失精度。
as不会用于自定义类型之间的转换(应使用From/Intotrait)。
9.3 具体示例
fn main() {
// 整数之间的转换
let a: i32 = 300;
let b = a as i8; // 300 转换为 i8 → 44 (因为 300 % 256 = 44)
println!("b = {}", b);
// 浮点转整数(截断)
let pi = 3.14159;
let int_pi = pi as i32; // 3
println!("int_pi = {}", int_pi);
// 整数转浮点
let x = 5;
let y = x as f64; // 5.0
// 字符转整数(得到 Unicode 码点)
let c = 'A';
let code = c as u32; // 65
// u8 转字符(得到对应的 Unicode 标量值)
let code: u8 = 97;
let ch = code as char; // 'a'
// bool 转整数(false→0, true→1)
let t = true;
println!("true as i32 = {}", t as i32); // 1
}
10. 普通数组 (Array)
10.1 功能作用
数组是固定长度、相同类型的元素集合,存储在栈上。数组的长度在编译时已知,不可改变。当你需要存储多个相同类型且数量固定的数据时,数组比向量(Vec)更高效。
10.2 基本规则
#![allow(unused)]
fn main() {
// 类型声明:[类型; 长度]
let 数组名: [类型; 长度] = [元素1, 元素2, ...];
// 自动推导类型
let 数组名 = [元素1, 元素2, ...];
// 初始化所有元素为相同值
let 数组名 = [初始值; 长度];
// 访问元素(索引从 0 开始)
let 元素 = 数组名[索引];
}
- 数组长度是类型的一部分,例如
[i32; 5]和[i32; 6]是不同的类型。 - 访问数组元素时,Rust 会进行边界检查,越界访问会导致程序 panic。
- 可以使用
数组名.len()获取长度。 - 遍历数组通常使用
for循环或迭代器。
10.3 具体示例
fn main() {
// 显式指定类型和长度
let arr1: [i32; 3] = [1, 2, 3];
// 自动推断
let arr2 = [10, 20, 30, 40];
// 所有元素初始化为 0,长度 5
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
// 访问和修改(如果数组可变)
let mut arr3 = [5, 6, 7];
arr3[1] = 60; // 修改第二个元素
println!("arr3[1] = {}", arr3[1]);
// 获取长度
println!("arr2 有 {} 个元素", arr2.len());
// 越界访问(会 panic,演示时注释掉)
// let out = arr2[10]; // 编译通过,运行时 panic
// 遍历数组
for elem in &arr2 {
print!("{} ", elem);
}
println!();
// 使用索引遍历
for i in 0..arr2.len() {
print!("arr2[{}] = {} ", i, arr2[i]);
}
}
11. String 基础使用
说明:本节覆盖
String的常用操作,快速上手够用。String作为 Rust 的核心集合类型之一,它的内存布局(栈上的指针+堆上的数据)、与&str的关系、UTF-8 编码带来的遍历注意事项等,第十章:容器 会做更系统的讲解。
String 是 Rust 中可增长的、拥有所有权的 UTF-8 字符串类型。与字符串字面量 &str 不同,String 可以在运行时修改。
11.1 创建
| 方法/函数 | 接口签名 | 说明 |
|---|---|---|
String::new() | pub fn new() -> String | 创建一个空的 String |
String::from() | pub fn from(s: &str) -> String | 从字符串字面量创建 |
to_string() 方法 | pub fn to_string(&self) -> String | 将实现了 Display 的类型转为 String |
一行调用示例:
#![allow(unused)]
fn main() {
let empty = String::new(); // 创建空字符串
let hello = String::from("Hello"); // 从 &str 创建
let num_str = 42.to_string(); // 数字转 String
}
11.2 增加(追加内容)
| 方法 | 接口签名 | 说明 |
|---|---|---|
push_str() | pub fn push_str(&mut self, string: &str) | 追加一个字符串切片 |
push() | pub fn push(&mut self, ch: char) | 追加一个字符 |
+ 运算符 | fn add(self, other: &str) -> String | 拼接,会消耗左侧 String |
一行调用示例:
#![allow(unused)]
fn main() {
let mut s = String::from("Hello");
s.push_str(" World"); // 追加字符串
s.push('!'); // 追加字符
let s2 = s + " Rust"; // 拼接(注意 s 的所有权被移动)
}
11.3 转换(与其他类型互转)
| 转换方向 | 常用方法/函数 | 一行调用示例 |
|---|---|---|
String → &str | as_str() 或 & 解引用 | let slice: &str = s.as_str(); 或 let slice = &s; |
String → Vec<u8> | into_bytes() | let bytes = s.into_bytes(); |
&str → String | to_string() 或 String::from() | let s = "hi".to_string(); |
String → 数字 | parse() | let num: i32 = "42".parse().unwrap(); |
数字 →String | to_string() 或 format! | let s = 3.14.to_string(); |
一行调用示例:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let slice = s.as_str(); // String → &str
let bytes = s.into_bytes(); // String → Vec<u8> (消耗 s)
let back = String::from("world"); // &str → String (已演示)
let num = "100".parse::<i32>().unwrap(); // &str → i32
let num_str = 256.to_string(); // i32 → String
}
12. 借用 (Borrowing)
说明:这里介绍借用的基本概念和规则,让你能看懂和写出简单的借用代码。借用是 Rust 所有权系统的核心组成部分,第四章:Rust 内存管理 中的所有权机制章节会深入讲解移动语义、借用规则的原理、以及编译器如何通过借用检查在编译期保证内存安全。
12.1 功能作用
借用是 Rust 所有权系统中的一个核心概念。它允许你临时访问一个值而不获取其所有权。通过借用(即创建引用),你可以传递数据给函数或进行其他操作,而原变量仍然保持所有权。借用分为不可变借用(&T)和可变借用(&mut T)。
12.2 基本规则
- 不可变借用:可以同时存在多个,但不能与可变借用共存。不能通过不可变借用修改数据。
- 可变借用:同一时间只能有一个可变借用,且在可变借用存在期间,不能有任何其他借用(不可变或可变)。
- 借用必须始终有效:引用的生命周期不能超过被引用数据的生命周期(编译器通过生命周期检查确保)。
12.3 具体示例
fn main() {
let mut data = 42;
// 不可变借用(多个允许)
let ref1 = &data;
let ref2 = &data;
println!("ref1 = {}, ref2 = {}", ref1, ref2);
// ref1 和 ref2 最后一次使用之后,不可变借用结束,可以再进行可变借用
// 可变借用(只能有一个)
let ref_mut = &mut data;
*ref_mut += 1; // 通过可变引用修改原值
println!("现在 data = {}", data);
// 下面的代码会编译错误(取消注释查看):
// let ref3 = &data; // 如果后面还要继续使用 ref_mut,这里就不能再借用 data
// 当 ref_mut 不再使用后,又可以创建新的借用
}
// 函数参数使用借用,不获取所有权
fn calculate_length(s: &String) -> usize {
s.len() // s 是引用,函数结束后不会 drop 原 String
}
fn modify(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
let len = calculate_length(&s); // 不可变借用
println!("长度: {}", len);
modify(&mut s); // 可变借用
println!("修改后: {}", s); // "hello world"
}
简单理解:借用就像是“借阅”一本书——你可以看(不可变借用)或者修改(可变借用),但书的所有权仍然属于原主人。多人同时看一本书没问题,但有人修改时其他人不能看或改。
13. 指针、取地址与解引用
Rust 中有三种“指针“相关的基础语法,它们和借用紧密配合使用。
13.1 取地址 / 创建引用:& 和 &mut
功能:& 操作符用来创建引用(也就是“取地址“)。它不获取数据的所有权,只是“借来看“或“借来改“。
| 语法 | 创建的引用类型 | 含义 |
|---|---|---|
&变量 | &T(不可变引用) | 借来读,不能改 |
&mut 变量 | &mut T(可变引用) | 借来读写,独占 |
示例:
#![allow(unused)]
fn main() {
let x = 42;
let r = &x; // &i32,不可变引用,可以读 x 但不能改 x
println!("r = {}", r);
let mut y = 10;
let r_mut = &mut y; // &mut i32,可变引用,可以读写 y
*r_mut += 1; // 通过可变引用修改 y
println!("y = {}", y);
}
13.2 解引用:*
功能:* 操作符用于解引用——“穿过“引用,访问或修改它指向的实际数据。
示例:
#![allow(unused)]
fn main() {
let x = 10;
let r = &x;
println!("*r = {}", *r); // 解引用:读取引用指向的值,输出 10
// *r = 20; // 编译错误:不可变引用不能修改
let mut y = 5;
let r_mut = &mut y;
*r_mut = 100; // 解引用 + 赋值:修改引用指向的值
println!("y = {}", y); // 输出 100
}
自动解引用:Rust 在某些情况下会自动插入 *,你不用手写。最常见的是 . 方法调用——编译器会自动对引用解引用以找到匹配的方法:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let r = &s;
// 下面两行等价,编译器自动帮你解引用:
println!("长度: {}", r.len()); // 自动解引用,推荐写法
println!("长度: {}", (*r).len()); // 手动解引用,多余但有效
}
13.3 裸指针:*const T 和 *mut T
除了安全的引用(&T / &mut T),Rust 还保留了类似 C 语言的裸指针(raw pointer),用于底层操作和 FFI 场景:
| 类型 | 含义 |
|---|---|
*const T | 不可变的裸指针 |
*mut T | 可变的裸指针 |
特点:
- 裸指针不受借用检查器的保护——编译器不追踪它们的生命周期,不检查它们是否有效。
- 解引用裸指针必须在
unsafe块中进行。 - 裸指针不实现自动释放(没有
Drop),可以随意拷贝,不计入所有权系统。
示例:
#![allow(unused)]
fn main() {
let x = 42;
let p: *const i32 = &x; // 从引用转换为裸指针(安全操作)
// println!("{}", *p); // 编译错误:解引用裸指针需要 unsafe
unsafe {
println!("{}", *p); // 在 unsafe 块中可以解引用
}
let mut y = 10;
let p_mut: *mut i32 = &mut y;
unsafe {
*p_mut += 1; // 通过可变裸指针修改值
println!("{}", *p_mut); // 11
}
}
何时用裸指针? 日常写 Rust 几乎用不到——引用和智能指针覆盖了绝大多数场景。裸指针主要用于:调用 C 语言的 FFI 接口、实现某些高性能数据结构、或者在不适用安全引用规则的极端场景中。如果你刚学 Rust,先专注于
&T/&mut T即可。
14. 切片 (Slice)
14.1 切片的概念
切片是对一个集合(如数组、Vec、字符串)中连续一段元素的引用。它不拥有数据,只是借用。可以把切片想象成“指向原始数据中间某一段的窗口”:
- 切片本身不复制数据,因此非常高效。
- 通过切片,我们可以用统一的方式处理不同长度的数据块。
- 切片具有运行时确定的长度,而数组的长度是编译时固定的。
生活的理解: 有一整条面包(原始数组),切片就是切下来的一段(比如第3片到第5片),但这段面包并没有被拿走,只是告诉别人“你看这段”。原始面包仍然完整。
在 Rust 中的表现形式:
- 数组切片类型:
&[T](不可变)或&mut [T](可变) - 字符串切片类型:
&str(总是不可变)
切片总是通过引用(&)来创建和传递。
14.2 切片的通用语法格式
语法 &集合[范围表达式]
&:创建一个引用(切片本质是引用)。集合:可以是数组、Vec、String等支持范围索引的类型。范围表达式:使用..或..=指定起止位置。
14.2.1 ⚠️ 重要:范围是 左闭右开(除非使用 ..=)
[start..end] 表示从 start 开始,到 end 之前结束,即包含 start,不包含 end。
这是 Rust 范围(Range)的默认行为,与很多编程语言(如 Python、Go)中的切片规则一致。
| 写法 | 含义 | 等效区间 | 是否包含 end |
|---|---|---|---|
&coll[..] | 整个集合 | 0 .. len | - |
&coll[start..] | 从索引 start 到末尾 | start .. len | - |
&coll[..end] | 从开头到索引 end(不包含 end) | 0 .. end | ❌ 不包含 |
&coll[start..end] | 从 start 到 end(不包含 end) | start .. end | ❌ 不包含 |
&coll[start..=end] | 从 start 到 end(包含 end) | start ..= end | ✅ 包含 |
重要规则:
- 范围边界必须落在集合的合法索引内,否则程序会 panic。
- 对于字符串切片,范围还必须落在 UTF-8 字符的字节边界上,否则 panic。
- 切片的结果类型取决于原始集合:
- 数组 →
&[T] Vec→&[T]String或字符串字面量 →&str
- 数组 →
示例(数组切片):
#![allow(unused)]
fn main() {
let arr = [10, 20, 30, 40];
let s1 = &arr[1..3]; // [20, 30]
let s2 = &arr[..2]; // [10, 20]
let s3 = &arr[2..]; // [30, 40]
let s4 = &arr[..]; // [10, 20, 30, 40]
}
14.3 数组切片
类型:&[T](不可变)或 &mut [T](可变)
功能:指向数组或 Vec 中一段连续元素。
14.3.1 创建数组切片
使用 &数组名[范围] 即可。
#![allow(unused)]
fn main() {
let arr = [5, 10, 15, 20, 25];
// 取索引 1~3(不含 3)→ [10, 15]
let slice1 = &arr[1..3];
// 从开头到索引 2(不含 2)→ [5, 10]
let slice2 = &arr[..2];
// 从索引 3 到末尾 → [20, 25]
let slice3 = &arr[3..];
// 整个数组切片 → [5, 10, 15, 20, 25]
let whole = &arr[..];
}
14.3.2 使用数组切片
切片支持索引访问、len() 方法、迭代等操作。
fn print_slice(s: &[i32]) {
println!("切片长度: {}, 第一个元素: {}", s.len(), s[0]);
for &val in s {
print!("{} ", val);
}
println!();
}
fn main() {
let arr = [1, 2, 3, 4, 5];
let mid = &arr[1..4]; // [2, 3, 4]
print_slice(mid);
}
14.3.3 可变数组切片
如果原始数组是可变的,可以创建可变切片 &mut [T],并通过它修改原数组中的元素。
fn add_one(slice: &mut [i32]) {
for elem in slice.iter_mut() {
*elem += 1;
}
}
fn main() {
let mut arr = [10, 20, 30];
{
let slice = &mut arr[0..2]; // 可变借用 [10, 20]
slice[0] = 99;
add_one(slice); // [100, 21]
} // slice 作用域结束
println!("{:?}", arr); // [100, 21, 30]
}
14.4 字符串切片
类型:&str(总是不可变)
功能:指向 String 或字符串字面量中的一段连续 UTF-8 字符。
14.4.1 创建字符串切片
使用 &字符串变量[范围] 或直接对字符串字面量切片。
#![allow(unused)]
fn main() {
let s = String::from("你好Rust");
// 注意:'你' 占 3 字节,'好' 占 3 字节,'R','u','s','t' 各占 1 字节
let slice1 = &s[0..3]; // "你"
let slice2 = &s[3..6]; // "好"
let slice3 = &s[6..10]; // "Rust"
let whole = &s[..]; // "你好Rust"
// 字符串字面量本身就是 &str 类型,也可切片
let literal = "Hello世界";
let hello = &literal[0..5]; // "Hello"
let world = &literal[5..]; // "世界"('世' 3 字节,'界' 3 字节)
}
14.4.2 使用字符串切片
字符串切片支持 len()(返回字节数,不是字符数)、chars() 遍历字符等。
#![allow(unused)]
fn main() {
let text = String::from("Rust编程");
let sub = &text[4..]; // "编程"
println!("子串: {}", sub);
println!("字节长度: {}", sub.len()); // 6('编'3字节 + '程'3字节)
println!("字符个数: {}", sub.chars().count()); // 2
// 遍历字符
for ch in sub.chars() {
println!("{}", ch);
}
}
一句话总结:切片就是“不拿所有权,只看一段”的引用。数组切片让你灵活处理数组片段,字符串切片让你安全高效地截取子串。掌握切片是写出高效 Rust 代码的关键一步。
15. 标准输入输出
14.1 标准输出 – println! 和 print!
14.1.1 功能作用
println! 和 print! 是宏,用于向标准输出(通常是控制台)打印文本。println! 会自动在末尾添加换行,print! 不会。
14.1.2 基本规则
println!() // 只打印一个换行
println!("文本") // 打印字符串并换行
println!("{}", 变量) // 使用 `{}` 占位符(实现了 `Display` trait)
println!("{:?}", 变量) // 使用 `{:?}` 占位符(实现了 `Debug` trait)
println!("{0} {1}", a, b) // 按索引使用参数
println!("{name}: {value}", name="Rust", value=42) // 命名参数
print!("不换行") // 类似但无换行
{}用于实现std::fmt::Display的类型(如整数、字符串)。{:?}用于实现std::fmt::Debug的类型(几乎所有类型都可派生 Debug)。- 可以使用多个占位符,按顺序匹配参数。
14.1.3 具体示例
fn main() {
let name = "Alice";
let age = 30;
println!("Hello, {}! You are {} years old.", name, age);
println!("Debug: {:?}", (name, age));
print!("同一行"); // 不会换行
println!("继续"); // 会接在上一行后面然后换行
}
14.2 标准输入 – stdin()
14.2.1 功能作用
从标准输入(通常是键盘)读取用户输入。常用方法包括 read_line 读取整行,或使用 bytes() / chars() 逐个读取。
14.2.2 基本规则
use std::io;
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取失败");
// input 现在包含用户输入的一整行(末尾包含换行符)
// 清空 String 以便重用
input.clear();
read_line将读取所有字节直到遇到换行符,并追加到给定的String中。- 返回
Result<usize, io::Error>,通常用unwrap()或expect()处理。 - 读取后记得
.trim()去掉首尾空白(包括换行符)。
14.2.3 具体示例
use std::io;
fn main() {
let mut buffer = String::new();
println!("请输入您的名字:");
io::stdin().read_line(&mut buffer).expect("读取失败");
let name = buffer.trim(); // 去掉换行符
println!("你好,{}!", name);
}
14.3 组合使用:读取并解析数字
use std::io;
fn main() {
let mut input = String::new();
println!("请输入一个整数:");
io::stdin().read_line(&mut input).unwrap();
let num: i32 = input.trim().parse().expect("请输入有效的数字");
println!("您输入的是 {}", num);
}
16. 完整示例:猜数字游戏
下面是一个完整的猜数字游戏,包含了变量、循环、控制流、函数、match、Option/Result、数组(本例未用)、借用(read_line 使用了可变引用)以及标准输入输出等几乎所有介绍过的知识点。代码中使用了 rand crate 生成随机数。
15.1 代码实现
首先在 Cargo.toml 中添加依赖:
[dependencies]
rand = "0.8"
然后编写以下代码:
#![allow(unused)]
fn main() {
use rand::Rng;
use std::io;
pub fn demo() {
println!("...............基础语法示例开始...............");
guess_number(); // 第一次游戏
guess_number(); // 第二次游戏
println!("...............基础语法示例结束...............");
}
fn guess_number() {
// 生成 1~100 之间的随机数(闭区间)
let secret_number = rand::thread_rng().gen_range(1..=100);
let mut guess = String::new(); // 可变字符串,用于存储用户输入
let mut count = 1; // 计数器
// 最多允许猜 20 次
while count <= 20 {
println!("第 {} 次猜测,请输入一个数字:", count);
// 读取一行输入,存入 guess 字符串(这里传递了 &mut guess,即可变借用)
let n = io::stdin().read_line(&mut guess).unwrap();
count += 1;
// 将输入的字符串去除首尾空白后解析为 u32 整数
let num = guess.trim().parse::<u32>();
// 使用 match 处理解析结果(Result 类型)
match num {
Ok(num) => {
// 比较猜测值与秘密数字
if num == secret_number {
println!("恭喜!猜对了!");
break; // 猜对后退出循环
} else if num > secret_number {
println!("猜大了");
} else {
println!("猜小了");
}
}
Err(_) => {
// 解析失败,输入的不是有效数字
println!("请输入一个有效的数字!");
}
}
// 清空 guess 字符串,为下一次输入做准备
guess.clear();
}
// 如果循环结束是因为超过了 20 次,则提示失败
if count > 20 {
println!("很遗憾,20 次机会用完了。正确答案是 {}", secret_number);
}
}
}
15.2 代码讲解
-
引入依赖
use rand::Rng;引入随机数生成器 trait。use std::io;引入标准库的输入输出模块。
-
demo()函数简单地调用两次guess_number(),演示游戏可以进行多轮。 -
生成随机数
rand::thread_rng().gen_range(1..=100)生成一个 1 到 100 之间的整数(包含 100)。 -
可变变量
let mut guess = String::new();– 因为需要反复清空和读入,所以guess必须是可变的。let mut count = 1;– 计数器也要递增。 -
while循环条件count <= 20控制最多猜 20 次。 -
读取用户输入
io::stdin().read_line(&mut guess)将用户输入追加到guess字符串中,这里&mut guess就是可变借用。函数返回Result<usize, io::Error>,这里用unwrap()简单处理错误(实际生产代码应更严谨)。 -
字符串解析
guess.trim().parse::<u32>()–trim()去掉换行符和空格,parse尝试将字符串转换为u32整数。返回值是Result<u32, ParseIntError>。 -
match处理ResultOk(num)表示解析成功,进入比较逻辑。Err(_)表示解析失败,提示用户重新输入。
-
内部
if-else比较判断num与secret_number的关系,并打印相应提示。 -
break退出循环猜对时立刻退出循环,不再继续猜测。 -
清空字符串
guess.clear();清空旧内容,避免下次读取时附加到旧数据后面。 -
最终判断 循环结束后,如果
count > 20说明是因为耗尽次数而退出,此时公布答案。