【Rust笔记】03-引用

03 - 引用

  • 所有型指针:

    • 当所有者被清除时,引用的资源也会随之清楚。
    • 包括 Box<T> 堆指针、StringVec 值内部的指针。
  • 非所有型指针:引用(reference)

    • 当所有者被清除时,对它所引用的资源的生命期没有影响。
    • 引用的生命期不能超过其引用的资源的生命期。
    • 引用的本质是内存地址。
    • 通过引用,可以访问值,又不会影响其所有权。
  • 借用:创建对某个值的引用。(将引用类型作为函数参数的行为叫做借用)

  • 引用的分类:

    • 共享引用(shared reference):属于&T 类型

      • 可以读取引用的值,但不能修改这个值。
      • 在编译时,执行多读(multiple readers)的检查规则。
      • 只要存在对某个值的共享引用,即便该值的所有者也不能修改它。
    • 可修改引用(mutable reference):属于&mut T类型

      • 可以读取和修改引用的值。
      • 在编译时,执行单写(single writer)的检查规则。
      • 在可修改引用的存续期间,连对应值的所有者都无法使用。
  • 重要原则:

    • 保持共享引用和可修改引用分开,时保障内存安全的基本前提。
    • 迭代对向量的共享引用,按照定义会产生对其元素的共享引用。
  • 对函数传递参数:

    • 传值(by value):以转移所有权的方式给函数传参。
    • 传引用(by reference):传给函数的是对值的引用。

3.1 - 引用作为值

3.1.1-Rust 引用与 C++ 引用

  • 在 Rust 中,引用是通过 & 操作符显示创建,解引用也要显示地使用 * 操作符;

    let x = 10;
    let r = &x;          // &x是对x的共享引用
    assert!(*r == 10);   // 显示地对r解引用
    
    • 要创建可修改引用,则使用 &mut 操作符;

      let mut y = 32;
      let m = &mut y;    // &mut y是y的课修改引用
      *m += 32;          // 显示地对m解引用,以便设置y的值
      assert!(*m == 64); // 读取y的新值,同样要显示解引用
      
    • . 操作符:

      • 会在必要时,对其左侧操作数进行隐式解引用。

        struct Anime {
          name: &'static str,
          bechdel_pass: bool
        };
        let aria = Anime {
          name: "Aria: The Animation",
          bechdel_pass: true
        };
        let anime_ref = &aria;
        assert_eq!(anime_ref.name, "Aria: The Animation");
        
        // 等价于上一行代码,只不过显示地解引用
        assert_eq!((*anime_ref).name, "Aria: The Animation");
        
      • 在调用需要时,还可以隐式解用对其左侧操作数的引用。

        let mut v = vec![1973, 1968];
        v.sort();           // 隐式借用了对v的可修改引用
        (&mut v).sort();    // 等价,不推荐
        
  • 对比 C++:

    • C++ 会隐式地在引用和左值(即引用内存位置的表达式)之间转换,这些转换会出现在任何需要用到它们的地方。
    • 而 Rust,则必须使用 &* 操作符来创建和追随引用;只有. 操作符例外,它会隐式地借用和解引用。

3.1.2 - 给引用赋值

  • 给 Rust 引用赋值,会导致它指向新值:

    let x = 10;
    let y = 20;
    let mut r = &x;
    
    // 引用r开始时指向x;如果b为true,它就会指向y
    if b {
      r = &y;
    }
    
    assert!(*r == 10 || *r == 20);
    
  • C++ 中,给引用赋值会将值存储在引用中。而且,没有办法将 C++ 引用指向初始值之外的其他值。

3.1.3 - 引用的引用

Rust 允许引用的引用:

struct Point {
  x: i32,
  y: i32
}
let point = Point {
  x: 100,
  y: 729
};

// 下面的引用类型可以忽略
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr; // 3层引用

assert_eq!(rrr.y, 729);

3.1.4 - 比较引用

  • 比较操作符:

    • . 操作符类似,可以 “看穿” 任意多个引用。

    • 左右两个操作符的类型必须相同。

    • 常用于泛型函数中。

      let x = 10;
      let y = 10;
      
      let rx = &x;
      let ry = &y;
      
      let rrx = &rx;
      let rry = &ry;
      
      assert!(rrx <= rry);   // false
      assert!(rrx == rry);   // true
      
  • std::ptr::eq 方法:

    • 比较引用的地址;

    • 即,比较两个引用是不是指向同一块内存。

      assert!(rx == ry);              // true
      assert!(std::ptr::eq(rx, ry));  // false,两个值在不同的内存地址
      

