Rust学习笔记3-Rust核心概念之所有权

概述

这篇文件介绍了 Rust 中最核心,也是最特别的一个概念:所有权,想学好 Rust 就必须充分的理解所有权。所有权让 Rust 在无需垃圾回收机制就可以保证内存安全,而且在其他编程中从未有过所有权的概念,因此从其他编程语言转来学 Rust 时会感觉难以理解。

什么是所有权

所有权是 Rust 的核心特性,Rust 中使用所有权系统来管理内存使用,它让 Rust 无需 GC (垃圾回收)就可以保证内存安全。所有权机制不像 java、C# 使用垃圾回收器管理内存,也不像 C/C++ 语言一样需要程序员显式的申请与释放内存。

所有权系统让 Rust 在编译时就可以完成内存使用的检查,因而程序在运行时就不会产生任何额外开销,这样既保证了内存安全,又提升了运行速度。真是妙啊!

Rust 所有权三大规则

在 Rust 的世界中,每个数据都有且只有一个所有者,如果所有者不在了,数据所占用的内存空间也会被释放。

  • Rust 中每个值都有一个变量,这个变量就是这个值的所有者。
  • 同一时刻一个值只能有一个所有者。
  • 当所有者离开作用域时,该值将被删除,内存将被释放。

关于栈(stack)存储和堆(heap)存储

计算机为每个运行的程序分配内存资源,程序在运行时管理和使用内存资源。在计算机内存中只存储程序运行状态时的所有变量、常量。

对于常量和静态变量,它们的生命周期从程序运行开始到程序终止,将被存储在静态内存区,这类数据在程序开始运行时被分配,运行结束时被释放。

对于函数中的变量,其生命周期很灵活,在函数调用时开始到函数返回时结束,这类数据将以栈的数据结构来管理,存储此类数据的内存区域被称为栈区,栈区内存空间有限,由操作系统自动分配和释放。

程序中还有一些大小不确定的数据,无法在编译时确定大小,只能在运行时由程序向系统申请存储空间,分配好后返回一个指针,就是这个空间的地址,在这些数据不在被使用时由程序释放。这些数据以堆的数据结构存储,存储此类数据的内存空间被称为堆区,栈区内存空间很大,需要程序申请分配和释放。

stack
stack
heap
heap
index
index
value
value
0
0
h
h
1
1
e
e
2
2
l
l
3
3
l
l
4
4
o
o
let str = String::from(“hello”);
let str = String::from(…
name
name
value
value
ptr
ptr
len
len
5
5
capacity
capacity
5
5
Rust stack&heap
Rust stack&heap

在程序中,指针是已知固定大小的,可以把指针存放在栈上,实际数据存在堆上,如果需要实际数据,必须使用指针来定位。

把数据压入栈比在堆上分配快得多:因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在栈的顶端。

在堆上分配空间需要做更多的工作:操作系统首选要找到一个足够大的空间来存储数据,然后要做好记录方便下次分配。

访问堆中的数据比访问栈中数据慢,因为需要通过指针才能找到堆中的数据,多了一步指针跳转的动作。对于现代处理器来说,由于缓存的缘故,如果指令在内存中跳转次数越少,那么速度就越快。

Rust 之前的程序内存管理方式:

1、手动管理:以 C/C++ 语言为代表,需要手动申请分配内存,如使用 malloc() 函数分配内存,使用结束后需要手动释放内存,如使用 free() 函数释放内存。

2、垃圾回收:以 JAVA 为代表,分配内存使用 new 关键字,JAVA 虚拟机提供了垃圾回收机制,自动回收不再使用的资源。

Rust 的程序内存管理方式:所有权机制

所有权与堆栈

**管理堆数据是所有权存在的原因。**所有权解决的问题:

  • 跟踪代码哪些部分正在使用堆的哪些数据
  • 最小化堆上重复数据
  • 清理堆上未使用的数据以避免空间不足

所有权的应用

在 Rust 中,将一个变量赋值给另一个变量或将变量值传递给一个函数时,会发生所有权移动(Move)或值的复制(Copy)。

移动(Move)

当一个包含堆数据的变量值被赋值给另一个变量或作为函数参数传递时,其所有权会被移动到另一个变量。如 String 类型的变量被赋值给另一个变量后,其所有权将被移动给新变量,原变量将不可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let a = String::from("1");
let b = a; // 所有权转移给变量b,变量a就失效了
// println!("a={}", a); // borrow of moved value: `a`

let str = String::from("hello");
print_string(str); // 所有权转移给函数
// println!("str={}", str); // borrow of moved value: `str`
}

