Rustlings 入门

关键资源与配置指令

学习 Rust 前需完成环境配置,并储备核心学习资源,以下是经过验证的可靠资源和关键指令,确保环境配置顺利、学习有支撑:

Rustlings 核心练习对应

本节入门课程与 Rustlings 中的多个练习模块直接对应,建议结合课程知识点同步练习,实现理论与实操的深度结合,具体对应练习模块如下:variables(变量)、functions(函数)、if(条件判断)、primitive_types(基本数据类型)、vecs(向量)、move_semantics(所有权转移)、structs(结构体)、strings(字符串),每完成一个知识点的学习,及时练习对应模块的题目,巩固知识点记忆。

Rust 基础语法

程序入口与基础结构

与大多数编程语言类似,Rust 程序的入口的是 main 函数,所有可执行代码都会从 main 函数开始执行。最基础、最经典的 Rust 程序是 Hello World 程序,其代码简洁易懂,可快速熟悉 Rust 的基本代码结构:

1
2
3
fn main(){
println!("Hello, world!");
}

在实际开发中,我们通常会通过 cargo 工具创建和管理 Rust 工程,cargo 是 Rust 官方提供的构建工具,可自动处理依赖、编译、运行等流程,具体工程创建与运行步骤如下:

  • 创建工程:在终端中输入指令 cargo new hello,即可创建一个名为“hello”的 Rust 工程,工程会自动生成标准的目录结构(src 文件夹、Cargo.toml 配置文件等)。
  • 运行工程:进入工程目录(cd hello),输入指令 cargo run,cargo 会自动编译工程代码,生成可执行文件,然后执行该文件,最终在终端输出“Hello, world!”,整个过程无需手动处理编译细节。

变量与可变性

变量声明与可变性

变量是 Rust 中存储数据的基本单元,其声明和使用有明确的规则,核心特点是“默认不可变”,这是 Rust 保障内存安全和并发安全的重要设计之一,具体规则如下:

  • 使用 let 关键字声明变量,默认情况下变量是不可变的(immutable),一旦将某个值绑定到变量上,就无法修改该变量的值,这种设计可有效防止意外的数据修改,尤其在并发场景中,能避免数据竞争问题。
  • 如果需要修改变量的值,需在 let 关键字后添加 mut 关键字,声明为可变变量,这样就可以随时修改变量绑定的值,满足实际开发中需要动态更新数据的场景。
  • 示例:通过简单代码对比不可变变量和可变变量的使用差异,更直观理解其规则:
1
2
3
4
let x = 5; // 不可变变量,无法修改
let mut y = 5; // 可变变量,可修改
y = 6; // 合法,可变变量可重新赋值
// x = 6; // 错误,不可变变量无法重新赋值

常量

常量与不可变变量类似,都是无法修改的值,但二者在使用场景、声明方式和特性上有明显区别,常量的核心规则如下:

  • 使用 const 关键字声明常量,与 let 声明的不可变变量不同,常量总是不可变的,且不允许使用 mut 关键字修饰,其值从声明时就固定,无法在任何场景下修改。
  • 常量只能赋值为常量表达式,不能是运行时才能计算出的值(如函数返回值、用户输入的值等),这是因为常量需要在编译阶段确定其值,确保程序运行时无需额外计算,提升执行效率。
  • 示例:声明一个表示“三小时秒数”的常量,清晰展示常量的使用方式:const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

变量隐藏(Shadowing)

变量隐藏是 Rust 中一个特殊的特性,指的是可以在同一个作用域内,定义与之前变量同名的新变量,新变量会覆盖旧变量的绑定,且新变量的类型可以与旧变量不同。需要注意的是,变量隐藏与可变变量(mut)有本质区别:mut 是修改同一个变量的值,而隐藏是重新绑定一个新变量,旧变量会被隐藏,无法再访问。

1
2
3
4
5
6
fn main() {
let x = 5; // 第一次声明x,类型为i32,值为5
let x = x + 1; // 隐藏旧x,新x的值为6,类型仍为i32
let x = x * 2; // 再次隐藏,新x的值为12,类型仍为i32
println!("The value of x is: {x}"); // 输出12,访问的是最后一次隐藏的x
}

