Rust 学习笔记

基本输入输出

输入并输出

1
2
3
4
5
6
7
use std::io;

fn main() {
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
println!("input is: {}", input);
}
  • stdin() 函数获取标准输入的句柄
  • read_line(buf: &mut String) -> Result<usize> 方法读取一行输入,并将其存储在传入的 String 变量中。成功时,返回读取的字节总数
  • println! 宏用于打印输出,类似于 C 语言中的 printf 函数。它会自动添加换行符
  • unwrap() -> T 方法解包 Result 类型,返回 Ok 中的值或引发 panic
  • 在使用 use 语句时一般引入到所用函数或模块的上一级,可以避免命名冲突

读取多行输入

1
2
3
4
5
6
7
8
use std::io;

fn main() {
let lines = io::stdin().lines();
for line in lines {
println!("got a line: {}", line.unwrap());
}
}
  • lines() 方法返回一个迭代器,迭代器产生的元素末尾不含换行符

格式化输出

  • format! 宏用于格式化字符串,返回一个 String
  • print! 宏格式化文本并打印到标准输出
  • println! 宏格式化文本并打印到标准输出,末尾自动添加换行符
  • eprint! 宏格式化文本并打印到标准错误输出
  • eprintln! 宏格式化文本并打印到标准错误输出,末尾自动添加换行符

以上宏的格式化语法全部相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// {} 用作占位符
println!("Hello, {}!", "world");

// 位置参数
println!("{1} {0}", "world", "hello");
// 多个 {} 的行为类似迭代器,依次填充参数
println!("{0} {} {1} {}", "hello", "world"); // => hello hello world world

// 命名参数
println!("{greeting} {name}", greeting="hello", name="world");
// 可以引用变量
let greeting = "hello";
let name = "world";
println!("{greeting} {name}");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 宽度 {:5},表示宽度为5
println!("Hello, {:5}!", "rust"); // "Hello, rust !"
// 宽度值可以使用变量,需要$后缀
println!("Hello, {1:0$}!", 5, "rust");
println!("Hello, {:width$}!", "rust", width=5);

// 对齐
println!("Hello, {:<6}!", "rust"); // "Hello, rust !" 左对齐
println!("Hello, {:^6}!", "rust"); // "Hello, rust !" 居中对齐
println!("Hello, {:>6}!", "rust"); // "Hello, rust!" 右对齐

// 数字格式
println!("{:-^12}", "数字");
println!("Hello, {:+}!", 42); // "Hello, +42!" 始终显示符号
println!("Hello, {:#x}!", 42); // "Hello, 0x2a!" 16进制,如果不带#,则不显示前缀
println!("Hello, {:#X}!", 42); // "Hello, 0x2A!" 16进制大写
println!("Hello, {:#o}!", 42); // "Hello, 0o52!" 8进制
println!("Hello, {:#b}!", 42); // "Hello, 0b101010!" 2进制
println!("Hello, {:#e}!", 42.0); // "Hello, 4.2e1!" 科学计数法
println!("Hello, {:#E}!", 42.0); // "Hello, 4.2E1!" 科学计数法大写
println!("Hello, {:05}!", 42); // "Hello, 00042!" 设置宽度并填充0
println!("Hello, {:.2}!", 42.12345); // "Hello, 42.12!" 设置精度
println!("Hello, {:.1$}!", 42.12345, 2); // "Hello, 42.12!" 使用参数设置精度,命名参数也可以

// 花括号转义,{{ 表示 { , }} 表示 }
println!("{{ Hello }}");

#[derive(Debug)] // 自动实现Debug trait
struct Point {
x: i32,
y: i32,
}
let p = Point { x: 1, y: 2 };
println!("{:?}", p); // debug输出
println!("{:#?}", p); // debug美观输出

Display trait

要在 println! 中以 {} 的形式输出自定义类型,需要实现 Display trait。在 fmt 方法中用 write! 宏格式化输出。

1
2
3
4
5
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}

