认识所有权
正是所有权引入,Rust才能够在没有垃圾回收机制的前提下保障内存安全
所有权规则
- 每一个值都有一个对应的变量作为其“所有者”
- 在同一时间内,值有且仅有一个所有者
- 当所有者离开自己的作用域时,其持有的值就会被释放掉
内存与分配:一个基于String类型的例子
字符串类型String,在堆上分配空间。
使用from
根据字符串字面量创建一个String实例
let s = String::from("hello");
String是一个可变的文本类型,意味着我们需要在运行时确定其大小,意味着
- 使用的内存是操作系统在运行时动态分配
- 当该变量完成其使命后,应该有某种方式将其内存归还给操作系统
不同的编程语言有不同的策略
- 通常第一步大家都是一样的,提供特定的函数以在堆上为变量分配空间
- 第二步,在Rust之前有两种作风
- 垃圾回收机制(Java,Go)
- 程序员手动释放(C,C++)
- Rust给出的解决方式是,在变量离开作用域时,自动为其调用
drop
函数,释放其持有的内存
{
let s = String::from("hello"); //变量s开始有效
//执行与s相关的操作
}//s的作用域结束,s生效,调用drop,释放s的内存
变量和数据的交互方式:移动
栈上的数据和堆上的数据是有区别的
对于栈上的数据,比如整型
let x = 5;
let y = x;
其最终效果是,两个值5都被推入栈,变量x,y各绑定一个
而对于堆上的数据
let s1 = String::from("hello");
let s2 = s1;
首先我们看一个String类型变量的内存布局:一个指向保存字符串内容的指针,一个长度,一个容量——这些储存在栈上;一个字符串——这些储存在堆上
当进行赋值时,复制了一遍String的数据(注意不是连带字符串一起复制),按理说应该是下面这样
但是,回顾我们的三大原则,“同一时间内,值有且仅有一个持有者”。上面展示的内存布局有什么问题呢,答案是当s1和s2同时离开作用域时,一块内存就被释放了两遍,这是不可饶恕的错误。所以实际上执行的结果是这样的
我们说这是s1“移动”到了s2。移动后,s1的内容(指针,长度,容量)现在在s2里,且变量s1不再有效(意味着这个操作之后,试图使用s1这个名称就会报错)
变量和数据的交互方式:克隆
像下面这样,同时复制String栈上和堆上的数据(即所谓的深拷贝)。使用clone()
方法
let s1 = String::from("hello");
let s2 = s1.clone();
其结果是
是否在赋值时发生移动的本质区别:trait
之后我们详细讨论trait,在这里只是提一嘴
- 拥有Copy这种trait的类型,其变量赋值给其它变量后,仍保可用性
- 所有整数类型
- 仅拥有两种值的布尔类型
- 字符类型
- 浮点类型
- 仅仅包含Copy类型的元组
- 如果类型本身或者其成员实现了Drop这种trait,赋值时就要发生移动
所有权与函数
传参时
将值传递给函数,语义上类似赋值。同理,此时也会产生移动
fn main() {
let s = String::from("hello"); //s进入作用域
takes_ownership(s);//发生移动,s所有权转换交给函数的参数
//s不再有效
}
fn takes_ownership(some_string: String) {//some_string进入作用域
println!("{}",some_string);
}//some_string离开作用域,调用drop,释放内存
返回值时
同理的,返回值的过程中也会发生移动
fn main() {
let s = gives_ownership(); //函数的返回值移动到s中
} //s离开作用域,调用drop,释放内存
fn gives_ownership() -> String {
let some_string = String::from("hello"); //som_string进入作用域
some_string //some_string作为返回值移动
}
至此为止,假如希望在调用函数时保留参数的所有权,就不得不让函数返回一个元组,将传入的值作为元组中的一员返回。我们将在下一节讨论另一种做法。
引用与借用
改变上面的例子
fn main() {
let s1 = String::from("hello");
takes_ownership(&s1);//注意传入引用
//s1仍然有效
}//s1离开作用域,调用drop,释放内存
fn takes_ownership(s: &String) { //注意形参声明
println!("{}",s);
}
有两点改变:在函数的参数声明中,我们使用引用类型&String
;在传入参数时,我们传入变量的引用&s1
引用语义下,允许在不拥有值的所有权的情况下使用值。这意味着s
根本就没有值的所有权,所以其在离开作用域时不会调用drop
释放内存,s1
也在函数调用后也仍然保持有效。
这就称为“借用”
可变引用
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(ss: &mut String) {
ss.push_str(" world!");
}
- 注意三个位置都出现了
mut
,声明变量s
,传入可变引用参数,声明函数的参数列表 - 同一数据,同一作用域下,只允许创建至多一个可变引用。(即不许
let s1 = &mut s;let s2 = &mut s
)这是为了在编译时期就避免数据竞争:- 多个指针同时访问一个空间
- 至少一个指针向空间写入数据
- 没有同步数据访问的机制
- 不能在拥有不可变引用的时候创建可变引用。这是为了保证不可变引用使用的安全性:使用不可变引用的用户期望值是不可变的,但可变引用可能会改变值
悬垂引用
在Rust中,编译阶段就可以检查悬垂引用的问题,所以通过编译的代码永远不可能出现存在指向已经释放的内存的引用,比如下面的代码
fn dangle() -> &String {
let s = String:from("s"); //s进入作用域
&s //返回s的引用
}//s离开作用域,调用drop,释放内存
就产生了一个悬垂引用,不过好在在编译期间就能检查出来
小结:引用的规则
- 在任何一个给定的时间里,要么只有一个可变引用,要么只有任意数量的不变引用
- 引用总是有效的
切片
切片允许我们引用集合某一段连续的元素序列,而不是整个集合
切片也是一种引用,遵守引用的规则和借用的规则
字符串切片
类型标识&str
,可以如下所示创建:&原String[区间]
let s = String::from("hello world");
let hello = &s[0..5]
let world = &s[6..11]
区间是经典的左闭右开,省略第一个下标表示从头开始,省略第二个下标表示直到结尾
-
字符串字面量的类型就是切片
-
可以将字符串切片作为函数参数类型和函数返回值类型。比如下面这个查找字符串中第一个单词的函数
fn first_word(s:&str) -> &str { let bytes = s.as_bytes(); for (i,item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] }
其他类型切片
以数组为例,切片类型可以是&[i32]
,创建和使用的语法和字符串切片一样
let a = [1,2,3,4,5];
let slice = &s[1..3];