基本数据类型

Rust 的基本数据类型分为标量类型和复合类型两大类,标量类型用于表示单个值,复合类型用于表示多个值的组合,掌握这些基本类型是编写 Rust 代码的基础。

标量类型(单个值类型)

标量类型是 Rust 中最基础的数据类型,每种标量类型都有明确的取值范围和用途,具体分为以下6类:

  • 有符号整数:用于表示正数、负数和零,包含 i8、i16、i32、i64、i128、isize 六种,后缀数字表示位数(如 i8 表示 8 位有符号整数),其中 isize 的位数与平台指针宽度一致(32位平台为32位,64位平台为64位),适用于表示内存地址相关的数值。
  • 无符号整数:仅用于表示非负数(正数和零),包含 u8、u16、u32、u64、u128、usize 六种,后缀数字表示位数,usize 与平台指针宽度一致,常用于表示数组索引、集合长度等非负数值。
  • 浮点数:用于表示带有小数部分的数值,包含 f32(单精度浮点数)和 f64(双精度浮点数)两种,其中 f64 是 Rust 的默认浮点数类型,精度更高,运算更稳定,大多数场景下推荐使用 f64。
  • char(字符):用于表示单个 Unicode 字符,不仅可以表示英文字母、数字、符号,还可以表示中文、日文、特殊符号等,每个 char 类型占 4 个字节,声明时用单引号包裹(如 ‘a’、’α’、’∞’、’中’)。
  • bool(布尔型):用于表示逻辑判断结果,仅包含 true(真)和 false(假)两个值,常用于 if 条件判断、循环控制等场景,占用 1 个字节的内存。
  • 单元类型(unit type):用 () 表示,其唯一的值也是 (),通常用于表示无返回值的函数(Rust 中没有明确的“无返回值”,而是返回单元类型),也可用于表示不需要实际意义的值。

复合类型(多个值组合)

复合类型可以将多个不同类型(或相同类型)的值组合在一起,Rust 中最常用的复合类型是元组和数组,二者的核心区别是:元组长度固定、元素类型可不同,数组长度固定、元素类型必须相同。

元组(tuple)

元组是一种灵活的复合类型,适用于将多个不同类型的值组合成一个整体,其核心特性和使用方法如下:

  • 长度固定:一旦声明元组,其包含的元素个数就无法增减,即使是添加或删除元素,也需要重新声明一个新的元组。
  • 元素类型可不同:元组中的每个元素可以是不同的基本数据类型,用圆括号包裹,元素之间用逗号分隔,声明时可指定每个元素的类型,也可让 Rust 自动推断。
  • 访问方式:有两种常用访问方式,一是通过“点号 + 索引”访问单个元素(索引从 0 开始),二是通过模式匹配(解构)将元组的元素分别绑定到变量上,方便一次性访问多个元素。
  • 示例:通过代码展示元组的声明、类型指定和访问方式:
1
2
3
4
5
6
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1); // 声明类型的元组,包含i32、f64、u8三种类型
let (x, y, z) = tup; // 模式匹配解构,将元组元素分别绑定到x、y、z变量
println!("The value of y is: {y}"); // 输出6.4,访问解构后的y变量
println!("The third element is: {}", tup.2); // 通过索引访问第三个元素,输出1
}

数组(array)

数组是一种用于存储多个相同类型值的复合类型,其内存是连续分配的,适用于存储固定数量的同类型数据,核心特性和使用方法如下:

  • 长度固定:与元组类似,数组的长度一旦声明就无法修改,若需要动态增减元素,应使用后续会介绍的 Vector 类型。
  • 元素类型必须相同:数组中的所有元素必须是同一种基本数据类型,不能混合不同类型的值,这是数组与元组的核心区别之一。
  • 存储位置:数组存储在栈上,而非堆上,访问速度更快,适合存储数量固定、无需动态修改的数据。
  • 声明方式:有两种常用声明方式,一是明确指定元素类型和长度([元素类型; 长度]),二是直接初始化元素,让 Rust 自动推断类型;此外,还可以使用“[初始值; 元素个数]”的快捷方式,创建所有元素均为初始值的数组。
  • 示例:通过代码展示数组的多种声明和访问方式:
