所有权是Rust特有的概念,正是因为所有权,Rust才能够在没有垃圾回收机制的前提下保证内存安全。 这里会介绍所有权以及相关功能:借用、引用、切片以及Rust在内存中布局数据的方式。
什么是所有权
所有的程序都需要管理自己在运行时的计算机内存空间。Rust正是采用了:包含特定规则的所有权系统来管理内存。这套规则允许编译器在编译过程中执行检查工作,而不会产生任何运行时开销。
堆与栈
堆和栈都是程序运行时可以使用的内存空间。 栈中只能存储大小确定的数据,也就是说编译器无法确定大小的数据只能存放到堆上。 堆分配获得一块内存,将数据存入其中,使用一个指针指向这个堆内存。虽然数据的大小无法在编译期确认,但是指针的大小在编译期可以固定,所以可以存放在栈上。通过指针指向的地址来访问具体的数据。 但是访问堆上的数据需要通过指针跳转,又难以利用缓存,所以访问堆上数据要慢于栈上数据。
所有权规则
- Rust中的每一个值都有一个对应的变量作为它的所有者。
- 在同一时间,值有且仅有一个所有者。
- 当所有者离开自己的作用域的时候,它持有的值就会被释放。
变量作用域
作用域是一个对象在程序中有效的范围。而变量的有效范围是从变量声明开始到当前作用域结束。
内存与分配
为了了解Rust如何自动回收内存,我们需要一个存储在堆上的数据(栈由操作系统管理)作为例子,这里使用String类型。
String类型是一个可变的、可增长的文本类型。也就是说它使用的内存是在运行时动态分配的,使用完成之后需要归还内存。
在Rust中:内存会在拥有它的变量离开作用域后自动释放。Rust会在变量作用域结束的地方自动调用drop函数,这个函数里就是释放内存的代码。
对于堆上的数据的赋值,在C/C++中我们了解过深度拷贝和浅拷贝。Rust永远不会自动创建数据的深度拷贝。而是使用move,将一个堆上的变量赋值给另一个变量,那么第一个变量就变为无效,好像将值移动给了第二个变量。
let s1 = String::from("hello");
let s2 = s1;
// 发生错误,s1不可用了
println!("{}!", s1);如果确实需要深拷贝,可以通过clone方法实现:
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1={}, s2={}", s1, s2);但是对于栈上数据的复制,是不会发生move的,因为栈交由操作系统管理:
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);像这种可以完全存储在栈上的类型,实现了名为Copy的trait。如果实现了Copy trait,那么变量就可以在复制给其它变量后仍然保持可用性。
不过需要注意,实现了Drop的trait后,就不能实现Copy trait,因为Drop是在离开作用域时执行特殊指令释放内存,而Copy trait类型是保存在栈上的,交由操作系统管理的。
对于Rust提供的数据类型,如果可以编译时确定大小,那么一般都实现了Copy trait。
所有权与函数
将变量传递给函数将会触发移动或复制(类型是否实现Copy trait),与赋值语句一样。
返回值与作用域
函数在返回值的过程中也会发生所有权的转移。函数局部变量返回,其所有权也会移动至调用函数。
引用与借用
对于函数调用传入的参数,我们希望调用完成之后仍然能够继续使用,要想实现这点,我们需要将传入的参数再返回,这样的写法太过笨拙。
Rust提供了引用很好地解决了这一点。
先查看一个示例代码:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len();
}上面示例代码中调用calculate_length传入了参数&s1,而其函数参数为&String,其中&就表示引用语义。
使用&创建的引用对象并不获取所有权,所以再离开作用域的时候也不会释放内存。因此也就不必为了能够继续使用传入的参数而将参数再返回。
引用的过程如下:

这种通过引用传递参数给函数的方法也被称为借用(borrowing)。就好像函数借用了一下参数然后又还了回去。
可变引用
与变量类似,引用默认也是不可变的。如果需要通过引用去更改,就需要用mut关键字修饰引用。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
soem_string.push(",world");
}可变引用有一个很大的限制:对于特定作用域的特定数据来说,一次只能声明一个可变引用。 这个限制可以避免数据竞争:
- 两个及以上的指针同时访问同一空间。
- 其中至少一个指针向空间写入数据。
- 没有同步数据的机制。
可以同时拥有多个不可变引用,因为同时读取数据并不会发生未定义行为,但是不能同时拥有不可变引用和可变引用,因为使用不可变引用的用户不希望值会发生变化。
悬垂引用
对于拥有指针概念的语言非常容易出现悬垂指针,但是Rust语言,编译器会保证永远不会出现悬垂引用。
Rust编译器会发现这种错误的,如下代码编译就会报错,让用户立即发现错误:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
} // s在这里被销毁,返回引用指向无效的String。引用的规则
- 在任何一段给定的时间里(同一个作用域中),要么只能有一个可变借用,要么只能有任意多个不可变引用。
- 引用总是有效的。
切片
切片也是一种不持有所有权的数据类型。允许引用集合中某一段连续的元素序列,而不是整个集合。 因为切片是子集,所以切片的大小在编译期也是无法确认的,因此无法存储在栈上,因此一般都使用切片的引用。
字符串切片
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];这里创建了一个切片引用,使用方括号指定切片的范围空间[start_index..end_index],其中start_index表示起始位置的索引,如果省略表示从索引0开始;end_index表示结束位置的下一个索引,如果省略表示包括最后一个字节。
切片的数据结构内部存储了指向起始位置的引用和切片的长度。

Note
字符串切片的边界必须位于有效的UTF-8字符边界内。就说说对于一个多字节的字符,切片的起始位置不能处于这个字符的中间。
字符串字面量就是切片。例如let s = "Hello, world!",s的类型就是&str,它是一个指向二进制程序特定位置的切片,正是因为&str是一个不可变切片,所以字符串字面量才是不可变的。
一般传递字符串作为函数参数的时候,形参的类型都是&str,这样既能够处理&str,也能够处理String(为其创建一个完整的切片)。会让API更加通用。
其它类型切片
字符串切片是专门用于字符串的,是比较特别的切片。对于一些其它的切片,例如数组如下:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];slice的类型为&[i32],内部同样存储了指向起始元素的引用及长度。与字符串切片的工作机制完全一样。
tags: Rust重点知识