四、作用域与值传递

118 阅读6分钟

四、作用域与值传递

1. 作用域

  • Rust 中,常用的有两种作用域:块级作用域、模块作用域

    • 这两种作用域类似于 ECMAScript 6 中的 块级作用域模块作用域
      • 不同于 Javascript 的是,Rust 没有函数作用域
  • Rust 中,通常来说,一个文件即为一个模块,可以为这个模块指定 私有/公有 元素 (结构体/枚举/函数/值)

  • Rust 中,变量定义之前是不可访问的 (常量可以),变量被重新定义之后,旧变量是不可访问的

    // main.rs
    
    const OUTER_2: i32 = OUTER + 1; // 这个定义是允许的
    const OUTER:i32 = 123; // OUTER 可以在这个模块内的任意位置被访问
    
    fn main() {
        // 这里以上、main 函数外的任意位置都不能访问 a
        let a = "abc";
        println!("a = {}, OUTER = {}", a, OUTER);
    
        {
            // 这里及以上,都不能访问 b
            let b = "innerB";
    	    println!("a = {}, b = {}, OUTER = {}", a, OUTER);
            // 这个块以外的任何地方都不能访问 b
        }
    
        // 这里以上,一直到 a 初次被定义的位置,都只能访问到初次定义的变量 a
        let a = 456;
        // 这里以下,一直到 main 函数结束的位置,都只能访问到重新定义的变量 a
        
        println!("a = {}, OUTER = {}", a, OUTER);
    }
    
  • 学习这部分最简单的方式,就是尝试在 IDE 中直接去定义变量,然后观察 IDE 给出的提示

  • 如果期望某个模块定义的 函数/常量/类 等可以被其他模块访问,可以使用关键字 pub 暴露出去

  • Rust 中与作用域相关的还有另一个概念:生命周期

    • 这个生命周期是指变量的可用状态,和其他语言、框架中的生命周期的概念不同,尤其是和 Vue, React 等前端框架的生命周期概念完全不同

2. 所有权

  • 每一个值,都有且只有唯一一个拥有它的变量,这个变量被称为值的所有者 (owner)
  • 当所有者所在的作用域被销毁时,所有者对应的值也会被丢弃

2.1 租借值 (引用)

  • 这个概念和 C 语言中的指针很类似,相关的一些概念和操作也是类似的

  • 通过 租借/取引用 某个变量而得到的值

  • 特性:允许使用这个值,但不允许改变这个值

  • 所有者:被 & 租借的原始对象

  • 存在堆里的数据

    • 可被租借的值,这些值的原始值都是存在堆里的,栈里只保存了引用
    • 不可通过引用来修改实际所有者的值
    • 如果一个所有者的值被借用了,那么这个所有者的值将不允许被修改,直到租借值被释放
  • 引用的规则

    • 引用必须总是有效的
    • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用
      • 不能同时有既有可变引用又有不可变引用

这部分应该要画一个图才能更好地理解,但是我懒,所以没有画

// 一个示例。建议直接将这部分代码 copy 到 IDE 中查看

fn main() {
    let hello = String::from("Hello, world!");
    let quoting = &hello;
    quoting.push_str(" 123"); // 报错:cannot borrow `*quoting` as mutable, as it is behind a `&` reference (不能借用 `*quoting` 作为可变的变量,因为它在 `&` 引用后面)

    let mut hello2 = String::from("Hello, world!");
    let quoting2 = &hello2;
    quoting2.push_str(" 123"); // 和上面的报错一样,而且 hello2 还会给 warning 提示 `没必要定义为可变的` 

    let mut hello3 = String::from("Hello, world!");
    let quoting3 = &mut hello3;
    quoting3.push_str(" 123"); // 可以正确执行
    println!("{}", quoting); // Hello, world! 123

    let quoting4 = &hello; // 允许,因为 hello 是不可变引用
}