1
2
3
4
5
6
7
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5]; // 常规声明,指定类型为i32,长度为5
let b = [3; 5]; // 快捷初始化,等价于[3,3,3,3,3],类型自动推断为[i32; 5]
let element = a[0]; // 索引访问,获取第一个元素1
println!("The first element: {}", element); // 输出1
println!("The length of array b: {}", b.len()); // 输出数组长度5
}

函数

函数是 Rust 中组织代码的核心单元,用于封装可复用的逻辑,通过函数可以将复杂代码拆分为多个简单模块,提升代码的可读性和可维护性,其核心特性和使用方法如下:

  • 声明方式:使用 fn 关键字声明函数,函数名称遵循 snake case 命名规范(全小写字母,单词之间用下划线分隔),例如 another_function、calculate_length 等,避免使用驼峰命名。
  • 入口函数:main 函数是 Rust 程序的唯一入口,无需参数,也无需返回值(默认返回单元类型 ()),程序运行时会首先执行 main 函数中的代码。
  • 函数调用与参数:函数可以嵌套调用(在一个函数中调用另一个函数),函数参数需明确指定类型(Rust 不允许省略参数类型),返回值可通过箭头 -> 指定类型,若省略返回值类型,则默认返回单元类型 ()。
  • 示例:通过简单的函数声明和调用,展示函数的基本使用:
1
2
3
4
5
6
7
8
fn main() {
println!("Hello, world!");
another_function(); // 调用无参数、无返回值的函数
}

fn another_function() { // 无参数、无返回值函数,省略返回值类型
println!("Another function.");
}

流程控制

流程控制用于控制程序的执行顺序,Rust 提供了三种核心流程控制结构:if 表达式(条件判断)、loop 循环、while 循环、for 循环(循环执行),其中 if 是表达式而非语句,这是 Rust 与其他语言的一个重要区别。

if 表达式

if 表达式用于根据不同的条件执行不同的代码分支,其核心特点是“条件无需加括号”,且可以作为表达式赋值给变量,具体规则和使用方法如下:

  • 条件判断:if 后面的条件是一个 bool 类型的值(true 或 false),无需用圆括号包裹,条件为 true 时执行 if 后面的代码块,条件为 false 时执行 else 后面的代码块(else 可选)。
  • 表达式特性:与其他语言不同,Rust 中的 if 是表达式,而非语句,这意味着 if 表达式会有返回值,返回值是执行分支中最后一条语句的值,且所有分支的返回值类型必须一致,否则会报错。
  • 示例:通过代码展示 if 表达式的条件判断和赋值用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let number = 3;
if number < 5 {
println!("condition was true"); // number<5为true,执行该语句
} else {
println!("condition was false"); // 否则执行该语句
}

// if 作为表达式赋值,两个分支返回值类型均为i32
let condition = true;
let number = if condition { 5 } else { 6 }; // number 为5
println!("The value of number is: {}", number); // 输出5
}

循环

循环用于重复执行一段代码,Rust 提供了三种循环方式,分别适用于不同场景:loop(无限循环)、while(条件循环)、for(遍历循环),可根据实际需求选择合适的循环方式。

loop 循环

loop 循环是 Rust 中最基础的循环方式,本质是无限循环,需通过 break 关键字手动终止循环,适用于不确定循环次数、需要手动控制终止时机的场景,其核心特性如下:

  • 无限循环:loop 后面的代码块会一直重复执行,直到遇到 break 关键字才会终止,若忘记添加 break,程序会进入死循环。
  • continue 关键字:用于跳过当前迭代的剩余代码,直接进入下一次循环,适用于需要跳过某些条件下的代码执行的场景。
  • 返回值特性:loop 循环可以作为表达式,通过 break 关键字返回一个值,该值会被赋值给接收变量,这是 loop 循环的一个实用特性。
  • 循环标签:当存在嵌套循环时,可给外层循环添加标签(标签前缀加 ‘),通过 break 或 continue 配合标签,指定作用的循环,避免影响内层循环。
  • 示例(从循环返回值):通过 loop 循环计算数值,终止时返回结果:
