rust在内存资源管理上采用了(先进优秀?算吗)但特立独行的设计思路:所有权。这是rust的核心,贯穿在整个rust语言的方方面面,并以此为基点来重新思考和重构软件开发体系。
涉及到的概念点:借用,引用,生命周期
借用(borrowing)
这里就要谈到这篇文章的主题,也是rust绕不开的主题:所有权。
所有权模型:Rust 的所有权模型允许值在内存中有唯一的所有者
一开始我以为:引用就是其他语言(如go)里常见的引用, 而借用是更在引用之上的一种抽象,是对我们【使用引用去达到目的】这种行为的抽象 。因为我看到在一些教程里直接将两者等同混用,但其实是不对的,
因为:Rust语言中的引用和其他语言中的引用不一样,其他语言里的引用是变量的别名,Rust语言中的引用是指向某个值的指针,而不是别名。在其他语言中,引用就相当于拿到了值的别名,跟原来的值是同一个东西,可以进行任何无差别访问。在Rust语言中的借用都是临时借用使用权,并不破坏单一所有权原则。
“借用” 是指通过引用来访问值而不获取所有权; Rust 使用 “借用” 这个术语来强调在借用期间,被借用的值的仍然归原所有者所有,并且原所有者在借用期间仍然保持可变性和原始值的所有权。
就好比我从你那里借了一本书,但我肯定不能随意在上面乱写乱画,因为书还是归你所有。所以Rust的借用默认是不可变的,如果想要修改借用的值,需要显示的声明使用可变借用&mut x(取得书主人的授权)。
通过借用而不是其他语言中的直接引用,可以确保在借用期间只有一个可变或不可变的访问者,避免了悬空指针和内存安全问题。
Rust里的借用没有开辟单独的表现方式,形式上仍然是其他语言里引用的样子:在rust里,以 变量前加“&”符来表示不可变借用,例如&x ,用&mut 表示可变借用,例如:&mut x。
从能力范围上来看: 借用包含了传统意义上的引用,但是能力范围又大于引用。注意这个图只是从能力范围上来看,可变借用和传统引用好像一样,本质还是不同的,即可变借用没有获取原始变量的所有权,就好比我租了你的房子,谈好条件可以让我装修,但房子还是你的,但在我装修房子后,你再给别的租客(别的借用)来看这个房子的时候, 这个房子会是装修好后的样子。
借用的限制
为了保证内存安全,Rust语言中的借用也有一些限制,比如:
- 在同一作用域中,同一数据只能有一个可变借用,即多个可变借用不能同时存在(更准确是同一所有权型变量的多个可变借用作用域不能交叠)。
- 在同一个作用域中,同一数据可以有多个不可变借用。
- 在一个作用域中,可变借用与不可变借用不能同时存在 (更准确是可变借用与不可变借用的作用域不能交叠)
- 所有借用的生命周期不能超出值的生命周期(防止悬垂指针,保证内存安全)。
- 在有借用(包括可变借用和不可变借用)存在的情况下,不能通过原所有权型变量对值进行更新。当借用完成后(借用变量的作用域结束),物归原主,才又可以使用所有权型变量对值做更新操作了。
fn main(){
let mut a = 10u32;
let b = &a;
a = 50;
println!("{b}")
}
-----------------------
fn main(){
let mut a = 10u32;
let b = &mut a;
a = 50;
println!("{b}")
}
两个例子都不能通过编译
另外有个格外的要单独说明的点:可变引用的再赋值,会执行移动操作(而不是复制),赋值后,原来那个可变引用的变量就不能用了。可以看出:一个☝️所有权型变量的可变引用也具有所有权特征。
多级可变引用
对于多级可变引用,要利用可变引用去修改目标资源值的时候,需要做正确的多级解引用操作,比如两级引用就要对应两级接引用。而且这个引用过程中必须保证全是可变引用,才可以修改到目标资源的值。
对于多级引用的,打印语句可以为我们自动接引用到正确的层数,直至访问到目标资源值。这很符合人的意图和业务需求。
在这里有个有意思的对比:
go:
func main() {
var a = 10
var b = &a
fmt.Println(a)//output:10
fmt.Println(b)//output:0x... (memory address )
}
rust:
fn main() {
let a = 10u32;
let b = &a;
let c = &&&&&&&a;
println!(a)//output:10
println!(b)//output:10
println!(c)//output:10
}
可以看到rust像gpt一样识别到了我们的人类意图,没有打印的引用的内存地址,而是打印了被引用对象的值。事实上,哪怕是像let c = &&&&&&&a这种对a的多级引用,rust也仍然正确获取了a的值。
触发所有权转移的行为
会触发所有权转移的行为有:赋值操作,函数入参,函数返回值,集合操作中的移动(如Vec、HashMap等),迭代器中的移动:
先来看一个列子
fn main() {
let s = String::from("rust");
let s1 = s; // s的所有权转移给了s1, s不再有效
// println!("{}", s); // 此处会报错,因为s已经不再有效
println!("{}", s1);
// s1释放
}
上面的例子中,当把s赋值给s1时,s的所有权转移到了s1,s不再有效,也就是Move语义,以确保字符串rust在同一时刻只能有一个所有者。当s1离开作用域时,s1的所有权释放,字符串rust也随之释放。
再来看下边一个:
fn main(){
let a = 10u32;
let b = a;
println!("{a}");// 此处不会报错,此时a和b都是有效的
// a 释放
println!("{b}");
// b 释放
}
为什么字符串的复制会报错 而整数类型不会报错呢?
这是因为Rust对简单数据类型做了处理。假如整型、浮点型、布尔型等,这都需要转移所有权,那这程序编写起来也太复杂了吧。所以Rust语言对于这些简单数据类型,采用了Copy trait来实现,这样就是复制,而不是转移所有权。在上边的例子中因为a是简单数据类型,采用了Copy trait,所以a和b都是有效的。
实现了Copy trait的类型,在赋值或者传参时,值会自动按位拷贝;而对于没有实现Copy trait的类型,会采用Move转移所有权的方式来传递数据。
实现了Copy trait的类型:
- 原生类型:整型(i8,u8,i16,u16,i32,u32,i64,u64,i128,u128,isize,usize)、浮点型(f32,f64)、布尔型、字符型(char)、单元类型()、Never Type(!)。
- 不可变引用(&T)
- 函数指针
- 裸指针(*const T, *mut T)
- 数组[T;N]、元组(T1, T2, …, Tn)、Option类型(需要注意的是:只有当它们的元素类型都实现了Copy trait时,它们才实现了Copy trait)。
对于复合类型,比如枚举体和结构体,Rust语言默认是不实现Copy trait的,但是如果这些类型的所有成员都实现了Copy trait,那么你可以手动添加#[derive(Copy, Clone)]来实现Copy trait。如果内部结构包含Move语义的类型,那么就无法实现Copy trait。