基本类型

  • 有符号整数:i8i16i32i64i128isize(指针宽度)
  • 无符号整数: u8u16u32u64u128usize(指针宽度)
  • 浮点数: f32f64
  • char:单个 Unicode 字符,如 'a','α' 和 ' 锈'
  • bool:truefalse
  • 单元类型:(),空元组
  • 数组:[T; N],长度为 N 的数组,元素类型为 T
  • 元组:(T1, T2, ...),可以包含不同类型的元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 显式类型注解
let x: i32 = 5;
let pi: f64 = 3.14;
let c: char = 'a';

let w:i32 = pi as i32; // 类型转换

// 编译器会根据上下文推断类型
let y = 5; // 整数默认为i32
let z = 5u32; // 字面量加类型后缀指定类型
let f = 0.1; // 浮点数默认为f64
let b = true;

// 数组
let arr: [i32; 5] = [1, 2, 3, 4, 5]; // 数组,长度为5
let zero_arr = [0; 5]; // 初始化为相同值
println!("First element: {}", arr[0]); // 访问数组元素
println!("Array length: {}", arr.len()); // 数组长度
println!("Slice: {:?}", &arr[1..3]); // 切片

// 元组
let tup: (i32, f64, char) = (1, 2.0, 'a'); // 元组
println!("First element: {}", tup.0); // 访问元组元素

变量绑定

  • let 用于将值绑定到变量。
  • 绑定默认不可变
  • 使用 mut 关键字使变量可变
1
2
3
4
5
6
7
8
let x = 5; // 不可变绑定
let mut y = 10; // 可变绑定
y = 20; // 修改可变变量的值
println!("x: {}, y: {}", x, y); // 输出 x: 5, y: 20

let z; // 声明变量z
z = 30; // 变量z在使用前必须初始化
println!("z: {}", z); // 输出 z: 30
  • 变量绑定的作用域限定在一个代码块中
  • 变量可以在同一作用域中被重新绑定,称为遮蔽(shadowing)
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let mut x = 5;
{
let x = "hello"; // 遮蔽外部变量x,且x为不可变
println!("Inner x: {}", x); // 输出 Inner x: hello

let y = 20;
} // y的作用域结束,在外部不可使用y

x = 10; // 修改外部变量x的值
println!("Outer x: {}", x); // 输出 Outer x: 10
}

自定义类型

结构体

结构体有三种类型: * 元组结构体:没有字段名,只有字段类型 * 命名结构体:有字段名和字段类型 * 单元结构体:没有字段,只有名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Unit; // 单元结构体

struct Point(i32, i32); // 元组结构体

#[derive(Debug)]
struct Person {
name: String,
age: u8,
} // 命名结构体

let p = Point(1, 2); // 创建元组结构体实例
// let p: Point = (1, 2); // 错误,元组结构体和对应结构的元组不是同一类型
let (x, y) = (3, 4);
let p = Point(x, y); // 用变量创建元组结构体实例
let p = Point { 0: 5, 1: 6 }; // 使用字段索引创建元组结构体实例
println!("Point: ({}, {})", p.0, p.1); // 访问元组结构体字段


let alice = Person { name: String::from("Alice"), age: 22 }; // 创建命名结构体实例
let name = String::from("Bob");
let age = 25;
let bob = Person { name, age }; // 变量名和字段名相同,可以省略字段名
let charlie = Person { name: String::from("Charlie"), ..person }; // 结构体更新语法,省略其他字段

枚举

枚举用于定义一个类型,它可以是多个不同的值之一。Rust 中的枚举有点类似于 C 语言中的联合体,但更强大。

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Direction {
Up,
Down,
Left,
Right,
} // 类似其他语言中的枚举

// 枚举可以包含数据,类似于联合体
enum Action {
Move { x: i32, y: i32 },
Speak(String),
Stop,
}

关联函数和方法

  • 结构体和枚举都可以使用 impl 块来实现关联函数或方法
  • 方法是依赖于实例的函数,必须有一个 self 参数。self 是结构体或枚举的实例
  • 关联函数和方法类似于面向对象语言中的静态方法和实例方法