3.1.5 - 引用永远不为空

  • Rust 引用永远不为空:
    • 没有 NULL(C 语言)或 nullptr(C++ 语言);
    • 不能将整数转换为引用,因此也不能把 0 转换成引用。
  • Option<&T> 引用,可以实现一个要么是引用,要么什么也不是的类型。用以代替 C++ 中的 nullptr
    • None 为空指针。
    • Some(r) 表示为非零地址,r&T 类型的值。
    • 在使用 Option<&T> 之前,必须检查它是否为 None

3.1.6 - 借用对任意表达式的引用

  • Rust 允许借用对任何类型表达式的值的引用:

    fn factorial(n: usize) -> usize {
      // 创建一个匿名变量来保存表达式的值,然后生成一个指向该值的引用
      (1..n+1).fold(1, |a, b| a * b)    //没有分号结尾,表示作为返回值
    }
    let r = &factorial(6);
    assert_eq!(r + &1009, 1729);
    
  • Rust 永远不会让代码中出现悬空指针:

    • 如果有超出匿名变量生命期,且永远不会用到的引用存在,那么 Rust 会在编译时发现,并报告给程序员。
    • 此时,只需要把匿名变量的值保存到一个命名变量中,并在给它一个适当的生命期即可。

3.1.7 - 对切片和特型对象的引用

  • 胖指针(fat point):包含 “某个值的地址”,以及 “与使用该值相关的必要信息” 的,一个两个字长的值。
  • Rust 有两种胖指针:
    • 对切片的引用:包含切片地址,及其长度信息。
    • 特型对象(trait object):即对实现某种特型的一个值的引用。包含一个值的地址和指向与该值匹配的特型实现的指针。
  • 两种胖指针的对比:
    • 都不拥有自己指向的值。
    • 生命期都不能超出目标值。
    • 可以是可修改或共享的。

3.2 - 引用安全

3.2.1 - 借用局部变量

  • 不能借用对一个局部变量的引用,然后将其拿到该变量的作用域之外:

    // 使用一对{}表示作用域
    {
      let r;
      {
        let x = 1;
        r = &x;
      }
      assert_eq!(*r, 1);  // 错误,读取了局部变量x的值。
    }
    
  • 每个引用类型附加一个生命期(lifetime),表示可以安全使用引用的一个范围。

    • 生命期是 Rust 在编译时虚构的东西;
    • 生命期没有运行时表示:在运行时,引用就是一个地址,其生命期取决于自身的类型。
  • 重要原则:

    • 变量的生命期必须包含涵盖从它那里借来的引用的生命期。
    • 引用的生命期必须包含或涵盖保存它变量的生命期。即,引用的生命期小于等于变量的生命期。
    • 对于一个引用向量而言,(作为向量元素)的所有引用的生命期拥有这个向量的变量的生命期。
    let v = vec![1, 2, 3];
    let r = &v[1];
    

3.2.2 - 接收引用作为参数 —— 阐述函数签名与函数体的关系

  • static静态变量等价于全局变量:在程序启动时创建,并一直存活到程序终止时的值。

    • 所有静态变量都必须初始化;

    • 只能在 unsafe 块中操作可修改静态变量

      static mut STASH: &i32 = &128;
      fn f<'a>(p: &'a i32) {  // 这个函数接收一个具有任意给定生命期‘a的i32类型的引用。
        unsafe {
          STASH = p;
        }
      }
      
  • 生命期参数(lifetime), 上述代码中:

    • 生命期'a(读作 “撇 A”)是 f 的生命期参数;
    • <'a> 表示:“对于任意生命期'a“。
  • 'static静态生命期:形如 STASH 的生命期与程序的整个过程一样长,所以它保存的引用类型的生命期,也必须同样长。

    • 上述示例代码中的函数不能接受任意引用作为参数,但可以接受一个具有'static 生命期的引用;

      fn f(p: &'static i32) {STASH = p;}

    • 只有静态变量的引用才不会导致 STASH 成为悬空指针。

3.2.3 - 将引用作为参数传递 —— 阐述函数与调用者的关系

  • 一般情况下,只需要在定义函数时考虑生命期参数,而在使用它们时 Rust 会自动推断生命期。

  • 如下代码会编译失败,引用 &x 不能活得比 x 还长,而把它传给 f(),会强迫它活的跟'static 一样长:

    fn f(p: &'static i32) {
      to_do!();
    }
    let x = 10;
    f(&x); // 引用&x是对普通变量的引用,当传递到函数`f()`内时,变成了静态变量的引用,所以不允许
    

3.2.4 - 返回引用

  • 取得某个数据结构的引用,然后返回对该结构某一部分的引用。

    // 返回一个对切片中最小元素的引用
    // v至少包含一个元素
    fn smallest<'a>(v: &'a [i32]) -> &'a i32 {
      let mut s = &v[0];
      for r in &v[1..] {
        if *r < *s {
          s = r;
        }
      }
      s // 返回值为s
    }
    fn main() {
      let parabola = [9, 4, 1, 0, 1, 4, 9];
      let s = smallest(&parabola);
      assert_eq!(*s, 0);
    }
    
  • 如果一个函数接收一个引用作为参数,并返回一个引用,那么这两个引用必须具有相同的生命期。

  • 函数签名中的生命期,让 Rust 可以评估传递给函数的引用和函数返回的引用之间的关系,从而确保安全使用它们。

