Rust学习笔记3-Rust核心概念之所有权
概述
这篇文件介绍了 Rust 中最核心,也是最特别的一个概念:所有权,想学好 Rust 就必须充分的理解所有权。所有权让 Rust 在无需垃圾回收机制就可以保证内存安全,而且在其他编程中从未有过所有权的概念,因此从其他编程语言转来学 Rust 时会感觉难以理解。
什么是所有权
所有权是 Rust 的核心特性,Rust 中使用所有权系统来管理内存使用,它让 Rust 无需 GC (垃圾回收)就可以保证内存安全。所有权机制不像 java、C# 使用垃圾回收器管理内存,也不像 C/C++ 语言一样需要程序员显式的申请与释放内存。
所有权系统让 Rust 在编译时就可以完成内存使用的检查,因而程序在运行时就不会产生任何额外开销,这样既保证了内存安全,又提升了运行速度。真是妙啊!
Rust 所有权三大规则
在 Rust 的世界中,每个数据都有且只有一个所有者,如果所有者不在了,数据所占用的内存空间也会被释放。
- Rust 中每个值都有一个变量,这个变量就是这个值的所有者。
- 同一时刻一个值只能有一个所有者。
- 当所有者离开作用域时,该值将被删除,内存将被释放。
关于栈(stack)存储和堆(heap)存储
计算机为每个运行的程序分配内存资源,程序在运行时管理和使用内存资源。在计算机内存中只存储程序运行状态时的所有变量、常量。
对于常量和静态变量,它们的生命周期从程序运行开始到程序终止,将被存储在静态内存区,这类数据在程序开始运行时被分配,运行结束时被释放。
对于函数中的变量,其生命周期很灵活,在函数调用时开始到函数返回时结束,这类数据将以栈的数据结构来管理,存储此类数据的内存区域被称为栈区,栈区内存空间有限,由操作系统自动分配和释放。
程序中还有一些大小不确定的数据,无法在编译时确定大小,只能在运行时由程序向系统申请存储空间,分配好后返回一个指针,就是这个空间的地址,在这些数据不在被使用时由程序释放。这些数据以堆的数据结构存储,存储此类数据的内存空间被称为堆区,栈区内存空间很大,需要程序申请分配和释放。
在程序中,指针是已知固定大小的,可以把指针存放在栈上,实际数据存在堆上,如果需要实际数据,必须使用指针来定位。
把数据压入栈比在堆上分配快得多:因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在栈的顶端。
在堆上分配空间需要做更多的工作:操作系统首选要找到一个足够大的空间来存储数据,然后要做好记录方便下次分配。
访问堆中的数据比访问栈中数据慢,因为需要通过指针才能找到堆中的数据,多了一步指针跳转的动作。对于现代处理器来说,由于缓存的缘故,如果指令在内存中跳转次数越少,那么速度就越快。
Rust 之前的程序内存管理方式:
1、手动管理:以 C/C++ 语言为代表,需要手动申请分配内存,如使用
malloc()
函数分配内存,使用结束后需要手动释放内存,如使用free()
函数释放内存。2、垃圾回收:以 JAVA 为代表,分配内存使用
new
关键字,JAVA 虚拟机提供了垃圾回收机制,自动回收不再使用的资源。Rust 的程序内存管理方式:所有权机制
所有权与堆栈
**管理堆数据是所有权存在的原因。**所有权解决的问题:
- 跟踪代码哪些部分正在使用堆的哪些数据
- 最小化堆上重复数据
- 清理堆上未使用的数据以避免空间不足
所有权的应用
在 Rust 中,将一个变量赋值给另一个变量或将变量值传递给一个函数时,会发生所有权移动(Move)或值的复制(Copy)。
移动(Move)
当一个包含堆数据的变量值被赋值给另一个变量或作为函数参数传递时,其所有权会被移动到另一个变量。如 String
类型的变量被赋值给另一个变量后,其所有权将被移动给新变量,原变量将不可用。
1 | fn main() { |
复制(Copy)
如果一个变量实现了 Copy
trait ,这个变量在赋值后仍然可用。
如果一个类型或该类型的一部分实现了 Drop
的 trait ,Rust 不允许它再实现 Copy
trait 。
一些实现 Copy
trait 的类型:
- 任何简单标量的组合类型都是可以 Copy 的,如整形数组
- 任何需要分配内存或某种资源的都不是 Copy 的,如 String
- 所有的整数类型都是可以 Copy 的
- bool 类型
- char 类型
- 所有浮点型
- 如果元组的所有字段都是可 Copy 的,那么此元组是可 Copy 的
1 | fn main() { |
克隆(Clone)
使用 clone
方法可以创建数据的深拷贝,即栈和堆上的数据都会被拷贝一份。
1 | fn main() { |
所有权与函数返回值
函数在返回值的过程中,也会发生所有权转移。
当一个包含 堆 数据的变量离开作用域时,它的值就会被 drop
函数清理,除非数据的所有权移动到另一个变量上了。
1 | fn main() { |
引用(Reference)与借用(Borrowing)
Rust 中在变量类型前加 &
表示该类型的引用类型,如 &String
表示字符串类型(String)的引用类型。
引用类型允许你引用某些值,而不获取其所有权,我们把引用作为函数参数这一行为叫做借用。
Rust 中使用 &
表示引用,使用 *
解引用,这与 C 语言中的指针概念很相似。
很多情况下,我们调用函数但又不希望主函数中失去变量所有权,有两种解决办法:
- 传递变量所有权给函数,让函数再将变量的所有权返回回来
- 传递变量的引用(Reference)给函数
1 | fn main() { |
可变引用
使用 &mut
为可变变量创建 可变引用,使用可变引用能够修改变量值。
可变引用有一个重要的限制:在特定作用域中,对某一块数据,只能有一个可变的引用。这样做的好处是在编译时防止数据竞争。
可以通过创建新的作用域,来创建多个非同时可变引用。
可变引用另一个限制:不能同时拥有一个可变引用和一个不可变引用。多个不可变引用是允许的。
1 | fn main() { |
切片(Slice)
Rust 中还提供一种不占用所有权的数据类型–切片,切片是对数组或字符串一部分内容的引用,只引用原始数组或字符串的一部分内容,不占用所有权。
字符串切片是对字符串中一部分内容的引用,字符串切片的数据类型为 &str
,创建形式为 [开始索引..结束索引]
,其中不包含结束索引位置字符。
创建切片时,仅会在栈上创建对原始数据的引用,消耗资源少。
语法糖:
获取切片时,如果开始位置是从索引 0 开始的,则 0 可以省略,如let hello = &str[..5]
。
如果结束位置是数组或字符串的末尾,则结束索引可省略,如let world = &str[6..]
。
如果想获取整个数组或字符串的切片,则开始索引与结束索引都可省略,如let whole = &str[..]
。
1 | fn main() { |
注意事项:
创建字符串切片必须以 UTF-8 字符为边界,如果从一个多字节的字符中间创建切片,程序会报错退出。
字符串字面值与字符串切片
字符串字面值是直接存储在二进制程序中的,字符串字面值就是指向这些二进制程序特定位置的字符串切片,因此字符串字面值的数据类型是&str
。有经验的开发者会将
&str
作为函数的参数类型,这样就可以接受String
类型和&str
类型。定义函数时使用字符串切片作为函数参数,会使我们的 API 更加通用,且不会损失任何功能。
参考资料
- 樊少冰, 孟祥莲. 《Rust编程从入门到实战》. 2022