1
2
3
4
5
6
7
8
9
10
11
12
13
impl Person {
fn new(name: String, age: u8) -> Person { // 关联函数
Person { name, age }
}

fn greet(&self) { // 方法,&self表示不可变引用
println!("Hello, my name is {} and I'm {} years old.", self.name, self.age);
}

fn rename(&mut self, name: String) { // 可变方法,&mut self表示可变引用
self.name = name;
}
}

类型别名

类型别名用于给现有类型起一个新的名字。它不会创建新的类型,只是给现有类型起一个别名。类似于 C 语言中的 typedef

1
2
3
4
5
type Int = i32; // 给i32起一个别名Int
type Point = (i32, i32); // 给元组类型起一个别名Point

let x: Int = 5; // 使用类型别名
let p: Point = (1, 2);

控制流

判断条件不加括号

if 语句

1
2
3
4
5
6
7
8
9
if x > 5 {
println!("x is greater than 5");
} else if x < 5 {
println!("x is less than 5");
} else {
println!("x is equal to 5");
}

let positive = if x > 0 { true } else { false }; // 类似于三元运算符的用法

loop 循环

loop 是无限循环

1
2
3
4
5
6
7
8
9
let cnt = 0;

loop {
println!("cnt: {}", cnt);
if cnt == 10 {
break; // 退出循环
}
cnt += 1;
}

可以从 loop 循环中返回值

1
2
3
4
5
6
7
8
let mut x = 1;
let result = loop {
x = x * 2
if x > 1000 {
break x; // 返回值
}
};
println!("Result: {}", result); // 输出 Result: 1024

while 循环

1
2
3
4
5
let mut x = 1;
while x < 5 {
println!("x: {}", x);
x += 1;
}

for 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
for i in 1..5 { // 1..5表示从1到4,如果要包含5,使用1..=5
println!("i: {}", i);
}

for i in (1..5).rev() { // 反向迭代
println!("i: {}", i);
}

let arr = [1, 2, 3, 4, 5];
for i in arr { // 可以遍历数组
println!("i: {}", i);
}

let mut names = vec!["Alice", "Bob", "Charlie"];
for name in names.iter() { // iter()迭代中的元素是不可变引用
println!("Name: {}", name);
}

for name in names.iter_mut() { // iter_mut()迭代中的元素是可变引用
*name = "Rust"; // 修改数组元素
}

// 等同于 for name in names
for name in names.into_iter() { // into_iter()迭代中的元素会被消耗
println!("Name: {}", name);
}

// 不能再使用names,因为它已经被消耗

循环标签

Rust 允许在循环前加标签,以便在嵌套循环中使用 breakcontinue 跳出指定的循环

1
2
3
4
5
6
7
8
'outer: for i in 0..5 {
for j in 0..3 {
if i == 2 && j == 2 {
break 'outer; // 跳出外层循环
}
println!("i: {}, j: {}", i, j);
}
}

模式匹配

可以在 matchif letwhile letforlet 和函数参数中使用模式匹配。

match 语句

match 语句用于模式匹配,可以匹配枚举、整数、字符、字符串等类型。类似于 switch 语句,但更强大

1
2
3
4
5
6
7
8
// 匹配字面值
let x = 5;
match x {
1 => println!("One"), // 匹配单个值
2 | 3 => println!("Two or Three"), // 匹配多个值
4..=10 => println!("Four to Ten"), // 匹配范围
_ => println!("Other"), // 匹配其他值
}
1
2
3
4
5
6
7
// 匹配变量
let x = Some(5);
match x {
Some(1) => println!("One"),
Some(n) => println!("Some {}", n), // 匹配变量
_ => println!("None"),
}

if letwhile let

if let 在表达式满足模式时执行代码块。它是 match 的简化版本,适用于只关心一个模式的情况。