1
2
3
4
5
6
7
8
9
10
fn main() {
let result = loop {
let mut counter = 0;
counter += 1;
if counter == 10 {
break counter * 2; // 循环终止,返回counter*2的值(20)
}
};
println!("The result is {result}"); // 输出20,接收loop循环的返回值
}

while 循环

while 循环是条件循环,其核心逻辑是“条件为真时执行循环体,条件为假时终止循环”,适用于不确定循环次数,但已知循环终止条件的场景,相比 loop 循环,无需手动添加 break,更简洁高效。

1
2
3
4
5
6
7
8
fn main() {
let mut number = 3;
while number != 0 { // 循环条件:number不等于0
println!("{number}!"); // 输出当前number的值
number -= 1; // 每次循环number减1,逐步接近终止条件
}
println!("LIFTOFF!!!"); // 循环终止后执行,输出LIFTOFF!!!
}

for 循环

for 循环是 Rust 中最常用、最安全的循环方式,主要用于遍历集合(如数组、元组、Vector 等)或范围,无需手动控制索引,可有效避免索引越界问题,代码简洁易读,适用于已知遍历对象的场景。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a { // 遍历数组a,每次迭代将元素赋值给element
println!("the value is: {element}"); // 输出数组中的每个元素
}

for number in 1..=5 { // 遍历范围(1到5,包含5),..=表示包含结束值
println!("{number}"); // 输出1、2、3、4、5
}
// 若需不包含结束值,可使用..,如1..5表示1到4
}

自定义类型与常用集合

除了 Rust 提供的基本数据类型,我们还可以根据需求自定义类型(结构体),同时 Rust 也提供了多种常用集合类型,用于存储大量数据,其中最常用的是 Vector 和 HashMap。

结构体(struct)

结构体是 Rust 中用于自定义复合类型的核心语法,通过结构体可以将多个不同类型的字段组合在一起,形成一个新的类型,适用于描述具有多个属性的实体(如用户、商品、坐标等),其核心特性和使用方法如下:

  • 定义方式:使用 struct 关键字定义结构体,结构体名称遵循 PascalCase 命名规范(首字母大写,单词首字母也大写),结构体内部包含多个字段,每个字段需指定名称和类型,字段之间用逗号分隔。
  • 实例创建:创建结构体实例时,使用“结构体名 + 大括号”的形式,通过 key: value 的方式为每个字段赋值,字段的赋值顺序可以与结构体定义时的顺序不同,灵活便捷。
  • 字段访问:通过“实例名.字段名”的方式访问结构体实例的字段,若需要修改字段的值,需将结构体实例声明为可变(mut),不可变实例的字段无法修改。
  • 示例:通过定义 User 结构体,展示结构体的定义、实例创建和字段修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义结构体,描述用户信息,包含4个字段
struct User {
active: bool, // 是否活跃
username: String, // 用户名
email: String, // 邮箱
sign_in_count: u64, // 登录次数
}

fn main() {
// 创建可变的结构体实例,为每个字段赋值
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};

// 修改字段值(需结构体为可变),仅修改email字段
user1.email = String::from("anotheremail@example.com");
println!("User email: {}", user1.email); // 输出修改后的邮箱
}

元组结构体

元组结构体是结构体的一种特殊形式,它有结构体名称,但没有字段名,只有字段类型,适用于无需为字段命名、只需将多个不同类型的值组合成一个整体的场景。需要注意的是,即使两个元组结构体的字段类型完全相同,只要结构体名称不同,它们就是不同的类型,无法相互赋值或混用。