3.2.5 - 结构体包含引用

  • 当引用类型出现在另一个类型的定义中时,必须写出生命期参数:

    struct S<'a> {
      r: &'a i32
    }
    // 保存在r中的任何引用的生命期最好包含'a,而'a也必须比保存S的任何值都长寿。
    
  • 通过类型的生命期参数,可以知道该类型是否包含具有相应(非‘static)生命期的引用,以及那些引用的情况如何。

3.2.6 - 不同的生命期参数

放宽限制的缺点:过多的生命期参数会导致类型和函数签名复杂难度。

fn f<'a>(r: &'a i32, s: &'a i32) -> &'a i32 {  // 严格型,两个参数生命期都是'a
  r
}

fn f<'a, 'b>(r: &'a i32, s: &'b i32) -> &'a i32 { // 宽松型
  r
}

3.2.7 - 省略生命期参数

  • 如果函数不返回任何引用(或其他需要生命期参数的类型),那么永远不需要写出生命期参数。
  • 如果函数参数中只出现了一个生命期,那么 Rust 会假定返回值中的生命期都是该生命期。
  • 函数参数中有多个生命期,Rust 会要求程序员自主决定。
  • Rust 会假定 self 的生命期就是返回值需要的生命期。(Rust 的 self 相当于 C++、Java 或 JavaScript 中的 this,或者 Python 中的 self

3.3 - 共享与修改

  • 可以借用向量的可修改引用,也可以借用对其元素的共享引用,但这两个引用的生命期不能重叠。

    let mut x = 10;
    let r1 = &x;
    let r2 = &x;    // 可以,允许多次共享借用
    x += 10;        // 错误,不能给x赋值,因为它已经被借用了
    let m = &mut x; // 错误,不能借用x的可修改引用,因为它已经被借出了不可修改引用
    
    let mut y = 20;
    let m1 = &mut y;
    let m2 = &mut y; // 错误,不能借两次可修改引用
    let z = y;       // 错误,不能用y作为只读赋值,因为它已经借出了可修改引用。
    
  • 从共享引用借用共享引用时可以的:

    let mut w = (107, 109);
    let r = &w;
    let r0 = &r.0;      // 可以,共享引用可以再借用为共享应用
    let m1 = &mut r.1;  // 错误,共享引用不能再借用为可修改引用。
    
  • 从可修改引用再借用可修改引用也是可以的:

    let mut v = (136, 139);
    let m = &mut v;
    let m0 = &mut m.0;  // 可以,可修改引用可以再借用为可修改引用
    *m0 = 137let r1 = &m.1;      // 可以,可修改引用可以再借用为共享引用,且不与(可修改引用)m0重叠
    v.1;                // 错误,禁止通过其他路径访问。
    
  • 只有某个值在线程间既可修改又可共享时,数据争用才会发生。

3.4 - 引用与对象

  • Rust 实现了指针、所有权和数据流单向地通过整个系统。
  • Rust 很难用结构体实现完全的类的效果。
  • Rust 强迫程序员理解为什么自己的程序是线程安全的,甚至还会要求程序员在更高层面上进行一些架构设计。

详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第五章
链接地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

phial03

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值