1
2
3
4
5
6
let x = Some(5);
if let Some(n) = x { // if let 模式 = 表达式
println!("Some {}", n); // 匹配成功
} else { // else分支是可选的
println!("None"); // 匹配失败
}

while let 在表达式满足模式时执行循环。

1
2
3
4
5
6
7
8
9
10
let mut x = Some(1);
while let Some(n) = x { // while let 模式 = 表达式
if n > 9 {
println!("x is {}, enough", n); // 匹配成功
x = None;
} else {
println!("x is {}, not enough", n); // 匹配成功
x = Some(n + 1); // 更新x的值
}
}

守卫语句和 @绑定

守卫语句可以在模式匹配时添加额外的 if 条件。

1
2
3
4
5
6
7
8
let x = 5;
let y = 10;
match x {
1 => println!("One"),
n if n < y => println!("{} is less than {}", n, y), // 守卫语句
n if n > y => println!("{} is greater than {}", n, y),
_ => println!("Other"),
}

@绑定可以在测试一个值是否匹配模式的同时将该值绑定到变量上

语法为 variable @ pattern

1
2
3
4
5
6
let x = 5;

match x {
n @ 1..=10 => println!("value in range 1 to 10: {}", n), // n 绑定为 5
_ => println!("other range"), // 匹配其他值
}

可以绑定整个模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Point {
x: i32,
y: i32,
}

let point = Point { x: 10, y: 20 };

match point {
p @ Point { x: 0..=10, y } => {
println!("点在 x 轴上 0 到 10 的范围内: {:?}, y = {}", p, y)
},
p @ Point { x, y: 0..=10 } => {
println!("点在 y 轴上 0 到 10 的范围内: {:?}, x = {}", p, x)
},
Point { x, y } => println!("点在其他位置: ({}, {})", x, y),
}

解构

解构元组

1
2
3
4
5
6
7
8
9
10
let point = (1, 2);
let (x, y) = point; // 解构元组
println!("x: {}, y: {}", x, y); // 输出 x: 1, y: 2

match point {
(0, 0) => println!("Origin"),
(x, 0) => println!("On the x-axis at {}", x),
(0, y) => println!("On the y-axis at {}", y),
(x, y) => println!("Point at ({}, {})", x, y),
}

解构结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Point {
x: i32,
y: i32,
}

let point = Point { x: 1, y: 2 };
let Point { x: i, y: j } = point; // 解构结构体
let Point { x, y } = point; // 当变量与字段名相同时的简化写法

match point {
Point { x: 0, y: 0 } => println!("Origin"),
Point { x, y: 0 } => println!("On the x-axis at {}", x),
Point { x: 0, y } => println!("On the y-axis at {}", y),
Point { x, y } => println!("Point at ({}, {})", x, y),
}

解构枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Action {
Move { x: i32, y: i32 },
Speak(String),
Stop,
}

let action = Action::Move { x: 1, y: 2 };

match action {
Action::Move { x, y } => println!("Move to ({}, {})", x, y),
Action::Speak(message) => println!("Speak: {}", message),
Action::Stop => println!("Stop"),
}

嵌套解构

解构的模式类似于初始化时的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Point {
x: i32,
y: i32,
}

struct Circle {
center: Point,
radius: i32,
}

let circle = Circle {
center: Point { x: 1, y: 2 },
radius: 5,
};

let Circle { center: Point { x, y }, radius } = circle; // 嵌套解构
println!("Circle center: ({}, {}), radius: {}", x, y, radius); // 输出 Circle center: (1, 2), radius: 5

忽略模式

在模式匹配中,可以忽略某些值

使用 _ 忽略整个值

下划线 _ 是一个通配符,可以匹配任何值但不绑定到变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let x = 5;

// 忽略函数返回值
let _ = calculate_value();

// 在match中忽略某些情况
match x {
1 => println!("One"),
2 => println!("Two"),
_ => println!("Other"), // 匹配所有其他值但不绑定
}
}

fn calculate_value() -> i32 {
// 一些计算...
42
}

使用下划线忽略部分值