1
2
3
4
5
6
7
8
9
struct Color(i32, i32, i32); // 元组结构体,用于表示颜色(红、绿、蓝)
struct Point(i32, i32, i32); // 元组结构体,用于表示坐标(x、y、z)

fn main() {
let black = Color(0, 0, 0); // 创建Color实例,代表黑色
let origin = Point(0, 0, 0); // 创建Point实例,代表原点
// let error = Color(origin); // 错误,Color和Point是不同类型,无法混用
println!("Black color: ({}, {}, {})", black.0, black.1, black.2); // 通过索引访问字段
}

常用集合

集合用于存储大量数据,与数组、元组不同,集合的长度可以动态变化(部分集合),且存储在堆上,适用于需要存储不确定数量数据的场景,Rust 中最常用的两种集合是 Vector 和 HashMap。

Vector<T>

Vector(向量)是一种动态数组,可存储多个相同类型的值,内存连续分配,支持动态增减元素,适用于需要动态添加、删除数据的场景,其核心特性和使用方法如下:

  • 核心特性:动态可变长度,元素类型必须相同,存储在堆上,访问速度接近数组,支持随机访问(通过索引)。
  • 创建方式:有两种常用创建方式,一是使用 Vec::new() 方法创建空 Vector,需明确指定元素类型(Rust 无法自动推断空 Vector 的类型);二是使用 vec![] 宏创建,可直接初始化元素,Rust 会自动推断类型。
  • 常用方法:push(向 Vector 末尾添加元素)、len(获取 Vector 的长度)、get(通过索引获取元素,返回 Option<&T>,避免索引越界)、pop(删除并返回 Vector 末尾的元素)等。
  • 示例:通过代码展示 Vector 的创建和常用方法使用:
1
2
3
4
5
6
7
8
9
fn main() {
let mut v: Vec<i32> = Vec::new(); // 空vector,指定类型为i32
v.push(5); // 向vector末尾添加元素5
v.push(6); // 继续添加元素6
println!("Vector length: {}", v.len()); // 输出长度2

let v2 = vec![1, 2, 3]; // 用宏创建,自动推断类型为Vec<i32>
println!("The second element: {:?}", v2.get(1)); // 通过get获取索引1的元素,输出Some(2)
}

HashMap<K, V>

HashMap 是一种键值对集合,通过哈希函数将键(K)映射到值(V),键具有唯一性(不可重复),适用于需要通过键快速查找值的场景(如存储用户ID与用户信息的映射、成绩与学生姓名的映射等),其核心特性和使用方法如下:

  • 核心特性:键值对存储,键唯一,通过哈希函数实现快速查找,存储在堆上,长度可动态变化,键和值可以是任意类型(需满足一定条件)。
  • 导入方式:HashMap 不在 Rust 的 prelude(预导入模块)中,使用前需手动导入 use std::collections::HashMap;,否则无法使用。
  • 常用方法:insert(插入键值对,若键已存在,会覆盖原有值)、get(通过键获取值,返回 Option<&V>,若键不存在则返回 None)、contains_key(判断某个键是否存在)、remove(通过键删除键值对)等。
  • 示例:通过代码展示 HashMap 的导入、创建和常用方法使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::collections::HashMap;

fn main() {
let mut scores = HashMap::new(); // 创建空的HashMap,键为String类型,值为i32类型
scores.insert(String::from("Blue"), 10); // 插入键值对("Blue", 10)
scores.insert(String::from("Yellow"), 50); // 插入键值对("Yellow", 50)

// 获取值,copied()转换为值类型(避免处理引用),unwrap_or(0)指定默认值(键不存在时返回0)
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
println!("Team Blue score: {}", score); // 输出10

// 判断键是否存在
println!("Is team Red exist? {}", scores.contains_key(&String::from("Red"))); // 输出false
}

Rust 所有权机制

所有权机制是 Rust 最核心、最独特的特性,也是 Rust 保障内存安全、无需垃圾回收(GC)的关键,其核心思想是通过一套规则管理内存,确保程序运行时不会出现内存泄漏、空指针、数据竞争等问题,所有 Rust 开发者都必须熟练掌握。

