Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 mutlet 遮蔽
本质修改同一个变量的值创建一个新变量,同名但独立
类型能否改变❌ 不能改变类型✅ 可以改变类型
旧变量被覆盖,不存在了被“藏起来“,新变量离开作用域后可能重新可见
是否需要 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 提供更简洁的写法。这里只需了解基本用法即可,更深入的模式匹配将在第五章:返回值和错误处理中结合 OptionResult 详细展开。

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 提供了三种循环结构:loopwhilefor。它们都用于重复执行一段代码,但适用场景不同。

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左闭右开,不包含 end1..51, 2, 3, 4
start..=end双闭区间,包含 end1..=51, 2, 3, 4, 5
..end从 0 到 end(不包含)..50, 1, 2, 3, 4
start..start 到无穷(惰性)3..3, 4, 5, ...
..=end从 0 到 end(包含)..=50, 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. OptionResultmatch

说明:本节仅介绍 OptionResultmatch 的基本语法和大致功能。它们是 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 目标类型
  • 只能转换原始类型i8u16f32charbool 等)以及某些指针/引用之间的转换。
  • 整数截断:将大整数转换为小整数时,高位会被丢弃(相当于取模)。
  • 浮点转整数会向零舍入(截断小数部分);超出目标整数范围时会饱和到边界值。
  • 整数转浮点可能丢失精度。
  • as 不会用于自定义类型之间的转换(应使用 From/Into trait)。

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&stras_str()& 解引用let slice: &str = s.as_str();let slice = &s;
StringVec<u8>into_bytes()let bytes = s.into_bytes();
&strStringto_string()String::from()let s = "hi".to_string();
String → 数字parse()let num: i32 = "42".parse().unwrap();
数字 →Stringto_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 切片的通用语法格式

语法 &集合[范围表达式]

  • &:创建一个引用(切片本质是引用)。
  • 集合:可以是数组、VecString 等支持范围索引的类型。
  • 范围表达式:使用 ....= 指定起止位置。

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不包含 end0 .. end❌ 不包含
&coll[start..end]startend不包含 endstart .. end❌ 不包含
&coll[start..=end]startend包含 endstart ..= 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. 完整示例:猜数字游戏

下面是一个完整的猜数字游戏,包含了变量、循环、控制流、函数、matchOption/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 代码讲解

  1. 引入依赖

    • use rand::Rng; 引入随机数生成器 trait。
    • use std::io; 引入标准库的输入输出模块。
  2. demo() 函数简单地调用两次 guess_number(),演示游戏可以进行多轮。

  3. 生成随机数 rand::thread_rng().gen_range(1..=100) 生成一个 1 到 100 之间的整数(包含 100)。

  4. 可变变量 let mut guess = String::new(); – 因为需要反复清空和读入,所以 guess 必须是可变的。let mut count = 1; – 计数器也要递增。

  5. while 循环条件 count <= 20 控制最多猜 20 次。

  6. 读取用户输入 io::stdin().read_line(&mut guess) 将用户输入追加到 guess 字符串中,这里 &mut guess 就是可变借用。函数返回 Result<usize, io::Error>,这里用 unwrap() 简单处理错误(实际生产代码应更严谨)。

  7. 字符串解析 guess.trim().parse::<u32>()trim() 去掉换行符和空格,parse 尝试将字符串转换为 u32 整数。返回值是 Result<u32, ParseIntError>

  8. match 处理 Result

    • Ok(num) 表示解析成功,进入比较逻辑。
    • Err(_) 表示解析失败,提示用户重新输入。
  9. 内部 if-else 比较判断 numsecret_number 的关系,并打印相应提示。

  10. break 退出循环猜对时立刻退出循环,不再继续猜测。

  11. 清空字符串 guess.clear(); 清空旧内容,避免下次读取时附加到旧数据后面。

  12. 最终判断 循环结束后,如果 count > 20 说明是因为耗尽次数而退出,此时公布答案。