Rust 通过所有权系统管理内存,编译器在编译时会根据一系列规则进行检查,如果违反了这些规则,程序不能通过编译。在运行时,所有权系统的任何功能都不会减缓程序的运行
所有权规则
- Rust 中的每一个值都有一个所有者
- 值在任意时刻有且仅有一个所有者
- 当所有者(变量)离开作用域,这个值将被丢弃
变量作用域
同C语言,以 Rust 中的String
类型举例
1 | { |
当变量str
离开作用域时,会自动调用一个名为drop
的函数,类似C++中的RAII
变量与数据交互的方式:移动
对于以下代码
1 | let x = 5; |
其将5绑定到x
,接着生成一个x
的拷贝并绑定到y
,两个变量都在栈上
而对于以下代码
1 | let s1 = String::from("hello"); |
变量s1
实际内容是存放在堆中,String
对象由指向字符串内容的指针,一个长度与一个容量组成,这一部分数据是在栈上的,此处的操作类似于C++11中的std::move()
操作,将s1
中长度与容量赋值给s2
,将s2
的指针指向s1
所指向的字符串内容,在C++11中,std::move()
这一操作一般是对将亡值使用,在Rust中并无这一限制,但是在Rust中一旦使用这样的移动,则移动前的变量将不再有效,不能使用,这样就避免了变量离开作用域时的多次释放导致的未定义行为
简单来说,以上的操作是直接将s1
认定为将亡值并使用移动语义将其内容转义到s2
中
变量与数据的交互方式:克隆
如下
1 | let s1 = String::from("hello"); |
类似C++中的拷贝构造
只在栈上的数据:拷贝
如果数据本身大小已知,且整个存储在栈上,则在赋值时会进行拷贝操作,Rust 中存在一个 Copy
traitde的特殊注解,可以用在类似整型这样存储在栈上的类型上,一个类型如果实现了 Copy
trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。同时,Rust 不允许自身或其任何部分实现了 Drop
trait 的类型使用 Copy
trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy
注解,将会出现一个编译时错误。
一下是一些实现了 Copy
trait 的类型:
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都实现
Copy
的时候。比如,(i32, i32)
实现了Copy
,但(i32, String)
就没有。
所有权与函数
将值传递给函数与给变量赋值类似,可能会移动或赋值
1 | fn main() { |
返回值与作用域
返回值也可以转移所有权
1 | fn main() { |
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop
被清理掉,除非数据被移动为另一个变量所有。
引用
引用可以不获取所有权就可以使用值
1 | fn main() { |
&s1
语法可以创建一个指向值s1
的引用,但不拥有它,这种行为被称为借用,借用的变量不能进行修改
可变引用
1 | fn main() { |
可以看到,在增加了mut
关键字后,可以对引用值进行修改,但需要注意的是,如果拥有一个对该变量的可变引用,那就不能再对该变量创建引用,不能同一时间多次将一个值作为可变变量借用,这样的限制可以避免数据竞争,数据竞争主要由以下三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
当然,可以使用花括号来划分不同的作用域来实现同意值得多次借用,但在同一个作用域内,这样的行为不被允许。而可变引用与不可变引用也不能在同一个作用域中出现,但不可变引用可以在同一作用域出现多个,例如
1 | let mut str = String::from("hello"); |
不过,一个引用的作用域是从声明的地方到最后一次使用位置,所以可以进行如下改造
1 | let mut str = String::from("hello"); |
悬垂引用
我习惯称为野引用,指被借用的值的生命周期小于引用,例如
1 | fn main() { |
引用的规则
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
Slice类型
slice
允许引用集合中一段连续的元素序列,而不是引用整个集合,它是一类引用,所以没有所有权。
现在假设一个场景,在一个以空格为分隔符的字符串中,找到第一个子串并返回,如果没有空格则返回其本身,在不使用slice
的情况下,我们没有办法真正获取部分字符串,但可以返回索引,例如
1 | fn first_word(s: &String) -> usize { |
这会引发一个问题,索引和字符串本身没什么关系,但凡字符串有改动,那么拿到的索引就会失效
字符串slice
字符串slice
是String
中一部分值的引用,写法如下:
1 | let s = String::from("hello world"); |
不同于String
的引用,通过切片,将其分为了两个引用,分别是从第0个元素到第5个元素前,第6个元素到第11个元素前,形式为[start_idx..end_idx]
,如果start_idx
的值为0,那么也可以简写为[..end_idx]
,如果end_idx
的值为最后一个元素的位置,那么也可写作[start_idx..]
,若要获取整个元素的切片,可以写作[..]
现在,试着使用切片重写获取第一个子串的函数
1 | fn first_word(s: &String) -> &str { |
在进行编译是,会发现sclear()
会引发编译错误,这是因为clear
想要清空字符串,需要获取一个可变的引用,而其下一句println!
则使用了字符串中的不可变引用,rust中,可变引用和不可变引用不能同时存在,可变引用的最后一次使用应该位于不可变引用最后一次使用前
字符串面值也是slice
例如let s = "hello world";
此处s
的类型是&str
,其是指向二进制程序特定位置的slice
(静态区),所以&str
是一个不可变引用
字符串slice
做参数
1 | //对于以下函数定义 |
对第二种写法,如果有一个字符串slice
,可以传递,如果有一个String
,则可以传递整个String
的引用或对String
的引用,即这里会进行一次隐式类型转换,在使用时可如下
1 | let my_string = String::from("hello world"); |
其他类型slice
跟string
差裘不多