2.2 变量的值传递

  • 在 JavaScript 中

    • JavaScript 中有两种值类型:

      • 原始值
      • 引用值
    • 这两种值类型发生拷贝时:

      • 原始值直接拷贝,两个变量之间的修改互不影响
      • 引用值只拷贝引用,当通过一个引用来修改某属性时,另一个引用对应的属性也会发生改变
      // JavaScript 示例代码
      let name = "Cherry";
      let secondName = name;
      name = "Jelly";
      console.log("name =",name); // name = "Jelly"
      console.log("secondName =", secondName); // secondName ="Cherry"
      
      const user = {
          name: "Cherry",
      };
      const secondUser = user;
      user.name = "Jelly";
      console.log("user.name =", user.name); // user.name = "Jelly";
      console.log("secondUser.name =", secondUser.name); // secondUser.name = "Jelly";
      console.log(user ==== secondUser); // true;
      console.log(user.name ==== secondUser.name); // true;
      
      
  • Rust 中,Rust 有一套特有的传递规则

    • 简单拷贝

      • 规则描述:直接将原变量的值拷贝一份给新变量,两个变量互不影响
      • 类似于 JavaScript 中的原始值拷贝
      • 这个规则仅针对保存在栈中的数据,即这些值的类似是基本数据类型
        • 整型
        • 浮点型
        • 布尔型
        • 字符型
        • 以及 仅包含以上数据类型的元组 (数组都不行,只能是元组)
      • 特殊情况:引用的传递规则也是简单拷贝
    • 直接移动

      • 规则描述:直接将原变量持有的引用传递给新变量,原变量不可继续使用,仅能使用新变量

      • 这个规则针对的是保存在堆中的数据,即这些值的类型不是上一条规则描述到的基本数据类型

        let hello = String::from("Hello, world!");
        let world = hello;
        println!("{}, {}", hello, world); // × borrow of moved value: `hello`。即 `hello` 变量已失效
        
        // 特例
        let s1 = "hello world";
        let s2 = s1;
        println!("{}, {}", hello, world); // √ s1 和 s2 拿到的都是字符串的引用,是 &str 类型
        
        
    • 堆数据克隆

      • “ Rust 会尽可能地降低程序的运行成本,所以默认情况下,长度较大的数据存放在堆中”

      • 如果期望将数据备份一份以供其他地方使用,就需要采用 克隆 的方式

      • 比如字符串:

        fn main() {
            let hello = String::from("Hello, world!");
            let world = hello.clone(); // 把 hello 进行一次克隆
            println!("{}, {}", hello, world); // hello 和 world 指向了堆里的两个独立的、一模一样的字符串对象
        }
        
  • 变量在作为函数参数时的传递规则

    • 基本数据类型

      • 传递规则与上文描述的 简单拷贝 规则一致
    • 非基本数据类型

      • 传递规则与上文描述的 直接移动 规则一致
    • 租借值/引用

      • 传递规则与上文描述的 简单拷贝 规则一致

3. 切片 (Slice)

  • 切片,即对引用类型的数据值的部分引用

    • 常用的是数组切片、字符串切片
  • 切片的特性

    • 字符串字面值就是 slice
    • 数组也可以利用类似的方式,来获取数组的切片
      • 我的理解:字符串是一种特殊的数组 (字符数组),字符串切片是一种特殊的数组切片 (类型为 &u8 的切片)
  • 示例代码

    // 来自官方教程的一个示例
    pub fn word_test() {
      let mut hello = String::from("hello world!");
      let hello_first = first_word(&hello);
      println!("first word: {}.", hello_first);
      hello.clear();
    }
    
    fn first_word(source: &str) -> &str {
      let charts = source.as_bytes();
      let iter_wrapper = charts.iter().enumerate();
    
      for (index, &item) in iter_wrapper {
        if item == b' ' {
          return &source[0..index];
        }
      }
    
      return &source;
    }