让Rust在不需要gc的情况下保证内存安全
栈和堆的差异
- gc
- 手动分配和释放
Rust另辟蹊径——通过所有权管理内存
- Rust中的每一个值都有一个被称为其 所有者owner 的变量
- 值在任意时刻有且只有一个所有者
- 所有者(变量)离开作用域时,这个值将被丢弃
补充:String类型
之前的数据类型都是分配在栈上的,包括字符串字面值(它是被硬编码到程序中的)
字符串字面值的缺点
- 不可变(适用性受限)
- 编码时不一定能知道字符串的值,比如想要获取用户输入
String是分配到堆上的
- 可存储编译时位置大小的文本
- 为支持可变文本,Rust怎么做的?——C++的RAII
//移动 { let s1 = String::from(“hello”); let s2 = s1; //没有拷贝堆上的数据,而是拷贝了栈上的s1 }//我们傻乎乎地自作聪明,认为Rust调用drop,s1和s2尝试释放相同内存,double free //实际上s2复制时,s1不再有效 //!!这就是移动嘛,太强了!!
<a name="moNJK"></a>
### 克隆Clone
我们当然会想要一个String的深拷贝,此时clone函数就是我们想要的
- clone当然开销很大啦,于是Rust不想随随便便就去克隆一个String,需要我自己主动要求
但好像基本类型像i32他们,似乎并不需要指定调用clone,也做到了克隆
- 这种类型在编译时大小已知,可以存栈上,于是拷贝其实没什么大的消耗,并没有必要去要求在创建y的时候让x无效化
Copy trait
- 是一种特殊注解
- 如果一个类型有Copy trait,那么旧的变量赋值给其他变量后,旧变量仍然可以使用
- Rust禁止实现了Drop trait的类型使用Copy trait
- 判断可Copy的类型:简单标量和其组合可Copy,不需要分配内存或某种形式资源可Copy
- u32/bool/f64/char/各元素都是Copy的tuple
<a name="sSRN7"></a>
## 三、所有权与函数、返回值
向函数传参可能会移动和复制,就和给普通变量赋值类似
```rust
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效
//如果使用s,则报错
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
返回值也可以转移所有权
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
四、引用和借用
我们有时不想移动来移动去的,只是想稍微用一下堆上的数据而已
——引用
!!!需要记住
- 任意给定时间,只能有一个mut可变引用,或者,多个不可变引用
引用必须有效,不要返回悬垂引用(可以尝试直接使用移动语义来返回,转移所有权)
1、引用,用引用定义借用
允许使用值,但不获取所有权;引用离开作用域时,引用指向的值也不会销毁
- 引用没有所有权,函数使用引用参数时不需要返回值来归还所有权,因为不曾拥有~
- 我们将获取引用作为参数称为 借用borrowing
- 书写要点
- 实参标记&
- 形参&标记到类型上
代码示例
危害:未定义行为,运行时难跟踪,难以诊断
产生原因
书写要点:实参和形参都是直接在&后加上mut
- 限制:特定作用域中,特定数据,只能有一个可变引用
- 阻止数据竞争data race
代码示例
- 可变理解为写,不可变理解为读 ```rust //多于一个的写 let mut s = String::from(“hello”); let r1 = &mut s; let r2 = &mut s; //简单的修改 let mut s = String::from(“hello”); { let r1 = &mut s; }//r1离开作用域,内外都是只有一个写 let r2 = &mut s;
//多个读没问题,但读写不能共存 //多个读一个写时,具体是否报错根据具体情况而定,下面会报错 let mut s = String::from(“hello”); let r1 = &s; //ok let r2 = &s; //ok let r3 = &mut s; //damn!!! println!(“{}, {}, and {}”, r1, r2, r3); //上面的例子换成下面这样就没事了,好优化的情况下编译器会控制作用域的编排 let mut s = String::from(“hello”); let r1 = &s; //ok let r2 = &s; //ok println!(“{} and {}”, r1, r2); //之后r1 r2不会用了 let r3 = &mut s; //ok println!(“{}”, r3);
<a name="m2kMA"></a>
### 4、悬垂指针
- 当我有一些数据的引用时,编译器确保数据不会在引用之前离开作用域
- 在不用生命周期参数的情况下,我自己制造一个悬垂,编译器会保存叫我用lifetime parameter
```rust
fn dangle() -> &String{//返回一个对字符串的引用
let s = String::from("hello");
&s; //返回引用
}//s离开作用域并被丢弃,释放内存
fn main(){
let reference_to_nothing = dangle();
}
//解决办法:直接返回一个String,所有权直接被移动出去,没有值被释放
fn no_dangle() -> String {
let s = String::from("hello");
s
}
Slice类型
是一个没有所有权的数据类型
slice允许引用集合中一段连续的元素序列,而不引用整个集合
1、字符串slice
是对String中一部分值的引用
[start, end]——左闭右开区间
let s = String::from("hello world");
let hello = &s[..5];
let word = &s[6..];
let hello_world = &s[..];
设计一个函数,返回使用空格分隔的第一个单词
- 返回的是字符串slice,它有非常好的性质
如果上面的函数返回的不是slice,而是一个usize会怎样???
- main函数中首先将word变量绑定到usize的值上,假设是5
- 而后清空字符串,此时word仍是原来的usize值,还是5
- 此时这个word无效,但程序在编译时没报错!!!!
为啥我想要程序编译报错呢??
- 我需要时刻担心索引word和s的数据是否同步,不然用过时的索引进行访问会出问题!
- 代码可能没直接的错误,但是逻辑是错的
- 这个时候slice就发挥作用了
回头看slice到底好在哪儿,如下图代码fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word 的值为 5
s.clear(); // 这清空了字符串,使其等于 ""
// word 在此处的值仍然是 5,
// 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效!
}
slice的数据结构存储了自己的开始位置和长度
当调用slice版本first_word时,编译器会保证指向String的引用持续有效
- 报immutable borrow问题:cannot borrow
s
as mutable because it is also borrowed as immutable - 本质还是:拥有某个值的不可变引用后,就不能再获取一个可变引用
- clear要清空String,因此尝试获得可变引用;而word已经表明引用不可变了
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for(i, &item) in bytes.iter().enumerate() {
if item==b' ' {
return &s[0..i]; //返回的不是一个usize索引,而是一个切片
}
}
&s[..];
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // 错误!
}
2、惊!!!字符串字面量就是slice!
let s = "Hello world"; //这里s的类型就是&str
- clear要清空String,因此尝试获得可变引用;而word已经表明引用不可变了
- 报immutable borrow问题:cannot borrow
s类型是&str:是指向二进制程序特定位置的slice
- 可以看出为什么字符串字面量不可变:&str是不可变引用
3、老司机字符串传参
first_word是个好函数,可惜之前写的只能作用于String,是堆上分配可动态改变长度的那种,但我也想用在字符串字面量上 ——可以将字符串slice作为参数,从而适用于String和&str
下面代码很重要
fn first_word(s: &str) -> &str { ... }
fn main() {
let my_string = String::from("hello world");
// first_word 中传入 `String` 的 slice
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word 中传入字符串字面值的 slice
let word = first_word(&my_string_literal[..]);
// 因为字符串字面值 **就是** 字符串 slice,
// 这样写也可以,即不使用 slice 语法!
let word = first_word(my_string_literal);
}
4、其他类型slice
和字符串工作方式一样
下面的slice变量类型就是 &[i32]
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];