可以在模式中使用 _ 忽略元组、结构体或者模式的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 忽略元组中的某些元素
let tuple = (1, 2, 3, 4);
let (first, _, third, _) = tuple;
println!("first: {}, third: {}", first, third);

// 忽略结构体中的某些字段
struct Point {
x: i32,
y: i32,
z: i32,
}

let point = Point { x: 1, y: 2, z: 3 };
let Point { x, y: _, z } = point;
println!("x: {}, z: {}", x, z);

// 在函数参数中忽略
fn foo(_: i32, y: i32) {
println!("y: {}", y);
}
foo(3, 4); // 忽略第一个参数

使用 .. 忽略剩余值

双点号 .. 可以用来忽略模式中剩余的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 忽略元组中间的值
let tuple = (1, 2, 3, 4, 5);
let (first, .., last) = tuple;
println!("first: {}, last: {}", first, last);

// 忽略结构体中的大部分字段
struct Point3D {
x: i32,
y: i32,
z: i32,
label: String,
visible: bool,
}

let point = Point3D {
x: 10,
y: 20,
z: 30,
label: String::from("point1"),
visible: true,
};

// 只关心x字段
let Point3D { x, ..} = point;
println!("x: {}", x);

// 在match中结合..使用
match point {
Point3D { x: 0, .. } => println!("On the YZ plane"),
Point3D { x: 10, y, .. } => println!("x is 10, y is {}", y),
Point3D { x, y, z, .. } => println!("Located at ({}, {}, {})", x, y, z),
}

.. 必须是无歧义的。例如,(first, .., second, ..) 会导致编译错误,因为无法确定哪些值应该匹配第一个 .. 和哪些值应该匹配第二个 ..

使用前缀下划线 _var 忽略未使用变量

如果你需要绑定一个变量但不打算使用它,可以给变量名加上下划线前缀来避免未使用变量的警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 没有下划线前缀,编译器会警告 unused_variables
fn process_data(data: &str) {
let (name, age) = get_user_info(data);
println!("Name: {}", name);
// age 未使用,编译器会警告
}

// 使用下划线前缀避免警告
fn process_data_better(data: &str) {
let (name, _age) = get_user_info(data);
println!("Name: {}", name);
// _age 未使用,但编译器不会警告
}

fn get_user_info(data: &str) -> (String, u32) {
// 解析数据...
(String::from("Alice"), 30)
}

下划线前缀 _age 与单个下划线 _ 的区别在于,_age 仍然会绑定值,而 _ 根本不会绑定。

函数和闭包

使用 fn 关键字定义函数

1
2
3
4
5
6
7
8
9
10
11
fn function_name(param1: Type1, param2: Type2) -> ReturnType {
// 函数体
if condition {
return value; // 用return显式或提前返回值
}
expression // 函数最后的表达式自动作为返回值
}

fn do_nothing() { // 没有返回值的函数,返回类型为 ()

}

函数可以作为参数,其类型是 fn

1
2
3
4
5
6
7
8
9
fn function(){
println!("call function");
}

fn call(f:fn()) {
f()
}

call(function);

闭包 (Closure)

闭包类似其他语言中的 lambda 表达式。它们可以捕获周围的环境。闭包的语法如下:

1
2
3
4
5
6
let closure_name = |param1: Type1, param2: Type2| -> ReturnType {
// 闭包体,对于单个表达式可以省略大括号
};

let add = |x: i32, y: i32| x + y; // 闭包,返回两个数的和
println!("{}", add(1, 2)); // 调用闭包,返回3

闭包可以捕获上下文中的变量,闭包有三种捕获方式:

  • 通过引用: &T
  • 通过可变引用: &mut T
  • 通过值: T
1
2
3
4
5
6
7
8
9
10
11
let name = String::from("Alice");

// 通过引用捕获 name
let greet = || {
println!("Hello, {}!", name);
};

greet(); // 打印: Hello, Alice!