fn print_string(s: String) {
println!("{}", s);
}

stack
stack
heap
heap
index
index
value
value
0
0
h
h
1
1
e
e
2
2
l
l
3
3
l
l
4
4
o
o
let str = String::from(“hello”);
let str = String::from(…
name
name
value
value
ptr
ptr
len
len
5
5
capacity
capacity
5
5
let st2 = str;

let st2 = str;
name
name
value
value
ptr
ptr
len
len
5
5
capacity
capacity
5
5
Rust ownership move
Rust ownership move

复制(Copy)

如果一个变量实现了 Copy trait ,这个变量在赋值后仍然可用。

如果一个类型或该类型的一部分实现了 Drop 的 trait ,Rust 不允许它再实现 Copy trait 。

一些实现 Copy trait 的类型:

  • 任何简单标量的组合类型都是可以 Copy 的,如整形数组
  • 任何需要分配内存或某种资源的都不是 Copy 的,如 String
  • 所有的整数类型都是可以 Copy 的
  • bool 类型
  • char 类型
  • 所有浮点型
  • 如果元组的所有字段都是可 Copy 的,那么此元组是可 Copy 的
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
fn main() {
let a = 1_u64;
let b = a; // 整形数据值会被拷贝
println!("a={}, b={}", a, b);

let c = 3.14_f32;
let d = c; // 浮点型数据会被拷贝
println!("c={}, d={}", c, d);

let e = false;
let f = e; // 布尔型数据会拷贝
println!("e={}, f={}", e, f);

let g = 'A';
let h = g; // char 类型数据会拷贝
println!("g={}, h={}", g, h);

let i: (i32,f32) = (2, 1.414);
let j = i; // 如果元组的所有字段都是可 Copy 的,那么此元组是可 Copy 的
println!("i=({},{}), j=({},{})", i.0, i.1, j.0, j.1);

let str = String::from("hello");
print_string(&str); // 字符串切片不转移所有权
println!("str={}", str); // 正常打印
}

fn print_string(s: &str) {
println!("{}", s);
}

克隆(Clone)

使用 clone 方法可以创建数据的深拷贝,即栈和堆上的数据都会被拷贝一份。

1
2
3
4
5
6
7
8
9
10
fn main() {
let str: String = String::from("hello");
let str2 = str.clone();
print_string(str2); // 将字符串克隆后传递给函数
println!("{}", str); // 原来的变量依然可用
}

fn print_string(s: String) {
println!("{}", s);
}

stack
stack
heap
heap
index
index
value
value
0
0
h
h
1
1
e
e
2
2
l
l
3
3
l
l
4
4
o
o
let str = String::from(“hello”);
let str = String::from(…
name
name
value
value
ptr
ptr
len
len
5
5
capacity
capacity
5
5
let st2 = str.clone();

let st2 = str.clone();
name
name
value
value
ptr
ptr
len
len
5
5
capacity
capacity
5
5
Rust ownership clone
Rust ownership clone
index
index
value
value
0
0
h
h
1
1
e
e
2
2
l
l
3
3
l
l
4
4
o
o

所有权与函数返回值

函数在返回值的过程中,也会发生所有权转移。

当一个包含 堆 数据的变量离开作用域时,它的值就会被 drop 函数清理,除非数据的所有权移动到另一个变量上了。

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

let str2 = String::from("hi");
let str3 = give_back_String(str2); // 将 str2 的所有权移动给了函数,然后函数又返回了它的所有权,最后将值的所有权交给了变量 str3

let mut str4: String;
{
let str5 = String::from("rust");
str4 = str5;
} // str5 的所有权移动给了 str4,所以不会触发 drop 释放内存
}

fn get_string() -> String {
let s = String::from("hello"); // 声明一个 String 类型变量
s // 将变量返回,同时移交所有权
}

fn give_back_String(s: String) -> String {
s // 接收了参数值的所有权,然后返回了该值的所有权
}

引用(Reference)与借用(Borrowing)

Rust 中在变量类型前加 & 表示该类型的引用类型,如 &String 表示字符串类型(String)的引用类型。

引用类型允许你引用某些值,而不获取其所有权,我们把引用作为函数参数这一行为叫做借用

Rust 中使用 & 表示引用,使用 * 解引用,这与 C 语言中的指针概念很相似。

很多情况下,我们调用函数但又不希望主函数中失去变量所有权,有两种解决办法:

  • 传递变量所有权给函数,让函数再将变量的所有权返回回来
  • 传递变量的引用(Reference)给函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let str1 = String::from("hi rust");
let (s, len) = get_len(str1); // 传递所有权给函数,函数将变量所有权返回
println!("{}, {}", s, len);

let str2 = String::from("hello");
let len2 = get_len_by_ref(&str2); // 传递变量引用给函数,不传递所有权
println!("{}, {}", str2, len2);
}

