rust(四) - 所有权

188 阅读12分钟

什么是所有权

  • Rust的核心特性就是所有权
  • 所有程序在运行时都必须管理它们使用的计算机内存的方式
    • 有些语言有垃圾收集机制,在程序运行时,它们会不断地寻找不再使用的内存
    • 在其他语言中, 程序员必须显式地分配和释放内存
  • Rust 采用了第三种方式:
    • 内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则。
    • 当程序运行时,所有权特性不会减慢程序的运行速度。

stack and heap(栈内存和堆内存)

  • stack 按值的接收顺序来存储,按相反的顺序将它们移除(先进后出,后进先出,LIFO)
    • 添加数据叫做压入栈
    • 移除数据叫做弹出栈
  • 所有存储在stack上的数据必须拥有已知的固定的大小
    • 编译时大小未知的数据或运行时大小可能发生改变的数据必须存放在heap上
  • heap 内存组织性差一些:
    • 当你把数据放入heap时,你会请求一定数量的空间
    • 操作系统在heap里找到一块足够大的空间, 把它标记为在用,并返回一个指针,也就是这个空间的地址。
  • 把值压到stack上不叫分配
  • 因为指针是已知固定大小的,可以把指针存存放在stack上。
    • 但如果想要实际数据,你必须使用指针来定位。
  • 把数据压到stack上要比在heap上分配快得多:
    • 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在stack的顶端
  • 在heap上分配空间需要做更多的工作
    • 操作系统首先需要找到一个足够大的空间来存放数据,然后要做好妄方便下次分配

访问数据

  • 访问heap中的数据要比访问stack中的数据慢,因为需要通过指针才能找到heap中的数据
    • 对于现代的处理器来说,由于缓存的缘故,如果指令在内在中跳转的次数越少,那么速度就越快
  • 如果数据存放的距离比较近, 那么处理器的处理速度就会更快一些(stack上)
  • 如果数据存放的距离比较远, 那么处理速度就会慢一些(heap上)
    • 在heap上分配大量的空间也是需要时间的

函数调用

  • 在你的代码调用函数时,值被传入到函数(也包括指向heap的指针)。函数本地的变量被压到stack上。当函数结束后,这些值会从stack上弹出。

所有权存在的原因

  • 所有权解决的问题
    • 跟踪代码的哪些部分正在使用heap的哪些数据
    • 最小化heap上的重复数据量
    • 清理heap上未使用的数据以避免空间不足
  • 一旦你懂的了所有权,那么就不需要经沉睡去想stack 或heap了。
  • 但是知道管理heap数据是所有权存在的原因,这有助于解释它为什么会这样工作。

所有权规则

  • 每个值都有一个变量,这个变量就是该值的所有者
  • 每个值同时只有一个所有者
  • 当所有者超出作用域(scope)时,该值将被删除

变量作用域

  • scope 就是程序中一个项目的有效范围。
    • String类型
      • string比那些基础标量数据类型更为复杂
      • 字符串字面值:程序里手写的那些字符串值,它们是不可变的
      • Rustust还有第二种字符串类型:string
        • 在heap 上分配。能够侟储在编译时未知数量的文本
    • 创建string类型的值
      • 可以使用from函数从字符串字面值创建出String类型
      • let s = String::from("hello")
        • "::"表示from是string类型下的函数
      • 这类字符串是可以被修改的(例子)
      • 为什么string类型的值可以修改, 而字符串字面值却不能修改?
        • 因为它们处理内存的方式不同
  • 内在和分配
    • 字符串字面值, 在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里
      • 速度快、高效。是因为其不可变性
    • String类型,为了支持可变性, 需要在heap上分配内在来保存编译时未知的文本内容:
      • 操作系统必须在运行时来请求内存
        • 这步通过调用string::from来实现
      • 当用完string之后,需要要使用某种方式将内存返回给操作系统
        • 这步,在拥GC的语言中,GC会跟踪并清理不再使用的内存
        • 没有GC,就需要我们去识别内存何时不再使用,并调用代码将它返回。
          • 如果忘了,那就浪费内存
          • 如果提前做了,变量就会非法
          • 如果做了两次,也是BUG。必须一次分配对应一次释放
    • rust 采用了不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交还给操作系统。
    • drop函数

    变量和数据交互的方式: 移动 (MOVE)

    • 多个变量可以与同一个数据使用一种独特的方式来交互
    • let x = 5; let y = x;
    • 整数是已知且固定大小的简单的值,这两个5被压到stack中
    • String复杂类型的移动
      • let s1 = String::from('hello');
        let s2 = s1;
      • 一个String由3部分组成
        • 一个指向存放字符串内容的内存的指针
        • 一个长度
        • 一个容量
      • 上面这些东西放在stack上。
      • 存放在字符串内容的部分在heap上
      • 长度len,就是存入字符串内容所需要的字节数
      • 容量capacity是指string从操作系统总共获得内存的总字节数 image.png
    • 当把s1赋给S2,string的数据被复制了一份:
      • 在stack上复制了一份指针、长度、容量
      • 并滑复制指针所指向的heap上的数据
    • 当变量离开作用域时,Rust会自动调用drop函数,并将变量使用的heap内存翻译。
    • 当s1、s2离开作用域时,它们的都会尝试释放相同的内存:
      • 二次释放(double free) bug
        • 为子保证内存的安全:
          • Rust没有尝试复制被分配的内存
          • Rust 让 s1 失效。
            • 当s1离开作用域的时候,Rust不需要释放任何东西
              image.png
  • 浅拷贝(shallow copy)
  • 深拷贝(deep copy)
    • 你也许会将复制指针、长度、容量视为shallow copy,但由于rust让s1失效了,所以我们用一个新的术语:移动(move)
    • 隐含的一个设计原则:Rust不会自动创建数据的深拷贝
      • 就运行性能而言,任何自动赋值的操作都是廉价的
    • clone
      • 如果真想对heap上面的string数据进行深度拷贝,而不仅仅是stack上的数据,可以使用clone方法