// name 仍然可用,因为闭包只借用了它
println!("Name is still available: {}", name);
1
2
3
4
5
6
7
8
9
10
11
12
13
let mut counter = 0;

// 通过可变引用捕获 counter
let mut increment = || {
counter += 1;
println!("Counter: {}", counter);
};

increment(); // 打印: Counter: 1
increment(); // 打印: Counter: 2

// counter 仍然可用且已被修改
println!("Final counter: {}", counter); // 打印: Final counter: 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let name = String::from("Bob");

// 通过值捕获 name
let greet = || {
println!("Hello, {}!", name);
take_ownership(name); // 将所有权转移到函数
}

greet(); // 打印: Hello, Bob!

// 错误!name 已被闭包消耗
// println!("Can't use name anymore: {}", name);

fn take_ownership(s: String) {
println!("Goodbye {}!", s);
}
1
2
3
4
5
6
7
8
9
10
11
let name = String::from("Charlie");

// 可以使用 move 关键字强制通过值捕获
let greeting = move || {
println!("Goodbye, {}!", name);
};

// 错误!name 的所有权已转移到闭包
// println!("Can't use name anymore: {}", name);

greeting(); // 打印: Goodbye, Bob!

上面三种捕获方式直接影响闭包的 trait 实现:

  • Fn: 通过引用捕获,闭包可以多次调用,但在最后一次调用前不能可变借用被捕获的变量
  • FnMut: 通过可变引用捕获,闭包可以多次调用,但在最后一次调用前不能再次借用被捕获的变量
  • FnOnce: 通过值捕获,闭包只能调用一次,因为它消耗了捕获的变量

三种 trait 的关系:FnOnce > FnMut > Fn

只要闭包中有变量是通过值捕获的,就实现 FnOnce;如果没有值捕获,但有可变引用捕获,则实现了 FnMut;如果只有引用捕获,则实现了 Fn

闭包可以作为函数的参数或返回值。在返回闭包时必须使用 move 关键字来捕获变量的所有权,否则在函数退出后产生无效引用。

1
2
3
4
5
6
7
8
fn apply<F>(f: F) where F: FnOnce() {
f(); // 调用闭包
}

fn create_closure() -> impl FnOnce() {
let name = String::from("Dave");
move || println!("Hello, {}!", name) // 返回闭包
}

高阶函数

高阶函数是指接受一个或多个函数作为参数,并 / 或返回另一个函数的函数。在迭代器中经常使用高阶函数。

1
2
3
4
5
6
7
8
let numbers = vec![1, 2, 3, 4, 5];

let sum_of_squares_of_odds: i32 = numbers.iter()
.filter(|&&x| x % 2 != 0)
.map(|&x| x * x)
.sum();

println!("{}", sum_of_squares_of_odds); // 输出 35

发散函数

发散函数不会返回,使用 ! 标记返回类型。

1
2
3
fn no_return() -> ! {
panic!("This function never returns!");
}

! 可以被转换为任何类型,这在有些时候很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn sum_odd_numbers(up_to: u32) -> u32 {
let mut acc = 0;
for i in 0..up_to {
// 注意这个 match 表达式的返回值必须为 u32,
// 因为 addition 变量是这个类型。
let addition: u32 = match i%2 == 1 {
// “i” 变量的类型为 u32,这毫无问题。
true => i,
// continue 表达式不返回任何值,所以是 ! 类型
// 不会有类型的问题
false => continue,
};
acc += addition;
}
acc
}
println!("Sum of odd numbers up to 9 (excluding): {}", sum_odd_numbers(9));

模块

错误处理

泛型

所有权和生命周期

所有权规则

Rust 的所有权系统遵循三个基本规则:

  1. 每个值都有一个所有者(变量)
  2. 值在任意时刻只能有一个所有者
  3. 当所有者离开作用域时,值会被丢弃
1
2
3
4
5
6
{
let s1 = String::from("hello"); // s1是字符串的所有者
let s2 = s1; // s2获取s1的所有权,s1不再有效
// println!("{}", s1); // 错误!s1已经失效
println!("{}", s2);
} // s2超出作用域,内存被释放,s2不再有效