所有权核心规则

Rust 所有权机制有三条核心规则,所有关于所有权的操作都必须遵循这三条规则,牢记这三条规则就能快速理解所有权的核心逻辑:

  • 所有权唯一:Rust 中的每一个值都有一个对应的所有者(owner),这个所有者通常是声明该值的变量。
  • 单一所有者:值在任一时刻有且只有一个所有者,不存在多个变量同时拥有同一个值的所有权的情况。
  • 作用域释放:当所有者(变量)离开其作用域(大括号闭合处),这个值将被自动丢弃,Rust 会自动调用 drop 方法释放该值占用的内存,无需手动管理内存。

示例:通过简单代码展示所有权的核心规则,理解值的所有权绑定与释放:

1
2
3
4
let x = 123; // x是123的所有者,123的所有权绑定到x
let y = x; // 123的所有权从x转移到y,此时y是唯一所有者,x被废弃
// println!("x: {}", x); // 错误,x已失去所有权,无法再访问
println!("y: {}", y); // 合法,y是当前所有者,可访问123

变量作用域

变量的作用域是指变量的有效范围,即变量从声明处开始,到当前作用域结束(大括号闭合处)之间的区域,在作用域内,变量可以被正常访问和使用;超出作用域后,变量将被自动销毁,其对应的内存会被 Rust 自动释放,这是所有权机制中“作用域释放”规则的具体体现。

1
2
3
4
5
6
7
8
fn main() {
// s 在这里无效,尚未声明,无法访问
{
let s = "hello"; // s 开始有效,s是"hello"的所有者
println!("{}", s); // 可使用s,输出hello
} // 作用域结束,s 无效,"hello"被自动丢弃,内存释放
// println!("{}", s); // 错误,s已超出作用域,无法访问
}

所有权转移(移动)

所有权转移(也称为“移动”)是指将一个值的所有权从一个变量转移到另一个变量,转移后,原变量失去所有权,无法再访问该值,新变量成为该值的唯一所有者,这是遵循“单一所有者”规则的核心操作。需要注意的是,所有权转移的行为会因值的类型不同而有所差异,主要分为基本类型和复合类型两类。

基本类型与复合类型的差异

Rust 中的类型分为“实现 Copy trait 的类型”和“未实现 Copy trait 的类型”,这两类类型在赋值时的所有权行为完全不同:

  • 基本类型(标量类型、元组/数组中的基本类型):这类类型大多实现了 Copy trait,赋值时会复制值的内容,所有权不会转移,原变量仍然拥有原值的所有权,可正常访问,新变量拥有复制后的值的所有权。
  • 复合类型(String、Vector、HashMap 等):这类类型未实现 Copy trait,赋值时不会复制值的内容,而是将所有权从原变量转移到新变量,原变量被废弃,无法再访问该值,避免了重复释放内存的问题。

函数参数与返回值的所有权转移

所有权转移不仅发生在变量赋值时,在函数调用过程中,将值传递给函数参数、函数返回值时,也会发生所有权转移,具体规则与变量赋值一致:

  • 参数传递:将值传递给函数参数时,会发生所有权转移(除非值实现了 Copy trait),原变量失去所有权,无法再访问该值,函数参数成为该值的新所有者。
  • 返回值转移:函数返回值会将所有权转移给调用者,函数内部的变量(作为返回值)在函数结束时不会被销毁,而是将所有权转移给调用者,调用者可以正常访问该值。
  • 示例:通过函数调用展示所有权转移的过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let s = String::from("hello"); // s是String类型,未实现Copy trait
takes_ownership(s); // s的所有权转移到函数参数some_string,s后续无法使用
// println!("s: {}", s); // 错误,s已失去所有权

let x = 5; // x是i32类型,实现了Copy trait
makes_copy(x); // 复制x的值,所有权不转移,x仍可使用
println!("x = {}", x); // 合法,输出5
}