image.png

  • ...
    • stack上面的数据(复制)
    • copy trait,可以用于像整数这样完全存入在stack上面的类型
    • 如果一个类型实现了copy这个trait,那么旧的变量在赋值后仍然可用
    • 如果一个类型或者该类型的一部分实现了drop trait,那么rust不允许让它再去实现copy trait了
    • 一些拥有copy trait的类型
      • 任何简单的标量的组合类型都可以是copy的
      • 任何需要分配内存或某种资源的都不是copy的
      • 一些拥有copy trait的类型
        • 所有的整数类型,例如 u32
        • bool
        • char
        • 所有浮点类型,例如:f64
        • tuple(元组), 如果其所有的字段都copy的
          • [i32, i32] 是

          • [i32, String] 不是

fn main() {
    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);
}

所有权与函数

  • 在语义上,将值传递给函数和把值赋给变量是类似的:
    • 将值传递给函数将发生移动或复制
    •       fn main() {
              let s = String::from("hello");  // s comes into scope
      
              takes_ownership(s);             // s's value moves into the function...
                                              // ... and so is no longer valid here
      
              let x = 5;                      // x comes into scope
      
              makes_copy(x);                  // x would move into the function,
                                              // but i32 is Copy, so it's okay to still
                                              // use x afterward
      
          } // Here, x goes out of scope, then s. But because s's value was moved, nothing
            // special happens.
      
          fn takes_ownership(some_string: String) { // some_string comes into scope
              println!("{}", some_string);
          } // Here, some_string goes out of scope and `drop` is called. The backing
            // memory is freed.
      
          fn makes_copy(some_integer: i32) { // some_integer comes into scope
              println!("{}", some_integer);
          } // Here, some_integer goes out of scope. Nothing special happens.
      
  • 返回值与作用域
    • 函数在返回值的过程中同样也会发生所有权的转移
    • 一个变量的所有权总是遵循同样的模式:
      • 把一个值赋给其它变量时就会发生移动
      • 当一个包含heap数据的变量离开作用域时,它的值就会被drop函数清理,除非数据的所有权移动到另一个变量上了
            fn main() {
                      let s1 = gives_ownership();         // gives_ownership moves its return
                                                          // value into s1
    
                      let s2 = String::from("hello");     // s2 comes into scope
    
                      let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                                          // takes_and_gives_back, which also
                                                          // moves its return value into s3
                  } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
                    // happens. s1 goes out of scope and is dropped.
    
                  fn gives_ownership() -> String {             // gives_ownership will move its
                                                               // return value into the function
                                                               // that calls it
    
                      let some_string = String::from("yours"); // some_string comes into scope
    
                      some_string                              // some_string is returned and
                                                               // moves out to the calling
                                                               // function
                  }
    
                  // This function takes a String and returns one
                  fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                                        // scope
    
                      a_string  // a_string is returned and moves out to the calling function
                  }
                  
    
  • 如何让函数使用某个值,但是不获得其所有权?
    • Rust 有一个特性叫做“引用”
    fn main() {
              let s1 = gives_ownership();         // gives_ownership moves its return
                                                  // value into s1
    
              let s2 = String::from("hello");     // s2 comes into scope
    
              let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                                  // takes_and_gives_back, which also
                                                  // moves its return value into s3
          } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
            // happens. s1 goes out of scope and is dropped.
    
          fn gives_ownership() -> String {             // gives_ownership will move its
                                                       // return value into the function
                                                       // that calls it
    
              let some_string = String::from("yours"); // some_string comes into scope
    
              some_string                              // some_string is returned and
                                                       // moves out to the calling
                                                       // function
          }
    
          // This function takes a String and returns one
          fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                                // scope
    
              a_string  // a_string is returned and moves out to the calling function
          }
    

