所有权
所有权是 Rust 语言最为显著的特点,它让程序不用进行垃圾回收即可保证内存安全。
所有权是什么?
有的编程语言拥有属于自己的垃圾回收机制,有的编程语言则需要开发者自己去实现垃圾回收,而 Rust 则是使用了第三种方式——拥有属于自己的所有权系统来管理内存。
编译器在编译的时候,会根据一系列规则来进行检查,如果违反了某些规则,程序则不能通过编译。在运行时,所有权的一系列功能都不会影响程序,从而保证了原有程序的性能。也就是说,所有权不影响程序的性能。
理解了所有权机制,就不用纠结栈和堆的问题,所有权机制的主要目的是为了管理堆上的数据。
所有权机制
- Rust 中每个值都有自己的所有者(Owner),且每个值在任意时刻有且仅有一个所有者
- 当所有者或变量离开作用域时,这个值就会被抛弃
变量作用域
在 Rust 变量的作用域范围如下:
内存与分配
字符串字面值都是硬编码进二进制文件的,但是总有一些未知大小的字符串字面量,这些如何处理?
在 Rust 中,为了处理这些未知大小的字符串字面量,有一个 String
类型,它支持一个可变、可增长的文本值,并且将值分配给堆来存放内容。
这意味着:
- 必须在运行时向内存分配器申请内存空间
- 需要一个当
String
处理完时,将内存返还给内存分配器的方法
这看起来很像是生命周期的概念…
依据所有权规则,name
变量在作用域结束时就被抛弃了,换句话说就是被自动释放了。
在变量离开作用域,即作用域结束时,Rust 提供了一个 drop
函数,在结尾的 }
处自动调用 drop
函数。
多个变量使用在堆上
移动
将 6 绑定到变量 x 上,并且生成一个 x 的拷贝绑定到变量 y 上
String
类型由三部分组成:一个指向存放字符串内容内存的指针(堆上存放内容的内存部分)、一个长度(表示当前使用了多少字节的内存)、一个容量(从内存分配器总共获取了多少字节的内存)。
将 s1
赋值给 s2
,这意味着复制了 s1
的指针、长度、容量,但没有复制堆上的数据。
根据所有权的原则,当变量离开作用域后,Rust 会自动调用 drop
函数并清理变量的堆内存。此时 s1
和 s2
指向了同一个同一内存,那么当 s1
和 s2
离开作用域后,它们会尝试释放相同的内存。这是一个二次释放(double free)的错误。两次释放相同的内存会导致内存被污染,并导致可能存在潜在的安全漏洞。
为了保证内存的安全,Rust 在 let s2 = s1
后,认为变量 s1
不再有效,因此 Rust 不需要在 s1
离开作用域后清理任何东西。
Rust 禁止你去使用无效的引用。
拷贝了指针、长度、容量但没有拷贝堆上的数据,且 Rust 会同时使第一个变量无效,这被称为”移动”(move)
s1
被移动到了 s2
中,具体发生了什么,下图可以解释:
s1
变为无效的了,因为只有 s2
是有效的,当其离开作用域,它就释放自己的内存了。
Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制都可以被认为是对运行时性能影响较小的。
克隆
如果确实需要深度复制堆上的数据,而不仅仅是栈上的数据,可以使用一个通用的函数 clone
拷贝
拷贝只适用于拷贝栈上的数据。
上面的代码看起来和之前说的有冲突呀,为什么没有调用 clone
函数,x
依然有效且没有被移动到了 y
呢?
原因是:像整型这样的在编译时已知大小的类型被整个存储在栈上。
Rust 有一个 Copy trait
的特殊注解,可以用在类似整型这样的存储在栈上的类型上。如果一个类型实现了 Copy trait
,那么一个旧的变量在赋值给其他变量后仍然可用。
Rust 不允许自身或其任何部分实现了 Drop trait
的类型使用 Copy trait
,以下是一些常见的 Copy
的类型:
- 所有整数类型
- 布尔类型
- 所有浮点数类型
- 字符类型
- 元组(当且仅当其包含的类型也都实现
Copye
的时候),如 (i32, i32)
实现了 Copy
,但 (i32, String)
就没有
所有权与函数
返回值与作用域
返回值可以转移所有权
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop
被清理掉,除非数据被移动为另一个变量所有
可以使用元组返回多个值:
以上代码可以通过引用来解决不用获取所有权就能够使用值的情况。