Rust - 初识变量管理

变量绑定与解构

  • 绑定变量

    类似于其他语言的赋值, 绑定内存对象的 所有权

    let a = "hello world";
    
  • 变量的可变性

    变量默认是不可变的, 如上面的 a, 默认是不可再改变其他值, 如需要可变的变量,使用 mut 关键字:

    let mut a = "hello world";
    a = "hi there";
    
  • 解构

    类似于 python 的 unpack

    fn main() {
        let (a, mut b): (bool,bool) = (true, false);
        // a = true,不可变; b = false,可变
        println!("a = {:?}, b = {:?}", a, b);
    
        b = true;
        assert_eq!(a, b);
    }
    

    更复杂的解构

    struct Struct {
        e: i32
    }
    
    fn main() {
        let (a, b, c, d, e);
    
        (a, b) = (1, 2);
        // _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
        [c, .., d, _] = [1, 2, 3, 4, 5];
        Struct { e, .. } = Struct { e: 5 };
    
        assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
    }
    
  • 常量设置

    完全不可变, 在编译后就固定不变了, 使用 const 关键字

    const MAX_SIZE: u32 = 1000;
    
  • 变量遮蔽(shadowing)

    这是一个变量再绑定

    fn main() {
        let x = 5;
        // 在main函数的作用域内对之前的x进行遮蔽
        let x = x + 1;
    
        {
            // 在当前的花括号作用域内,对之前的x进行遮蔽
            let x = x * 2;
            println!("The value of x in the inner scope is: {}", x);
        }
    
        println!("The value of x is: {}", x);
    }
    

    Note that shadowing a name does not alter or destroy the value it was bound to, and the value will continue to exist until it goes out of scope, even if it is no longer accessible by any means

所有权

栈和堆

  • 栈: 存储大小已知且固定的数据, 如 基本的数据类型
  • 堆: 存储大小未知或者可能变化的数据, 如 String

所有权定义

  • Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  • 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  • 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

所有权转移仅发生在存储于 上的变量而已, 对于存储在 上的变量, 类似的语句,只会发生拷贝, 然后进行绑定

  • 操作1 (未发生所有权转移)
    let x = 5;
    let y = x;
    
  • 操作2 (存在所有权转移)
    let s1 = String::from("hello");
    let s2 = s1;
    

深拷贝与浅拷贝

  • 深拷贝
    let s1 = String::from("hello");
    let s2 = s1.clone();
    
    println!("s1 = {}, s2 = {}", s1, s2);
    
  • 浅拷贝

发生在 上的就是浅拷贝, 包括下面的值拷贝, 以及上面的所有权转移, 发生了 指针变量大小内存容量 的拷贝, 这些变量的 元数据, 是存储在 上的

let x = 5;
let y = x;

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

  • 可以直接进行浅拷贝而不会发生所有权错误的类型
    • 所有基本类型:
      • 所有整数类型,比如 u32
      • 布尔类型,bool,它的值是 true 和 false
      • 所有浮点数类型,比如 f64
      • 字符类型,char
      • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是
    • 不需要分配内存或某种形式资源的类型
      • 不可变引用 &T , 这里拷贝的是变量的元数据, 是存储在栈中的

函数传值与返回

变量通过函数传值与返回传递, 效果是等同于 “赋值” 操作的, 同样适用于 所有权 转移规则

  • 函数传值
    fn main() {
        let s = String::from("hello");  // s 进入作用域
    
        takes_ownership(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 main() {
        let s1 = gives_ownership();         // gives_ownership 将返回值
                                            // 移给 s1
    
        let s2 = String::from("hello");     // s2 进入作用域
    
        let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                            // takes_and_gives_back 中,
                                            // 它也将返回值移给 s3
    } // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
      // 所以什么也不会发生。s1 移出作用域并被丢弃
    
    fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                                 // 调用它的函数
    
        let some_string = String::from("hello"); // some_string 进入作用域.
    
        some_string                              // 返回 some_string 并移出给调用的函数
    }
    
    // takes_and_gives_back 将传入字符串并返回该值
    fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
    
        a_string  // 返回 a_string 并移出给调用的函数
    }
    