栈与堆

理解所有权之前,需要先了解栈与堆的区别:

  • :先进后出,存储已知固定大小的数据。数据必须具有已知固定的大小
  • :内存分配器找到足够大的空间,标记为已使用,返回指针。数据大小可以在运行时改变

所有权转移(Move)

当涉及堆数据时,赋值操作会导致所有权转移:

1
2
3
4
let s1 = String::from("hello"); // s1 -----> 堆上的"hello"
let s2 = s1; // s1 X s2 -----> 堆上的"hello"

// s1不再有效,这避免了"二次释放"错误

实现了 Copy trait 的类型不会发生所有权转移,而是进行复制:

  • 所有整数类型,如 u32
  • 布尔类型,bool
  • 浮点数类型,如 f64
  • 字符类型,char
  • 仅包含以上类型的元组,如 (i32, i32)
1
2
3
let x = 5;
let y = x; // x的值被复制给y
println!("x = {}, y = {}", x, y); // 都有效

克隆(Clone)

如果需要深度复制堆数据而不是转移所有权,可以使用 clone 方法:

1
2
3
let s1 = String::from("hello");
let s2 = s1.clone(); // 堆数据的深拷贝
println!("s1 = {}, s2 = {}", s1, s2); // 都有效

函数与所有权

函数参数和返回值也遵循所有权规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
let s1 = String::from("hello");
take_ownership(s1); // s1的所有权被转移
// println!("{}", s1); // 错误!s1已无效

let s2 = gives_ownership(); // 接收函数返回值的所有权
println!("{}", s2);

let s3 = String::from("world");
let s4 = takes_and_gives_back(s3); // s3所有权转移到函数,函数又返回新的所有权
// println!("{}", s3); // 错误!s3已无效
println!("{}", s4);
}

fn take_ownership(some_string: String) {
println!("{}", some_string);
} // some_string超出作用域被丢弃

fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // 返回并转移所有权
}

fn takes_and_gives_back(a_string: String) -> String {
a_string // 返回并转移所有权
}

引用和借用

为了避免每次传递值都转移所有权,Rust 提供了 "引用" 的概念,允许使用值但不获取其所有权。

引用基础

创建引用的行为称为 "借用":

1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传递 s1 的引用
println!("'{}' 的长度是 {}.", s1, len); // s1 仍然有效
}

fn calculate_length(s: &String) -> usize { // s 是指向 String 的引用
s.len()
} // s 离开作用域,但它不拥有所指向的值,所以什么也不会发生

引用的示意图:

1
2
3
4
s1 -----> String("hello")
^
|
s

可变引用

引用默认是不可变的。如果想修改借用的值,需要使用可变引用:

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello");
change(&mut s); // 传递可变引用
println!("{}", s); // 输出 "hello, world"
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

引用规则

引用有两条核心规则:

  1. 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用
  2. 引用必须始终有效(不能指向已释放的内存)

这些规则在编译时防止数据竞争(data race):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let mut s = String::from("hello");

// 例1:不能同时拥有可变和不可变引用
let r1 = &s; // 不可变引用
let r2 = &s; // 不可变引用
// let r3 = &mut s; // 错误!不能同时有可变和不可变引用
println!("{} and {}", r1, r2);

// r1和r2从此处不再使用

let r3 = &mut s; // 现在可以使用可变引用
println!("{}", r3);

// 例2:不能有多个可变引用
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // 错误!不能有多个可变引用

悬垂引用

Rust 编译器确保引用永远不会成为悬垂引用(指向已释放的内存):

1
2
3
4
5
6
7
8
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String { // 错误!返回了悬垂引用
let s = String::from("hello");
&s // 返回了s的引用,但s在函数结束时会被释放
} // s超出作用域被释放

解决方法是直接返回 String

1
2
3
4
fn no_dangle() -> String {
let s = String::from("hello");
s // 返回s并转移所有权
}

解引用与自动解引用

