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. 1个可变的引用。
  2. 任意数量不可变的应用。
  3. 引用必须一致有效(数据不会被销毁,不会出现悬空指针的情况)。

切片

不持有所有权的数据类型:切片。

字符串切片

[开始索引..结束索引]

  • 开始索引就是切片起始位置的索引值。
  • 结束索引是切片终止位置的下一个索引值。

索引第一个是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:切片,字符串字面量