引用与借用

  • 引用获取

    通过 &x 获得 变量 x引用

    let x: u32 = 5;
    let y = &x; 
    
  • 完成借用

    上面 let y = &x; 这个表达式, 完成了 yx 的借用

  • 解引用 - 使用借用变量的方式

    我们知道借用变量的引用,通过 引用 可以获取值, 这个获取的方式, 叫做 解引用 , 如下 *y, 为 解引用 操作

    let x: u32 = 5;
    let y = &x; 
    
    assert_eq!(5, *y);
    println!("{}", *y);
    
  • 不可变引用 与 可变引用

    可变引用 是指可以通过 解引用 来更改指变量的 值, 可读可写, 不可变引用 反之, 只读。 在上面的例子中, 引用变量 y, 是一个 不可变引用, 因为所引用的变量 x, 本身就是一个 不可变变量, 自然更是无法通过 引用 来更改的

    • 创建 可变引用 的必要条件
      1. 被引用变量需要声明为 可变变量, 使用 mut 关键词
      2. 创建引用变量时, 也需要声明是可变的, 同样使用 mut
      fn main() {
          let mut x: u32 = 5;
          println!("{}", x); // 输出为 5
      
          // 借给 y
          let y = & mut x;
      
          assert_eq!(5, *y);
      
          *y = 8; // 更改
      
          println!("{}", *y); // 输出为 8
      }
      
    • 对于 可变变量 同样也可以创建 不可变引用, 但是只读
       fn main() {
           let mut x: u32 = 5;
           println!("{}", x);
      
           // 借给 y
           let y = &x; // 此处不使用 mut
      
           *y = 8; // 此处会报错了
       }
      
  • 多个引用同时存在的限制
    • 可变引用与不可变引用不能同时存在

    为了安全考虑, 防止 不可变引用 在使用时, 变量发生意料之外的改变

    fn main() {
        let mut x: u32 = 5;
    
        let y1 = &x; // 通过不可变引用借用, 没问题
        let y2 = &x; // 定义第 2 个不可变引用, 也没问题
        let y3 = &mut x; // 同时定义了 1 个 可变引用, 不被编译器允许, 这里回报错
    
        println!("{}, {}", y1, y2);
        println!("{}", y3);
    
    }
    
    • 报错输出

      error[E0502]: cannot borrow x as mutable because it is also borrowed as immutable

      error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
        --> src/main.rs:6:14
         |
      4  |     let y1 = &x; // 通过不可变引用借用, 没问题
         |              -- immutable borrow occurs here
      5  |     let y2 = &x; // 定义第 2 个不可变引用, 也没问题
      6  |     let y3 = &mut x; // 同时定义了 1 个 可变引用, 不被编译器允许, 这里回报错
         |              ^^^^^^ mutable borrow occurs here
      ...
      10 |     println!("{}, {}", y1, y2);
         |                        -- immutable borrow later used here
      
      
    • 可变引用同时只能存在 1 个

      这个是为了安全考虑, 同时存在多个可修改的地方, 那就跟 python 样的, 被哪修改了都需要排查好久哦, 还需要加锁保证数据安全, rust 这个限制就避免了这个麻烦

      fn main() {
          let mut x: u32 = 5;
      
          let y1 = & mut x; // 定义第 1 个 可变引用, 没问题
          let y2 = & mut x; // 第 2 个不允许
      
          println!("{}, {}", y1, y2);
      
      }
      
      • 报错输出

      error[E0499]: cannot borrow x as mutable more than once at a time

  • 悬垂引用(Dangling References)

    如果一个 指针 指向的值被释放了, 但是这个 指针 没有被释放, 那么这个 指针 就变变成 悬垂指针, 这是不安全的, rust 编译器可以发现这个风险

    fn main() {
        let reference_to_nothing = dangle();
    }
    
    fn dangle() -> &String {
        let s = String::from("hello");
    
        &s
    }
    
    • 报错信息
      error[E0106]: missing lifetime specifier
       --> src/main.rs:5:16
        |
      5 | fn dangle() -> &String {
        |                ^ expected named lifetime parameter
        |
        = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
      help: consider using the `'static` lifetime
        |
      5 | fn dangle() -> &'static String {
        |                ~~~~~~~~
      

      可以通过直接返回 s 来改善, 这样是发生了 所有权, 是被编译器允许的

  • 借用规则总结

    • 同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
    • 引用必须总是有效的

参考