开始
所有权(ownership)是 Rust 用于如何管理内存的一组规则。所有程序都必须管理其运行时使用计算机内存的方式,通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序
所有权规则
所有权系统遵循三条规则
- rust 中的每个值都有一个所有者
- 值在任何时刻仅有一个所有者
- 当所有者离开作用域时,值将被丢弃
rust 中的数据有两种内存分配方式,分别是分配栈内存和分配堆内存
- 栈分配:存储大小固定的数据,例如整数、布尔值、浮点数、字符、引用
- 堆分配:存储动态大小的数据,例如字符串对象、动态数组、哈希表,在运行时分配内存
字符串字面量是不可变的,存储在只读内存中,而字符串对象是可变的,存储在堆中
1
2 let s1 = "hello"; // 字面量
let s2 = String::from("hello1"); // 字符串对象
当拥有数据的所有者(变量)离开作用域时,数据占用的内存就被自动释放(rust 在底层调用了 drop
函数释放内存)
1 | fn main() { |
变量与数据的交互
-
移动
1
2let s1 = String::from("hello");
let s2 = s1; // 所有权移动通常情况下,为字符串对象分配堆内存,s1 指向该内存区域,将 s1 赋值给 s2,则 s2 也指向这块内存
此时,若 s1 和 s2 都离开作用域,会调用两次
drop
函数,产生二次释放的错误,因此,rust 规定在此处赋值时,进行所有权移动,即赋值后 s2 是这个数据的所有者,s1 无效,释放后不会清理任何资源 -
克隆
1
2let s1 = String::from("hello");
let s2 = s1.clone();调用
clone
函数进行克隆,即深拷贝,此时,s1 依然有效,是 hello 字符串对象内存的所有者,s2 则是复制后的内存的所有者 -
浅拷贝
1
2let x = 5;
let y = x;对于只在栈上的数据,赋值给其他变量时进行浅拷贝,此时 x 依然有效
Drop
trait 和Copy
trait
Copy
trait:可以用在类似整型这样的存储在栈上的类型上,对于实现了Copy
trait 的类型,它的变量在赋值给其他变量后依然可用,不会发生所有权移动Drop
trait:用于离开作用域时需要释放资源的类型,它的变量在赋值给其他变量时会发生所有权移动,旧变量会失效Copy
trait 和Drop
trait 是互斥的,不允许实现了Drop
trait 的类型使用Copy
traitrust 的所有变量都具有对数据的所有权,无论这个数据在栈上分配还是在堆上分配
所有权与函数
参数传递
将数据传入函数时,数据会进行移动或拷贝
- 对于堆中的数据,所有权会在传入参数时移动到函数的参数上,移动后,外部的变量不再拥有所有权
- 对于栈中的数据,传入参数时会进行浅拷贝,外部的变量依然拥有所有权
1 | fn main() { |
返回值传递
函数在返回时,会将返回值的所有权移动到外部变量中,或者将数据复制到外部变量
1 | fn main() { |
引用
当向函数传参时,所有权会移动到参数中,导致外部变量无效。为了在函数调用后继续使用原变量,此时需要使用引用
引用可以获取变量值而不会移动所有权
使用 &
运算符创建一个引用,创建一个引用的行为称为借用,引用从创建到最后一次使用的一段时间称为借用期
1 | fn main() { |
引用的逆操作是解引用,使用解引用运算符
*
可变引用
引用值默认是不可变的,若需要修改引用的值,需要使用可变引用
使用 &mut
创建一个可变引用
1 | fn main() { |
多个可变/不可变引用的使用遵循以下规则
-
对于一个变量值,在借用期内不能使用其他可变引用
1
2
3
4let mut s = String::from("hello");
let a = &mut s;
let b = &mut s; // 不合法
println!("{a}"); -
对于一个变量值,在借用期内可以使用多个不可变引用
1
2
3
4let s = String::from("hello");
let a = &s;
let b = &s; // 合法
println!("{a}{b}"); -
对于一个变量值,在借用期内不可同时使用它的可变引用和不可变引用
1
2
3
4
5
6
7
8let mut s = String::from("hello");
let a = &s;
let b = &mut s; // 不合法
println!("{a}");
let c = &mut s;
let d = &s; // 不合法
println!("{c}"); -
对于一个变量值,必须在借用期结束后才可以继续借用
1
2
3
4
5
6let mut s = String::from("hello");
let a = &mut s;
println!("{a}"); // 最后一次使用a引用,a的借用期结束
let b = &s; // 可以继续借用
println!("{b}");
悬垂引用
当指针指向的内存被释放,而指针依然存在时,指针会变为悬垂指针,对于引用,则称为悬垂引用(Dangling Reference)
在 rust 中,rust 编译器确保悬垂引用永远不会存在
1 | fn main() { |
Slice 类型
Slice 类型是一个引用,可以引用集合中的一段连续序列,不具有所有权
切片的基本形式是 &variable[start..end]
,范围为左闭右开,最常用的切片主要有字符串切片和数组切片
-
字符串切片
1
2let s = String::from("hello");
let s1: &str = &s[1..3]; // -
数组切片
1
2let arr = [1, 2, 3, 4];
let a1: &[i32] = &arr[1..3];
slice 引用是一个不可变引用,依然遵循借用期规则
1 | let mut s = String::from("hello"); |
切片引用
切片返回的类型是 &str
和 &[i32]
,此处必须使用引用。若使用 str
和 [i32]
类型,编译器会抛出以下错误
1 | the size for values of type `str` cannot be known at compilation time [E0277] |
表示 str
类型的长度在编译期不可知
这是因为 str
和 [i32]
是 动态大小类型(DST),它的大小在编译期不可知,因此 rust 无法在栈上为变量分配内存,而引用类型包含一个指向动态大小内存的指针和内存大小信息,引用类型本身的大小是可知的,rust 可以在栈上为它分配内存,因此此处应该使用引用类型 &str
和 &[i32]
这同时也反映了动态大小类型只能通过引用间接使用,切片与切片引用可参考:切片和切片引用
String 和 str
rust 中有两种字符串类型,分别是 String
和 str
-
String
rust 标准公共库提供的类型,也是最常用的字符串类型,具有所有权
String
在栈上保存一个固定大小的结构体,其中保存了指向堆内存的指针、字符串的长度以及字符串的容量,字符串内容在堆上分配内存 -
str
rust 底层的数据类型,是一个动态大小类型,无法直接使用
str
的引用类型&str
可以引用任意动态大小的字符串,包括字符串对象和字符串字面量(可以认为字符串字面量的类型就是&str
)1
2
3let s = String::from("hello");
let s1: &str = &s;
let s2: &str = "hello1";