Rust所有权
Rust 所有权
无需GC就能保证内存的安全。
函数中借用是因为不需要转移所有权,只需要使用实参的数据。
Stack and Heap
储存数据
在rust里,一个值在栈还是堆上对语言的行为和为什么做这些决定有重大影响。
栈,后进先出,必须拥有已知的固定大小。未知的数据或运行时的大小可能发生改变的数据必须放在堆上,这个过程不叫做分配。因为指针也是固定的,可以把指针放在stack上,访问实际数据,必须用指针来定位。
堆,放入堆中,请求一定数量的空间。在堆内存中找到一块足够大的空间,标记为再用,返回指针(指向这个内存地址),这个过程叫做分配。
放入栈比放入堆要快得多。
访问数据
访问heap中的数据要比stack慢,因为需要指针才能找到heap中的数据(跳转)。在heap上分配大量的空间也是需要时间的,
如果数据存放的比较近,那么处理器的处理速度就会更快一点(stack)
如果数据存放的比较远,那么处理器的处理速度就会更慢一点(heap)
函数调用
函数调用,值被传到函数(包括heap指针),函数本地的变量被压到stack上,函数结束后释放,在stack上弹出(出栈入栈)
所有权
管理heap是所有权存在的原因。
所解决的问题:
- 跟踪代码的哪些部分正在使用heap的哪些数据。
- 最小化heap上的重复数据量。
- 清理heap上未使用的数据避免空间不足。
所有权规则,内存和分配
规则
- 每个值都有一个变量,这个变量是该值的所有者。
- 每个值同时只能有一个所有者。
- 当所有者超出作用域(scope)时,该值会被删除。
变量作用域
和python一样。不能提前声明。
String 类型
String类型是复杂类型,存在heap上。之前的类型是基础标量类型,存在栈上。
// '::' 表示from是String类型下的函数。
let s = String::from("hello");
和字符串字面值对比:
- 字符串字面值是写死不可变的。字符串值不不定长的。
- 字符串字面值直接被硬编码到最终的可执行文件里。
- String为了可变性,需要在heap上分配内存来保存编译时的未知文本内容:操作系统必须在运行时来请求内存。用完之后要释放,rust里当变量离开作用域就被释放。
- 释放内存用 drop()
Move 变量和数据交互的方式(所有权转移)
多个变量可以与同一个数据使用一种独特的方式来交互。
一个String类型的值分为两部分,其中一部分放在栈中,这部分包括指向具体值的指针,长度,容量(String从操作系统总共获得内存的总字节数)。还有一部分是具体的值,放在堆中,并且包括每个字符的索引(index)。
当复制一个变量时,只复制的是栈上的数据。当如果以此离开作用域时可能会出现二次释放。
为了保证内存的安全:rust会让被复制的变量失效。所以当被复制的变量离开作用域时,它并不会被释放。在调用的时,被复制的变量也不能被调用(编译会报move错误,提示已被转移),只能使用复制后的变量,保证同时只有一个所有者。
浅拷贝 深拷贝
let s1 = 32;
let s2 = s1
// 这是发生的是移动(move),或者说浅拷贝,而不是深拷贝。
rust不会自动创建数据的深拷贝。
深度拷贝
let s1 = 32;
let s2 = s1.clone()
深度拷贝会把堆上的数据都会克隆一遍。栈复制,堆克隆。
注意:只有复杂变量深度拷贝和浅拷贝有区别(也就是说标准类型都可以互相复制)。因为标准变量在编译时都确定了自己的大小,并且能将自己的数据完整的存在栈中。
所有权和函数
变量的所有权随着函数调用会发生转移:
- 把一个值赋给其他变量时所有权发生移动。
- 当一个包含heap数据的变量离开作用域时,它的值会被drop函数清理,除非数据的所有权移动到另一个变量上了。
fn main(){
let s = String::from("helloWorld");
// 所有权此时转移到takeOwnership
takeOwnership(s);
// takeOwnership没有返回所有权,所以这里编译会失败
println!("{}", s);
}
fn takeOwnership(someString: String) {
println!("{}", someString);
}
fn main(){
let s = String::from("helloWorld");
// 所有权此时转移到takeOwnership
let s = takeOwnership(s);
// takeOwnership返回了所有权,打印成功
println!("{}", s);
}
fn takeOwnership(someString: String) -> String {
println!("{}", someString);
someString
}
基础类型的所有权传入的只是原数据的副本,所以不会发生转移所有权。
fn main(){
let s = 5;
checkIntOwnership(s);
// 这里可以正常打印,因为s是基础变量,存于栈中
println!("{}", s);
}
fn checkIntOwnership(someNumber: i32) {
// 这里的形参会把原始数据的副本
println!("{}", someNumber);
}
copy trait
一个可以copy的接口,如果一个类型实现了copy的trait,那么旧的变量在赋值后仍然可用。一般用于基础变量类型的完全存放在stack上面的类型,任何需要分配内存或某种资源的都不是copy的。
drop trait
如果一个类型的一部分实现了drop trait,那么就不能实现 copy trait 了。
引用和借用
引用的意思是创造一个新的指针,指向原来的指向数据的指针。
新指针 -> 原指针 -> heap数据
&符号表示引用:允许引用某些值而不取得所有权。
fn main(){
let s = String::from("helloWorld");
// 这里传入的只是s的应用,而不是s的变量
let len = calcuateLength(&s);
println!("{}", len);
}
fn calcuateLength(someString: &String) -> usize {
// 签名里接收的也是一个字符串的引用
someString.len()
}
当一个函数的参数作为引用而不是真实所有权的时候,这种方式叫做借用。
借用不能修改借用的东西,默认情况下是不可变的。如果需要可变,就需要传入一个可变变量的引用。
fn main(){
// 改为可变变量
let mut s = String::from("helloWorld");
// 传入可变变量引用
let len = calcuateLength(&mut s);
println!("{}", len);
}
fn calcuateLength(someString: &mut String) -> usize {
// 签名里接收的也是一个字符串的可变变量引用
someString.push_str(", welcome to the rust world!");
someString.len()
}
修改了可变引用之后,原变量数据也会发生变化。但可变引用也是有限制的:在特定作用域内,对某一块数据,只能有一个可变的应用(类似读写锁,为了编译时防止数据竞争),非同时就可以。
fn main(){
let mut s = String::from("helloWorld");
let k = &mut s;
// 多次应用
let j = &mut s;
println!("{}", k);
}
不可以同时拥有可变的引用和不可变的应用,可以拥有多个不可变的应用。
fn main(){
let mut s = String::from("helloWorld");
let r1 = & s;
let r2 = & s;
let k = &mut s;
println!("{} {} {}", r1, r2, s);
}
悬空引用
悬空指针指的是一个指针应用了内存中的某个地址,而这块内存已经释放并分配给其他人用了。
rust会防止一个应用数据在离开作用域之前,数据不会离开作用域。也就是说当一个数据将会被销毁时,如果返回一个这个数据的引用,编译将不会被通过。
引用的规则总结:
- 1个可变的引用。
- 任意数量不可变的应用。
- 引用必须一致有效(数据不会被销毁,不会出现悬空指针的情况)。
切片
不持有所有权的数据类型:切片。
字符串切片
[开始索引..结束索引]
- 开始索引就是切片起始位置的索引值。
- 结束索引是切片终止位置的下一个索引值。
索引第一个是0,可以不用写:[..5],拿最后一个也不用写[6..],指向整个字符串的切片[..]
切片只能在UTF8范围之内。字符串字面量也是切片,所以字符串字面值也是不可引用的。
let s = "hello world";
字符串切片 &str
fn checkString(s: &String) -> &str{
&s[..]
}
// 可以采用&str来作为参数类型,这样可以同时接受String和&str的参数。
// 使用切片就直接调用该函数。
// 使用String,就创建一个完整的String切片来调用该函数传递进去。
fn main(){
// 传入切片类型,完整切片
let myString = String::from("test1");
let check1 = checkString(&myString[..]);
// 字符串字面量是切片类型
let youString = "test2";
let check2 = checkString(youString);
}
fn checkString(s: &str) -> &str{
&s[..]
}
#### 数组切片
```rust
fn test3(){
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
}
引用总结
- String: String类型
- &String: String类型引用
- &str:切片,字符串字面量