fn get_len(s: String) -> (String, usize) {
let len = s.len();
(s, len) // 用元组返回多个值
}

fn get_len_by_ref(s: &String) -> usize { // 参数类型是 &String 类型,表示字符串类型的引用类型
s.len()
}

可变引用

使用 &mut 为可变变量创建 可变引用,使用可变引用能够修改变量值。

可变引用有一个重要的限制:在特定作用域中,对某一块数据,只能有一个可变的引用。这样做的好处是在编译时防止数据竞争。

可以通过创建新的作用域,来创建多个非同时可变引用。

可变引用另一个限制:不能同时拥有一个可变引用和一个不可变引用。多个不可变引用是允许的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let mut str1 = String::from("hello");
say_hi(&mut str1); // 传递可变引用
println!("{}", str1);

let mut str2 = String::from("hi");
{
let s1 = &mut str2; // 创建新的作用域,获得了一个可变引用
}
let s2 = &mut str2; // 可变引用,这一行代码放到前面作用域前也可以
let s3 = &str2; // 编译出错:cannot borrow `str2` as immutable because it is also borrowed as mutable
println!("{}", s2); // 使用可变引用,如果这句注释掉,编译不报错
println!("{}", s3); // 使用不可变引用
}

fn say_hi(s: &mut String) -> usize { // 参数为可变引用
s.push_str(", rust"); // 通过可变引用修改变量值
s.len()
}

切片(Slice)

Rust 中还提供一种不占用所有权的数据类型–切片,切片是对数组或字符串一部分内容的引用,只引用原始数组或字符串的一部分内容,不占用所有权。

字符串切片是对字符串中一部分内容的引用,字符串切片的数据类型为 &str,创建形式为 [开始索引..结束索引] ,其中不包含结束索引位置字符。

创建切片时,仅会在栈上创建对原始数据的引用,消耗资源少。

语法糖:
获取切片时,如果开始位置是从索引 0 开始的,则 0 可以省略,如 let hello = &str[..5]
如果结束位置是数组或字符串的末尾,则结束索引可省略,如 let world = &str[6..]
如果想获取整个数组或字符串的切片,则开始索引与结束索引都可省略,如 let whole = &str[..]

1
2
3
4
5
6
7
8
9
10
fn main() {
let str = String::from("hello world");
let hello = &str[0..5]; // 创建字符串切片,从索引 0 开始,到索引 5 结束,不包含索引 5
let world = &str[6..11];
println!("{},{}", hello, world);

// 其他类型的切片
let a: [i32; 4] = [1, 2, 3, 4]; // 创建数组,数组的数据类型为 `[i32; 4]`
let slice: &[i32] = &a[1..3]; // 创建切片,切片的数据类型为 ` &[i32]`
}

stack
stack
heap
heap
let str = 
String::from(“hello world”);

let str =…
name
name
value
value
ptr
ptr
len
len
11
11
capacity
capacity
11
11
let world = &str[6…11];

let world = &str[6…11];
Rust ownership slice
Rust ownership clice
index
index
value
value
0
0
h
h
1
1
e
e
2
2
l
l
3
3
l
l
4
4
o
o
5
5
6
6
w
w
7
7
o
o
8
8
r
r
9
9
l
l
10
10
d
d
name
name
value
value
ptr
ptr
len
len
5
5

注意事项:

创建字符串切片必须以 UTF-8 字符为边界,如果从一个多字节的字符中间创建切片,程序会报错退出。

字符串字面值与字符串切片
字符串字面值是直接存储在二进制程序中的,字符串字面值就是指向这些二进制程序特定位置的字符串切片,因此字符串字面值的数据类型是 &str

有经验的开发者会将 &str 作为函数的参数类型,这样就可以接受 String 类型和 &str 类型。定义函数时使用字符串切片作为函数参数,会使我们的 API 更加通用,且不会损失任何功能。

参考资料

  • 樊少冰, 孟祥莲. 《Rust编程从入门到实战》. 2022