Rust核心:【所有权】相关知识点

rust在内存资源管理上采用了(先进优秀?算吗)但特立独行的设计思路:所有权。这是rust的核心,贯穿在整个rust语言的方方面面,并以此为基点来重新思考和重构软件开发体系。

涉及到的概念点:借用,引用,生命周期

借用(borrowing)

这里就要谈到这篇文章的主题,也是rust绕不开的主题:所有权。

所有权模型:Rust 的所有权模型允许值在内存中有唯一的所有者

一开始我以为:引用就是其他语言(如go)里常见的引用, 而借用是更在引用之上的一种抽象,是对我们【使用引用去达到目的】这种行为的抽象 。因为我看到在一些教程里直接将两者等同混用,但其实是不对的,

因为:Rust语言中的引用和其他语言中的引用不一样,其他语言里的引用是变量的别名,Rust语言中的引用是指向某个值的指针,而不是别名。在其他语言中,引用就相当于拿到了值的别名,跟原来的值是同一个东西,可以进行任何无差别访问。在Rust语言中的借用都是临时借用使用权,并不破坏单一所有权原则。

"借用" 是指通过引用来访问值而不获取所有权 ; Rust 使用 "借用" 这个术语来强调在借用期间,被借用的值的仍然归原所有者所有,并且原所有者在借用期间仍然保持可变性和原始值的所有权。

就好比我从你那里借了一本书,但我肯定不能随意在上面乱写乱画,因为书还是归你所有。所以Rust的借用默认是不可变的,如果想要修改借用的值,需要显示的声明使用可变借用&mut x(取得书主人的授权)。

通过借用而不是其他语言中的直接引用,可以确保在借用期间只有一个可变或不可变的访问者,避免了悬空指针和内存安全问题。

Rust里的借用没有开辟单独的表现方式,形式上仍然是其他语言里引用的样子:在rust里,以 变量前加"&"符来表示不可变借用,例如&x ,用&mut 表示可变借用,例如:&mut x。

从能力范围上来看: 借用包含了传统意义上的引用,但是能力范围又大于引用。注意这个图只是从能力范围上来看,可变借用和传统引用好像一样,本质还是不同的,即可变借用没有获取原始变量的所有权,就好比我租了你的房子,谈好条件可以让我装修,但房子还是你的,但在我装修房子后,你再给别的租客(别的借用)来看这个房子的时候, 这个房子会是装修好后的样子。

借用的限制

为了保证内存安全,Rust语言中的借用也有一些限制,比如:

  • 在同一作用域中,同一数据只能有一个可变借用,即多个可变借用不能同时存在(更准确是同一所有权型变量的多个可变借用作用域不能交叠)。
  • 在同一个作用域中,同一数据可以有多个不可变借用。
  • 在一个作用域中,可变借用与不可变借用不能同时存在 (更准确是可变借用与不可变借用的作用域不能交叠)
  • 所有借用的生命周期不能超出值的生命周期(防止悬垂指针,保证内存安全)。
  • 在有借用(包括可变借用和不可变借用)存在的情况下,不能通过原所有权型变量对值进行更新。当借用完成后(借用变量的作用域结束),物归原主,才又可以使用所有权型变量对值做更新操作了。
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 复制代码
go:
func main() {
	var a = 10
	var b = &a
	fmt.Println(a)//output:10
	fmt.Println(b)//output:0x... (memory address )
}
rust 复制代码
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等),迭代器中的移动:

先来看一个列子

rust 复制代码
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也随之释放。

再来看下边一个:

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。

相关推荐
得物技术1 分钟前
基于IM场景下的Wasm初探:提升Web应用性能|得物技术
rust·web·wasm
聪明的墨菲特i3 分钟前
Django前后端分离基本流程
后端·python·django·web3
wainyz4 分钟前
Java NIO操作
java·开发语言·nio
喵叔哟12 分钟前
重构代码之用委托替代继承
开发语言·重构
lzb_kkk18 分钟前
【JavaEE】JUC的常见类
java·开发语言·java-ee
SEEONTIME18 分钟前
python-24-一篇文章彻底掌握Python HTTP库Requests
开发语言·python·http·http库requests
起名字真南37 分钟前
【OJ题解】C++实现字符串大数相乘:无BigInteger库的字符串乘积解决方案
开发语言·c++·leetcode
tyler_download1 小时前
golang 实现比特币内核:实现基于椭圆曲线的数字签名和验证
开发语言·数据库·golang
小小小~1 小时前
qt5将程序打包并使用
开发语言·qt
hlsd#1 小时前
go mod 依赖管理
开发语言·后端·golang