// 接收String类型参数,获取所有权
fn takes_ownership(some_string: String) {
println!("{}", some_string); // 可访问some_string
} // some_string 超出作用域,String值被自动丢弃,内存释放

// 接收i32类型参数,复制值,不获取所有权
fn makes_copy(some_integer: i32) {
println!("{}", some_integer); // 可访问复制后的值
} // some_integer 超出作用域,复制的值被自动丢弃,不影响原变量x

引用与借用(避免所有权转移)

在实际开发中,我们经常需要访问一个值,但又不想获取该值的所有权(避免原变量被废弃),此时就可以使用“引用”,引用是一种不获取所有权的指针,指向其他变量的数据,这种通过引用访问数据的行为称为“借用”,引用是 Rust 中避免所有权转移、实现数据共享的核心方式。

引用的定义

引用用 & 符号表示,其核心特点是“不获取所有权”,仅拥有对数据的访问权,不会影响原变量的所有权。引用的生命周期与原变量一致,当原变量离开作用域,引用也会失效,无法再访问数据,避免了悬垂引用(指向已销毁数据的引用)的问题。

1
2
3
4
5
6
7
8
9
10
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传递s1的引用(&s1),不转移所有权
println!("The length of '{}' is {}.", s1, len); // s1仍可使用,输出hello和5
}

// 接收String的引用(&String),不获取所有权
fn calculate_length(s: &String) -> usize {
s.len() // 可通过引用访问数据,调用String的len()方法
} // s 超出作用域,引用失效,不影响原数据s1(无所有权)

可变引用

默认情况下,引用是不可变的,无法通过引用修改原数据,若需要通过引用修改原数据,需使用“可变引用”,可变引用用 &mut 表示,但其使用有严格的规则,用于避免数据竞争,保障并发安全。

  • 不可变引用限制:默认的引用(&T)是不可变的,即使原变量是可变的,也无法通过不可变引用修改原数据,只能访问数据。
  • 可变引用声明:使用 &mut 声明可变引用(&mut T),只有原变量是可变的(mut),才能创建其可变引用,通过可变引用可以修改原数据。
  • 可变引用的规则(避免数据竞争):数据竞争是指多个线程同时访问同一个数据,且至少有一个线程在修改数据,这会导致程序行为不确定,Rust 通过以下规则避免数据竞争:
  • 同一时刻,一个数据只能有一个可变引用;
  • 同一时刻,可变引用和不可变引用不能同时存在;
  • 不可变引用可以有多个(多个读者同时访问数据,不会产生竞争)。

示例:通过可变引用修改原数据,展示可变引用的使用规则:

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s = String::from("hello"); // 原变量必须是可变的
change(&mut s); // 传递可变引用(&mut s)
println!("{}", s); // 输出 "hello, world",原数据被修改
}

// 接收可变引用(&mut String),可修改原数据
fn change(some_string: &mut String) {
some_string.push_str(", world"); // 通过可变引用修改原String的值
}

String 与 String Slice(&str)

String 和 String Slice(&str)是 Rust 中处理字符串的两种核心类型,二者密切相关但又有本质区别:String 是拥有所有权的动态字符串,&str 是无所有权的字符串切片,掌握二者的区别和使用场景,是编写 Rust 字符串处理代码的关键。

String 类型

String 是 Rust 中用于表示动态字符串的类型,其核心特点是“拥有所有权、可修改、存储在堆上”,适用于需要动态创建、修改字符串的场景(如用户输入、字符串拼接等),其核心特性和使用方法如下:

  • 核心特性:动态可变长度,可修改字符串内容,存储在堆上,拥有所有权,会自动管理内存(离开作用域后自动释放)。
  • 创建方式:常用的三种创建方式,可根据需求选择:
  • String::new():创建一个空的 String,后续可通过 push_str、push 等方法添加内容;
  • String::from(“xxx”):从字符串字面值(&str)创建 String,将字面值的内容复制到堆上;
  • “xxx”.to_string():与 String::from 功能一致,是字符串字面值的方法,更简洁。

字符串拼接:使用 + 运算符