引用和借用

  •     fn main() {
              let s1 = String::from("hello");
    
              let len = calculate_length(&s1);
    
              println!("The length of '{}' is {}.", s1, len);
          }
    
          fn calculate_length(s: &String) -> usize {
              s.len()
          }
    
  • 参数的类型是&string而不是String

  • &符号就表示引用:允许你引用某些值而不取得其所有权

    image.png

  • 可变引用

    • 可变引用有一个重要的限制:在特定作用域内,对某一块数据,只能在一个可变的引用。
      • 这样做的好处是可在编译时防止数据竞争。
    • 以下三种行为下会发生数据竞争
      • 两个或多个指针同时访问同一个数据
      • 至少有一个指针用于写入数据
      • 没有使用任何机制来同步对数据的访问
    • 可以通过创建新作用域,来允许非同时的创建多个可变引用
      fn main() {
             let mut s = String::from("hello");
      
             {
                 let r1 = &mut s;
             } // r1 goes out of scope here, so we can make a new reference with no problems.
      
             let r2 = &mut s;
         }
      
  • 另外一个限制

    • 不可以同时拥有一个可变引用和一个不变的引用
    • 多个不变的引用是可以的
    fn main() {
         let mut s = String::from("hello");
    
         let r1 = &s; // no problem
         let r2 = &s; // no problem
         let r3 = &mut s; // BIG PROBLEM
    
         println!("{}, {}, and {}", r1, r2, r3);
     }
    
    

悬空引用 dangling references

  • 悬空引用:一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其它人使用了。
  • 在Rust里,编译器可保证引用永远都不是悬空引用:
    • 如果你引用了某些数据,编译器将保证在引用永远都不是悬空引用:
      • 如果你引用了某些数据,编译器将保证在引用离开作用域之前数据不会离开作用域
      fn main() {
              let reference_to_nothing = dangle();
          }
      
          fn dangle() -> &String {
              let s = String::from("hello");
      
              &s
          }
      
      
  • 引用的规则
    • 在任何给定的时刻,只能满足下列条件之一
      • 一个可变的引用
      • 任意数量不可变的引用
    • 引用必须一直有效

切片

  • rust的另外一种不持有所有权的数据类型:切片(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() {}
    
    
    • 它接收字符串作为参数
    • 返回它在这个字符串里找到的第一个单词
    • 如果函数没找到任何空格, 那么整个字符串就被返回

字符串切片

  • 字符串切片是指向字符串中一部分内容的引用
  • 形式:[开始索引...结束过引]
    • 开始索引就是切片起始位置的索引值
    • 结束索引是切片终止位置的下一个索引值
    fn main() {
       let s = String::from("hello world");

       let hello = &s[0..5];
       let world = &s[6..11];
   }

image.png

  • 字符串切片的范围索引必须发生在效的uft-8字符边界内
  • 如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出
  • 使用字符串切片
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];
           }
       }

       &s[..]
   }

   fn main() {
       let mut s = String::from("hello world");

       let word = first_word(&s);

       s.clear(); // error!

       println!("the first word is: {}", word);
   }

  • 字符串字面值是切片
    • 字符串字面值被直接存储在二进制程序中
    • let s = "hello world!"
    • 变量s的类型是&str,它是一个指向二进制程序特定位置的切片
      • &str是不可变引用,所以字符串面值也是不可变的
  • 将字符串切片作为参数传递
    • fn first_word(s: &String) -> &str {
    • 有经验的rust开发者会采用&str作为参数类型,因为这样就可以同时接收string和&str类型的参数了
    • fn first_word(s: &Str) -> &str {
      • 使用字符串切片,直接调用该函数
      • 使用string,可以创建一个完整的string切片来调用该函数
    • 定义函数时使用字符串切片来代替字符串引用会使我们的api更加通用,且不会损失任何功能。
  • 其它类型的切面

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}