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。

相关推荐
JaguarJack41 分钟前
PHP 标准库的价值远不止少写几行代码
后端·php·服务端
鶴哥只手遮天1 小时前
《Redis 8.10.3 深度剖析》系列 · 第 1 章·全景与源码地图:版本谱系、构建体系、全局状态与启动链路
redis·后端·开源
用户400315043351 小时前
《从 skynet.timeout 到回调执行:Skynet 分层时间轮源码剖析》
后端
一条Go1 小时前
四个进程,一把锁:Alioth 的告警评测为什么不需要消息队列
后端·设计模式·架构
用户1996143559961 小时前
插件是怎么被安全装进系统的
后端
Pniubi1 小时前
Java为什么要分堆和栈
java·开发语言
愚农搬码2 小时前
自研表单引擎、开源组件二开和商业产品,企业应该如何选择?
后端
用户298698530142 小时前
Python 自动化处理 Word 页眉页脚的几种典型场景
后端·python·api
小小晓.2 小时前
Qt 对话框、QLineEdit 与布局管理实战详解
开发语言·qt
愚农搬码2 小时前
一款企业级电子表单系统应该具备哪些功能?
后端