使用 * 操作符可以访问引用指向的值:

1
2
3
4
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y); // 使用*解引用

Rust 在许多情况下会自动解引用,特别是:

  1. 方法调用:object_ref.method()
  2. 字段访问:object_ref.field
  3. 比较操作:ref1 == ref2
  4. 与模式匹配结合使用:match *ref_val { ... }
1
2
3
4
5
6
7
8
9
10
struct Point {
x: i32,
y: i32,
}

let point = Point { x: 1, y: 2 };
let point_ref = &point;

// 自动解引用,不需要使用(*point_ref).x
println!("坐标: ({}, {})", point_ref.x, point_ref.y);

切片(Slice)

切片是对集合中部分连续元素的引用,不拥有所有权:

1
2
3
4
let s = String::from("hello world");
let hello = &s[0..5]; // 等价于&s[..5]
let world = &s[6..11]; // 等价于&s[6..]
let whole = &s[..]; // 等价于&s[0..s.len()]

字符串字面量是字符串切片:

1
let s: &str = "Hello, world!"; // s是&str类型,指向二进制程序中的位置

函数参数应优先使用切片类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 更灵活的接口,可接受String和&str类型
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}

// 使用示例
let my_string = String::from("hello world");
let word = first_word(&my_string[..]); // 以切片形式传递String

let my_literal = "hello world";
let word = first_word(my_literal); // 直接传递字符串字面量

数组切片:

1
2
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 类型是&[i32]

生命周期

生命周期是 Rust 另一个独特的概念,用于确保引用在使用时始终有效。

生命周期的概念

生命周期是引用有效的范围。大多数情况下,生命周期是隐含的,但有时需要显式标注:

1
2
3
4
5
6
7
8
9
// 错误示例:引用超出生命周期
fn main() {
let r;
{
let x = 5;
r = &x; // x将在内部作用域结束时被释放
} // x超出作用域
println!("r: {}", r); // 错误!r引用了已释放的内存
}

生命周期标注语法

生命周期标注以撇号(')开头,通常使用小写字母如 'a

1
2
3
&i32        // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

函数中的生命周期标注

当函数有多个引用参数并返回引用时,编译器可能无法推断生命周期之间的关系:

1
2
3
4
5
6
7
8
// 没有生命周期标注,无法编译
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

使用生命周期标注,明确参数和返回值的关系:

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

这表示 x 和 y 至少与返回的引用存活一样长。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);
println!("最长的字符串是: {}", result);

// 错误示例:返回值的生命周期受限于最短的参数生命周期
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
// result的生命周期受限于string2
} // string2超出作用域
// println!("最长的字符串是: {}", result); // 错误!result已无效
}

生命周期省略规则

Rust 有三条生命周期省略规则,让代码更简洁:

  1. 每个引用参数都有自己的生命周期参数
  2. 如果只有一个输入生命周期参数,它会被赋给所有输出生命周期参数
  3. 如果有多个输入生命周期参数,但其中一个是 &self&mut self,则 self 的生命周期被赋给所有输出生命周期参数

例如,以下两个函数签名是等价的:

1
2
fn first_word(s: &str) -> &str { /* ... */ }
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }

结构体中的生命周期

当结构体持有引用时,需要标注生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ImportantExcerpt<'a> {
part: &'a str,
}

fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("找不到句号");

let excerpt = ImportantExcerpt {
part: first_sentence,
};

println!("{}", excerpt.part);
} // novel和excerpt都在这里超出作用域

静态生命周期

'static 生命周期表示引用在整个程序运行期间都有效。所有字符串字面量都有 'static 生命周期:

1
let s: &'static str = "我有静态生命周期";

生命周期约束与泛型

生命周期可以与泛型和 trait 约束结合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("公告:{}", ann);
if x.len() > y.len() {
x
} else {
y
}
}

trait


Rust 学习笔记
http://blog.qzink.me/posts/Rust学习笔记/
作者
Qzink
发布于
2025年4月22日
许可协议