rust指针

发表于 · 归类于 代码 · 阅读完需 97 分钟 · 报告错误 · 阅读:

很多时候,指针是相对宽泛概念,并非特指原始指针(raw-pointer)。以 引用(reference) 为例,在经过编译器检查后,其身上所承担的职责已经卸去。起码在底层指令层面,和原始指针并无区别。所以说它是指针确实算不得错。

引用和原始指针是独立对象,间接(*p)操作目标。透过指针修改目标值时,无需声明自身为可变(mut),除非打算改变自己。

指针

 

fn main() {
    // 要改变的目标,必须是mut。
    let mut x = 100;

    // r: &mut i32,表明目标类型可变。
    let r = &mut x;

    *r+=1;

    // r: &i32, 不可变引用
    let r = &x;
    assert_eq!(*r, 101);
}

 

原始指针

原始指针分*mut T*const T两种,分别对应可变不可变无论读写都不安全,须自行负责。

fn main() {
    let mut x = 100;

    let r: *mut i32 = &mut x;

    unsafe {
        *r += 1;
        assert_eq!(*r, 101);
    }

    let r: *const i32 = &x;
    assert_eq!(unsafe{ *r }, 101);
}

 

引用和指针互相转换

可以和引用相互转换。

fn main() {
    let mut x = 100;
    let r = &mut x;

    // 引用转指针必然是安全的。
    let p = r as *mut i32;
    assert_eq!(unsafe{ *p }, x);

    // 指针转回引用就未必安全了。
    let r2: &mut i32 = unsafe{ &mut *p };  // unsafe{ & *p } -> &i32
    *r2 += 1;
    assert_eq!(x, 101);
}
(gdb) info locals
r2 = 0x7fffffffe2a4
p = 0x7fffffffe2a4
r = 0x7fffffffe2a4
x = 101

// 从底层实现来看,引用和指针没有区别
(gdb) x/xg 0x7fffffffe2a4
0x7fffffffe2a4:	0x0000000000000064

(gdb) x/xg 0x7fffffffe2a4
0x7fffffffe2a4:	0x0000000000000064

// *r2 += 1;
(gdb) x/wx 0x7fffffffe2a4
0x7fffffffe2a4:	0x00000065

 

usize指针转换

可通过usize转换,实现指针运算。也正因为如此,指向无法保证。

use std::mem::size_of_val;

fn main() {
    let mut x = [1, 2, 3, 4];
    let mut p: *mut i32 = &mut x[0]; // 指针自身和目标都可变
    
    unsafe {
        assert_eq!(*p, 1);           // 断言x[0]是否等于1

        p = ((p as usize) + size_of_val(&x[0])) as *mut i32; // p++
        *p += 10;
        assert_eq!(*p, 12);
    }
}

 

原始指针不参与对象生命周期,也就是说可能成为悬垂指针

#[derive(Debug)]
struct Data {
    x: i32,
}

impl Drop for Data {
    fn drop(&mut self) {
        println!("Dropping!");
    }
}

fn main() {
    let d = Data{ x: 10 };
    let p = &d as *const Data;

    {
        let _d2 = d;  // move!!!
    }                 // drop!!!

    unsafe {
        println!("{:p}, {:?}", p, *p);
    }
}
$  cargo r
   Compiling ddd v0.1.0 (/root/rs/ddd)
    Finished dev [unoptimized + debuginfo] target(s) in 3.04s
     Running `/root/rs/ddd/target/debug/ddd`
Dropping!
0x7ffdd34821e4, Data { x: 10 }      // 虽然能返回内容,但这只是因为是栈内存未被覆盖的缘故

 

通过以下反汇编结果,可以看出原始变量d在内层超出{ ... }作用域后,进行了所有权转移,调用 drop进行释放。main函数结束时,超出该作用域没有调用drop函数释放局部变量p,则p成为了悬垂指针

; /root/rs/ddd/src/main.rs:
  fn main() {
  0x0000555555559250 <+0>: sub    rsp,0xb8

  let d = Data{ x: 10 };
  0x0000555555559257 <+7>: mov    DWORD PTR [rsp+0x34],0xa

  let p = &d as *const Data;
  0x000055555555925f <+15>:  lea    rax,[rsp+0x34]
  0x0000555555559264 <+20>:  mov    QWORD PTR [rsp+0x38],rax

  {
    let _d2 = d;  // move!!!
  0x0000555555559269 <+25>:  mov    ecx,DWORD PTR [rsp+0x34]
  0x000055555555926d <+29>:  mov    DWORD PTR [rsp+0x44],ecx
  }              // drop!!!

  0x0000555555559271 <+33>:  lea    rdi,[rsp+0x44]
  0x0000555555559276 <+38>:  call   0x5555555591f0 <core::ptr::drop_in_place>

  ; ...
  
  0x0000555555559346 <+246>:  lea    rdi,[rsp+0x48]
  0x000055555555934b <+251>:  call   QWORD PTR [rip+0x44a1f]        # 0x55555559dd70

    }
  }
  
  0x0000555555559351 <+257>:  add    rsp,0xb8
  0x0000555555559358 <+264>:  ret
; End of assembler dump.

   

智能指针

在C++中广泛采用智能指针,主要是由于原始指针非常不安全,开发者在使用它们时需要注意很多细节。不恰当地使用它们,可能会以非常隐蔽的方式导致诸如内存泄漏、引用挂起,以及大型代码库中的双重释放等问题。

智能指针(smart-pointer)是一类数据结构,其行为类似指针,拥有额外的功能。与引用的差别在于,引用 借用(borrow) 目标,而智能指针则 拥有(own) 目标。

智能指针通常实现DerefDrop特征。Deref重载解引用运算符,让它的操作和引用一致;Drop则负责在离开作用域时清理资源。

Drop

Drop可以自动释放相关值超出作用域后占用的资源。它包含一个 drop 方法,当对象超出作用域时,就会被调用。该方法将 &mut self 作为参数。使用 drop 释放值是以LIFO的方式进行的。也就是说,无论最后构建的是什么,都首先会被释放。

struct Character {
    name: String,
}

impl Drop for Character {
    fn drop(&mut self) {
        println!("{} went away", self.name);
    }
}

fn main() {
    let _steve = Character {
        name: "Steve".into(),
    };

    let _john = Character {
        name: "John".into(),
    };
}
root@8d75790f92f5:~/rs/ddd/src# cargo r
   Compiling ddd v0.1.0 (/root/rs/ddd)
    Finished dev [unoptimized + debuginfo] target(s) in 3.93s
     Running `/root/rs/ddd/target/debug/ddd`

John went away
Steve went away
(gdb) disassemble
Dump of assembler code for function ddd::main:
   0x0000555555559a10 <+0>:	  sub    rsp,0x78
   0x0000555555559a14 <+4>:	  lea    rax,[rip+0x34640]        # 0x55555558e05b
   0x0000555555559a1b <+11>:	lea    rdi,[rsp+0x18]
   0x0000555555559a20 <+16>:	mov    rsi,rax
   0x0000555555559a23 <+19>:	mov    edx,0x5
   0x0000555555559a28 <+24>:	call   0x55555555a610 <<T as core::convert::Into<U>>::into>
   0x0000555555559a2d <+29>:	mov    rax,QWORD PTR [rsp+0x28]
   0x0000555555559a32 <+34>:	mov    QWORD PTR [rsp+0x10],rax
   0x0000555555559a37 <+39>:	movups xmm0,XMMWORD PTR [rsp+0x18]
   0x0000555555559a3c <+44>:	movaps XMMWORD PTR [rsp],xmm0
   0x0000555555559a40 <+48>:	lea    rsi,[rip+0x34619]        # 0x55555558e060
   0x0000555555559a47 <+55>:	lea    rdi,[rsp+0x50]
   0x0000555555559a4c <+60>:	mov    edx,0x4
   0x0000555555559a51 <+65>:	call   0x55555555a610 <<T as core::convert::Into<U>>::into>
   0x0000555555559a56 <+70>:	jmp    0x555555559a58 <ddd::main+72>
   0x0000555555559a58 <+72>:	mov    rax,QWORD PTR [rsp+0x60]
   0x0000555555559a5d <+77>:	mov    QWORD PTR [rsp+0x40],rax
   0x0000555555559a62 <+82>:	movups xmm0,XMMWORD PTR [rsp+0x50]
   0x0000555555559a67 <+87>:	movaps XMMWORD PTR [rsp+0x30],xmm0
   0x0000555555559a6c <+92>:	lea    rdi,[rsp+0x30]
=> 0x0000555555559a71 <+97>:	call   0x555555559370 <core::ptr::drop_in_place>
   0x0000555555559a76 <+102>:	jmp    0x555555559a78 <ddd::main+104>
   0x0000555555559a78 <+104>:	mov    rdi,rsp
   0x0000555555559a7b <+107>:	call   0x555555559370 <core::ptr::drop_in_place>
   0x0000555555559a80 <+112>:	add    rsp,0x78
   0x0000555555559a84 <+116>:	ret
   0x0000555555559a85 <+117>:	mov    rdi,rsp
   0x0000555555559a88 <+120>:	call   0x555555559370 <core::ptr::drop_in_place>
   0x0000555555559a8d <+125>:	mov    rdi,QWORD PTR [rsp+0x68]
   0x0000555555559a92 <+130>:	call   0x555555559050 <_Unwind_Resume@plt>
   0x0000555555559a97 <+135>:	ud2
   0x0000555555559a99 <+137>:	mov    QWORD PTR [rsp+0x68],rax
   0x0000555555559a9e <+142>:	mov    DWORD PTR [rsp+0x70],edx
   0x0000555555559aa2 <+146>:	jmp    0x555555559a85 <ddd::main+117>
End of assembler dump.

(gdb) x/xg $rdi
0x7fffffffe3c0: 0x00005555555a09f0

(gdb) x/4bx 0x00005555555a09f0
0x5555555a09f0: 0x4a  0x6f  0x68  0x6e    ; John

通过反汇编结果,可以看出首先释放的是 john 变量。

 

Deref 和 DerefMut

为了能够像普通指针一样能够解引用被指向类型的调用方法,智能指针类型通常实现 Deref 特征,这允许用户对这些类型使用 解引用 运算符*。虽然 Deref 只为你提供了只读权限,但是还有 DerefMut,它可以为你提供对底层类型的可变引用。

Deref具有以下类型签名:

// library/core/src/ops/deref.rs

pub trait Deref {
  type Target: ?Sized;
  
  fn deref(&self) -> &Self::Target;
}

 

它定义了一个名为 deref 的方法,并通过引用获取 self 参数,然后返回对底层类型的不可变引用。与Rust的 deref 强制性特征结合,能够大幅减少开发编写代码的工作量。deref 强制性特征是指类型自动从一种类型的引用转换成另一种类型的其它引用。

use std::ops::Deref;

struct DerefExample<T> {
    value: T
}

impl<T> Deref for DerefExample<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

fn main() {
    let x = DerefExample { value: 'a' };
    assert_eq!('a', *x);
}

 

智能指针分类

 

Box

在堆(heap)上为值分配内存,栈上只是一个指向该内存的指针。


   stack              heap
  +=====+           +=======+
  | ptr | --------> | value |    Box<T>
  +=====+           +=======+

 

和引用一致的使用方式,没有特殊功能。超出作用域自动释放。

fn main() {
  // 在堆上分配 sizeof(i32) 大小内存,并存储值 5。
  let mut p = Box::new(5);
    
  *p += 1;
  let x: i32 = *p;
    
  assert_eq!(x, 6);
}
0x0000555555559452 <+18>:	mov    eax,0x4
0x0000555555559457 <+23>:	mov    rdi,rax
0x000055555555945a <+26>:	mov    rsi,rax
0x000055555555945d <+29>:	call   0x555555559730 <alloc::alloc::exchange_malloc>
0x0000555555559462 <+34>:	mov    rcx,rax
0x0000555555559465 <+37>:	mov    DWORD PTR [rax],0x5

; let mut p = Box::new(5);
0x000055555555946b <+43>:	mov    QWORD PTR [rsp+0x60],rcx     // p(stack) -> heap -> 0x00000005

(gdb) info locals
x = 6
p = 0x55555559c9d0

(gdb) x/xw 0x55555559c9d0                                       // 在heap上为值分配内存
0x55555559c9d0:	0x00000006

(gdb) info proc mappings
0x55555559c000     0x5555555bd000    0x21000        0x0 [heap]  // heap内存范围

(gdb) disass                                                    // 超出作用域,所有权自动释放
0x0000555555559630 <+496>:	lea    rdi,[rsp+0x60]
0x0000555555559635 <+501>:	call   0x555555559230 <core::ptr::drop_in_place>
0x000055555555963a <+506>:	mov    rdi,QWORD PTR [rsp+0xf0]

Box创建数组类型

fn main() {
    // 以数组实现类 calloc 功能。
    let mut p = Box::new([0i64; 10]);

    p[1] = 10;
    p[2] = 20;

    println!("{:?} {:?}", p[2], p);
}
(gdb) info locals                                               // 数组值被分配在堆上
p = 0x5555555a09d0

(gdb) x/10xg 0x5555555a09d0                                     // heap上值内容
0x5555555a09d0:	0x0000000000000000	0x000000000000000a
0x5555555a09e0:	0x0000000000000014	0x0000000000000000
0x5555555a09f0:	0x0000000000000000	0x0000000000000000
0x5555555a0a00:	0x0000000000000000	0x0000000000000000
0x5555555a0a10:	0x0000000000000000	0x0000000000000000

Box类型所有权

Box类型的所有权语义取决于包装类型。如果基础类型为 Copy,那么 Box实例将成为副本,否则默认情况下发生所有权转移

fn box_ref<T>(b: T) -> Box<T> {
  // 实例副本
  let a = b;
  Box::new(a)
}

struct Foo(i32);

fn main() {
  let boxed_one = Box::new(Foo(0x64));
  let unboxed_one = *boxed_one;
  box_ref(unboxed_one);
}
; 进程内存地址映射表
(gdb) info proc  mappings
process 1883
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x555555554000     0x555555559000     0x5000        0x0 /root/rs/ddd/target/debug/ddd
      0x555555559000     0x55555558b000    0x32000     0x5000 /root/rs/ddd/target/debug/ddd
      0x55555558b000     0x555555598000     0xd000    0x37000 /root/rs/ddd/target/debug/ddd
      0x555555598000     0x55555559b000     0x3000    0x43000 /root/rs/ddd/target/debug/ddd
      0x55555559b000     0x55555559c000     0x1000    0x46000 /root/rs/ddd/target/debug/ddd
      0x55555559c000     0x5555555bd000    0x21000        0x0 [heap]

      ...

      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]      

(gdb) info locals
boxed_one = 0x55555559c9d0

; 变量boxed_one指向heap地址: 0x55555559c9d0位置,内容为0x64
(gdb) x/xg 0x55555559c9d0
0x55555559c9d0:	0x0000000000000064

; let unboxed_one = *boxed_one 解引用,获取Foo struct数据
(gdb) info locals
unboxed_one = ddd::Foo (
  100
)

; fn box_ref<T>(b: T) -> Box<T>
(gdb) disassemble
Dump of assembler code for function ddd::box_ref:
=> 0x0000555555559ad0 <+0>:	sub    rsp,0x18
   0x0000555555559ad4 <+4>:	mov    DWORD PTR [rsp+0xc],edi
   0x0000555555559ad8 <+8>:	mov    DWORD PTR [rsp+0x10],edi
   0x0000555555559adc <+12>:	mov    DWORD PTR [rsp+0x14],edi
   0x0000555555559ae0 <+16>:	mov    eax,0x4
   0x0000555555559ae5 <+21>:	mov    DWORD PTR [rsp+0x8],edi
   0x0000555555559ae9 <+25>:	mov    rdi,rax
   0x0000555555559aec <+28>:	mov    rsi,rax
   0x0000555555559aef <+31>:	call   0x555555559660 <alloc::alloc::exchange_malloc>
   0x0000555555559af4 <+36>:	mov    rcx,rax
   0x0000555555559af7 <+39>:	mov    edx,DWORD PTR [rsp+0x8]
   0x0000555555559afb <+43>:	mov    DWORD PTR [rax],edx
   0x0000555555559afd <+45>:	mov    QWORD PTR [rsp],rcx
   0x0000555555559b01 <+49>:	mov    rax,QWORD PTR [rsp]
   0x0000555555559b05 <+53>:	add    rsp,0x18
   0x0000555555559b09 <+57>:	ret
End of assembler dump.

; main函数调用box_ref,通过$rdi寄存器进行传递
; 参数按值传递
(gdb) p/x $edi
$2 = 0x64

; Box::new(a) 返回堆上地址,该内存空间存储具体数据
(gdb) p/x $rcx
$4 = 0x55555559c9f0

(gdb) x/xg 0x55555559c9f0
0x55555559c9f0:	0x0000000000000064

; box_ref返回(rax 堆上地址) 
0x0000555555559b01 <+49>:	mov    rax,QWORD PTR [rsp]
0x0000555555559b05 <+53>:	add    rsp,0x18
0x0000555555559b09 <+57>:	ret

; 所有权释放
0x0000555555559313 <+67>:	call   0x555555559ad0 <ddd::box_ref>
0x0000555555559318 <+72>:	mov    QWORD PTR [rsp],rax
0x000055555555931c <+76>:	jmp    0x55555555931e <ddd::main+78>
0x000055555555931e <+78>:	mov    rax,QWORD PTR [rsp]
0x0000555555559322 <+82>:	mov    QWORD PTR [rsp+0x20],rax
0x0000555555559327 <+87>:	lea    rdi,[rsp+0x20]
0x000055555555932c <+92>:	call   0x5555555591e0 <core::ptr::drop_in_place>
0x0000555555559331 <+97>:	jmp    0x555555559333 <ddd::main+99>

; 释放heap 内存
0x0000555555559333 <+99>:	mov    rdi,QWORD PTR [rsp+0x10]
0x0000555555559338 <+104>:	call   0x555555559d10 <alloc::alloc::box_free>
0x000055555555933d <+109>:	add    rsp,0x48
0x0000555555559341 <+113>:	ret

(gdb) x/xg $rsp+0x10
0x7fffffffe3d0:	0x000055555559c9d0

(gdb) x/xg 0x000055555559c9d0
0x55555559c9d0:	0x0000000000000064

 

递归类型定义

Box 类型适用于创建递归类型定义。

struct Node {
  data: u32,
  next: Option<Node>
}

fn main() {
  let a = Node { data: 33, next: None };
}

 

在编译上述代码时,我们得到一下错误提示:

root@8d75790f92f5:~/rs/ddd/src# cargo c
    Checking ddd v0.1.0 (/root/rs/ddd)
error[E0308]: mismatched types
  --> src/main.rs:12:36
   |
12 |     let a = Node { data: 33, next: None };
   |                                    ^^^^ expected struct `Box`, found enum `Option`
   |
   = note: expected struct `Box<Node>`
                found enum `Option<_>`

由于每个数据片段在编译时都需要确定静态的已知尺寸,因此在 Rust 中这是一种不可表示的类型。我们需要让 next 字段具有固定大小,可以通过 next 放在指针后面来实现,因为指针总是具有固定大小。可以使用 Box 类型修改 Node结构体的定义:

struct Node {
  data: u32,
  next: Option<Box<Node>>
}

当定义需要隐藏在不定长结构后面的递归类型时,也可以使用 Box 类型。

引用计数的智能指针

引用计数类型支持某个粒度级别的垃圾回收。在这种方法中,智能指针类型允许用户对包装值进行多次引用。在内部,智能指针使用引用计数器(这里是refcount)来统计自发放的并且活动的引用数量,不过它只是一个整数值。当引用包装的智能指针值的变量超出作用域时,refcount的值就会递减。一旦该对象的所有权引用都消失,refcount 的值也会变成0, 之后该值被销毁。这就是引用计数指针常见的工作模式。

Rust提供了两种引用计数指针类型。

Rc

引用计数(reference counting)启用多所有权。当引用数为 0 时,进行清理。

RC底层数据结构:


   stack              heap
  +=====+           +============+
  | ptr | --------> | strong_cnt |    Rc<T>
  +=====+           +------------+
                    | weak_cnt   |
                    +------------+
                    | value      |
                    +============+
// library/alloc/src/rc.rs
struct RcBox<T: ?Sized> {
    strong: Cell<usize>,            // 强引用计数
    weak: Cell<usize>,              // 弱引用计数
    value: T,                       // 值
}

当我们与一个 Rc类型交互时,其内部会发生如下变化。

Rc<T> 主要通过两种方式使用:

use std::rc::Rc;

fn main() {
    let rc = Rc::new(5);
    assert_eq!(Rc::strong_count(&rc), 1);

    {
        // 增加引用,计数 +1
        let rc2 = rc.clone();
        assert_eq!(Rc::strong_count(&rc), 2);

        // 解引用
        assert_eq!(*rc2, 5);
    }   // 超出作用域,计数 -1

    assert_eq!(Rc::strong_count(&rc), 1);
}
// heap上存储的是RcBox 数据结构
(gdb) info locals
rc = alloc::rc::Rc<i32> {
  ptr: core::ptr::non_null::NonNull<alloc::rc::RcBox<i32>> {
    pointer: 0x55555559d9d0
  },
  phantom: core::marker::PhantomData<alloc::rc::RcBox<i32>>
}

(gdb) x/3xg 0x55555559d9d0
0x55555559d9d0:	0x0000000000000001	0x0000000000000001
0x55555559d9e0:	0x0000000000000005

// Rc::new执行后,rc引用计数为1
(gdb) x/xg 0x55555559f9d0
0x55555559f9d0:	0x0000000000000001

// rc.clone执行后,rc引用计数自加1, 即2
(gdb) c
Continuing.
Breakpoint 2, ddd::main () at /root/rs/ddd/src/main.rs:13
13	        assert_eq!(Rc::strong_count(&rc), 2);

(gdb) x/xg 0x55555559f9d0
0x55555559f9d0:	0x0000000000000002

// 离开rc2作用域,计数器自减1,即1
(gdb) c
Continuing.
Breakpoint 3, ddd::main () at /root/rs/ddd/src/main.rs:18
18	    assert_eq!(Rc::strong_count(&rc), 1);

(gdb) x/xg 0x55555559f9d0
0x55555559f9d0:	0x0000000000000001

 

Rc 内部会保留两种引用: 强引用(Rc<T>)弱引用(Weak<T>)。二者都会维护每种类型的引用数量的计数,但是仅在强引用计数值为零时,才会释放该值。这样做的目的是数据结构的实现可能需要多次指向同一事物。

 

弱引用

use std::rc::{Rc, Weak};

fn main() {
    let wr: Weak<i32>;

    {
        let rc = Rc::new(5);

        // 降级, 生成弱引用
        wr = Rc::downgrade(&rc);

        // 不影响强引用计数
        assert_eq!(Rc::strong_count(&rc), 1);
        assert_eq!(Rc::weak_count(&rc), 1);

        // 弱引用不能保证目标值存活,所以不能直接解引用。
        // 升级成强引用再操作。如已释放,返回None。
        if let Some(rc2) = wr.upgrade() {
            assert_eq!(*rc2, 5);
        } else {
            panic!("upgrade: None");
        }
    }   // Rc drop!!!

    assert_eq!(wr.upgrade(), None);
}
(gdb) info locals
rc = alloc::rc::Rc<i32> {
  ptr: core::ptr::non_null::NonNull<alloc::rc::RcBox<i32>> {
    pointer: 0x5555555a09d0
  },
  phantom: core::marker::PhantomData<alloc::rc::RcBox<i32>>
}

; let rc = Rc::new(5) 引用计数置为1
(gdb) x/xg 0x5555555a09d0
0x5555555a09d0:	0x0000000000000001

; wr = Rc::downgrade(&rc), rc降级为弱引用
; 引用计数不受影响
(gdb) c
Continuing.
Breakpoint 2, ddd::main () at /root/rs/ddd/src/main.rs:15
15	        assert_eq!(Rc::strong_count(&rc), 1);

; 弱引用计数器为1
rc = Rc(strong=1, weak=1) = {
  value = 5,
  strong = 1,
  weak = 1
}

(gdb) x/xg 0x5555555a09d0
0x5555555a09d0:	0x0000000000000001

; wr.upgrade(), wr升级为强引用
; 引用计数自增1
(gdb) c
Continuing.
Breakpoint 3, ddd::main () at /root/rs/ddd/src/main.rs:19
19	            assert_eq!(*rc2, 5);

(gdb) x/xg 0x5555555a09d0
0x5555555a09d0:	0x0000000000000002

; 超出作用域, 引用计数置为0
(gdb) c
Continuing.
Breakpoint 4, ddd::main () at /root/rs/ddd/src/main.rs:25
25	    assert_eq!(wr.upgrade(), None);

(gdb) x/xg 0x5555555a09d0
0x5555555a09d0:	0x0000000000000000

gdb反汇编调试尽可能使用rust-gdb,而不要使用gdb。因为,rust内部封装结构是否构成弱引用必然在实际内存数据上有所体现,但未必暴露给我们这些外部用户。所以gdb调试结果和rust-gdb调试结果有所出入。

 

循环引用问题

可以使用弱引用打破引用循环。链表可以通过将引用计数分别指向下一个元素和上一个元素的方式维护链接。更好的方法是 对一个方向使用强引用,而对另一个方向使用弱引用

单链表实例
use std::rc::Rc;

#[derive(Debug)]
struct LinkedList<T> {
    head: Option<Rc<Node<T>>>
}

#[derive(Debug)]
struct Node<T> {
    next: Option<Rc<Node<T>>>,
    data: T
}

impl<T> LinkedList<T> {
    fn new() -> Self {
        LinkedList { head: None }
    }

    fn append(&self, data: T) -> Self {
        LinkedList {
            head: Some(Rc::new(Node {
                data: data,
                next: self.head.clone()
            }))
        }
    }
}

fn main() {
    let list_of_nums = LinkedList::new().append(1).append(2);
    println!("numbs: {:?}", list_of_nums);

    let list_of_strs = LinkedList::new().append("foo").append("bar");
    println!("strs: {:?}", list_of_strs);
}

 

反汇编单向链表:

; list_of_nums
(gdb) info locals
list_of_nums = ddd::LinkedList<i32> {
  head: core::option::Option<alloc::rc::Rc<ddd::Node<i32>>>::Some(alloc::rc::Rc<ddd::Node<i32>> {
      ptr: core::ptr::non_null::NonNull<alloc::rc::RcBox<ddd::Node<i32>>> {
        pointer: 0x5555555a29d0
      },
      phantom: core::marker::PhantomData<alloc::rc::RcBox<ddd::Node<i32>>>
    })
}
(gdb) x/4xg 0x5555555a29d0
0x5555555a29d0:	0x0000000000000001	0x0000000000000001
0x5555555a29e0:	0x00005555555a2910	0x0000000000000002    ; 0x00005555555a2910 下一个链表节点地址 

(gdb) x/4xg 0x00005555555a2910
0x5555555a2910:	0x0000000000000001	0x0000000000000001
0x5555555a2920:	0x0000000000000000	0x0000000000000001

; list_of_strs
(gdb) info locals
list_of_strs = ddd::LinkedList<&str> {
  head: core::option::Option<alloc::rc::Rc<ddd::Node<&str>>>::Some(alloc::rc::Rc<ddd::Node<&str>> {
      ptr: core::ptr::non_null::NonNull<alloc::rc::RcBox<ddd::Node<&str>>> {
        pointer: 0x5555555a2aa0
      },
      phantom: core::marker::PhantomData<alloc::rc::RcBox<ddd::Node<&str>>>
    })
}

(gdb) x/4xg 0x5555555a2aa0
0x5555555a2aa0:	0x0000000000000001	0x0000000000000001
0x5555555a2ab0:	0x00005555555a2a70	0x000055555558f00b

(gdb) x/3xb 0x000055555558f00b
0x55555558f00b:	0x62	0x61	0x72                        ; bar

(gdb) x/4xg 0x00005555555a2a70
0x5555555a2a70:	0x0000000000000001	0x0000000000000001
0x5555555a2a80:	0x0000000000000000	0x000055555558f008

(gdb) x/3xb 0x000055555558f008
0x55555558f008:	0x66	0x6f	0x6f                        ; foo

 

双向链表实例

可以使用 downgrade 方法将一个 Rc<T> 类型转换成一个 Weak<T>类型。类似地,可以使用 upgrade 方法将一个 Weak<T> 类型转换成一个 R<T> 类型。downgrade 方法始终有效,而在弱引用上调用 upgrade 方法时,实际的值可能已经被删除,在这种情况下,你将获得的值是None。 所以,添加一个只想上一个节点的弱指针。

use std::rc::Rc;
use std::rc::Weak;
use std::cell::RefCell;

#[derive(Debug)]
struct LinkedList<T> {
    head: Option<Rc<LinkedListNode<T>>>
}

#[derive(Debug)]
struct LinkedListNode<T> {
    next: Option<Rc<LinkedListNode<T>>>,
    prev: RefCell<Option<Weak<LinkedListNode<T>>>>,
    data: T
}

impl<T> LinkedList<T> {
    fn new() -> Self {
        LinkedList { head: None }
    }

    fn append(&mut self, data: T) -> Self {
        let new_node = Rc::new(LinkedListNode {
            data: data,
            next: self.head.clone(),
            prev: RefCell::new(None)
        });

        match self.head.clone() {
            Some(node) => {
                let mut prev = node.prev.borrow_mut();
                *prev = Some(Rc::downgrade(&new_node));
            },
            None => {

            }
        }

        LinkedList {
            head: Some(new_node)
        }
    }
}

fn main() {
    let list_of_nums = LinkedList::new().append(1).append(2).append(3);
    println!("nums: {:?}", list_of_nums);
}

双向链表结果:

list_of_nums = ddd::LinkedList<i32> {
  head: core::option::Option<alloc::rc::Rc<ddd::LinkedListNode<i32>>>::Some(Rc(strong=1, weak=1) = {
      value = ddd::LinkedListNode<i32> {
        next: core::option::Option<alloc::rc::Rc<ddd::LinkedListNode<i32>>>::Some(Rc(strong=1, weak=1) = {
            value = ddd::LinkedListNode<i32> {
              next: core::option::Option<alloc::rc::Rc<ddd::LinkedListNode<i32>>>::Some(Rc(strong=1, weak=0) = {
                  value = ddd::LinkedListNode<i32> {
                    next: core::option::Option<alloc::rc::Rc<ddd::LinkedListNode<i32>>>::None,
                    prev: RefCell(borrow=0) = {
                      value = core::option::Option<alloc::rc::Weak<ddd::LinkedListNode<i32>>>::Some(alloc::rc::Weak<ddd::LinkedListNode<i32>> {
                          ptr: core::ptr::non_null::NonNull<alloc::rc::RcBox<ddd::LinkedListNode<i32>>> {
                            pointer: 0x5555555a3a10
                          }
                        }),
                      borrow = 0
                    },
                    data: 1
                  },
                  strong = 1,
                  weak = 0
                }),
              prev: RefCell(borrow=0) = {
                value = core::option::Option<alloc::rc::Weak<ddd::LinkedListNode<i32>>>::Some(alloc::rc::Weak<ddd::LinkedListNode<i32>> {
                    ptr: core::ptr::non_null::NonNull<alloc::rc::RcBox<ddd::LinkedListNode<i32>>> {
                      pointer: 0x5555555a3a50
                    }
                  }),
                borrow = 0
              },
              data: 2
            },
            strong = 1,
            weak = 1
          }),
        prev: RefCell(borrow=0) = {
          value = core::option::Option<alloc::rc::Weak<ddd::LinkedListNode<i32>>>::None,
          borrow = 0
        },
        data: 3
      },
      strong = 1,
      weak = 1
    })
}

 

只让结构体的某个部分可变,可以通过RefCell进行修改:

// 1. 添加RefCell引用
use std::cell::RefCell;

// 2. 将RefCell添加到LinkedListNode
#[derive(Debug)]
struct LinkedListNode<T> {
  next: Option<Rc<LinkedListNode<T>>>,
  prev: RefCell<Option<Weak<LinkedListNode<T>>>>,
  data: T
}

// 3. 修改append方法以创建新的RefCell,并通过RefCell可变借用更新之前的引用
fn append(&mut self, data: T) -> Self {
  let new_node = Rc::new(LinkedListNode{
    data: data,
    next: self.head.Clone(),
    prev: RefCell::new(None)
  });

  match self.head.Clone() {
    Some(node) => {
      let mut prev = node.prev.borrow_mut();    // 借用
      *prev = Some(Rc::downgrade(&new_node));
    },
    None => {
    }
  }

  LinkedList {
    head: Some(new_node)
  }
}

 

自动解引用

自动解引用,可直接调用值方法。

use std::rc::Rc;

struct User {
    age: u8
}

impl User {
    fn test(&self) {
        println!("{}", self.age);
    }
}

fn main() {
    let rc = Rc::new(User{ age: 10 });
    rc.test();
}

 

内部可变性

Rust通过在任何给定作用域中仅允许一个可变引用,从而在编译时保证我们免受指针别名的影响。但是,在某些情况下,它会变得非常严格,因为严格的借用检查使我们知道由于代码的安全性而不能通过编译器的编译。对于这种情况,其中一个解决方案是将借用检查从编译时移动到运行时,这是通过内部可变性实现的。

 

内部可变性(interior mutability)利用unsafe绕过借用规则,允许通过不可变引用修改内部值。

编译器静态规则检查过于保守,某些设计需要运行期操作。

 

在运行时不存在两个可变借用。这些类型将多个可变借用的检测从编译时移动到了运行时,如果存在对值的两个可变借用,就会发生异常。当希望向用户公开不可变API时,通常会遇到内部可变性,不过上述API内部存在部分可变性。标准库中有两个通用的智能指针类型提供了共享可变性: CellRefCell

 

Cell

Cell<T>类型是一种智能指针类型,可以为值提供可变性,甚至允许值位于不可引用之后。它以极低的开销提供了此功能,并具有最简单的API。

 

Cell优势?

便捷版本Cell,开辟一块儿始终可变内存区域。

use std::cell::Cell;

struct Data {
    x: i64,
    y: Cell<i64>,
}

fn main() {
    let d = Data{ x: 1, y: Cell::new(2) };

    // d.x = 10;
    // ^^^^^^^^ cannot assign

    d.y.set(200);
    assert_eq!(d.y.get(), 200);
}
(gdb) ptype d.y                         // Cell数据类型
type = struct core::cell::Cell<i64> {
  value: core::cell::UnsafeCell<i64>,
}

(gdb) info locals
d = ddd::Data {
  x: 1,
  y: core::cell::Cell<i64> {
    value: core::cell::UnsafeCell<i64> {
      value: 2
    }
  }
}

(gdb) p/x &d                            // 分配在stack
$1 = 0x7fffffffe350

(gdb) x/2xg 0x7fffffffe350
0x7fffffffe350:	0x0000000000000001	0x0000000000000002

(gdb) info proc  mappings
0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
// d.y.set(200) 实现

0x0000555555559585 <+53>:	mov    rdi,rcx
0x0000555555559588 <+56>:	mov    esi,0xc8
0x000055555555958d <+61>:	call   0x5555555591b0 <core::cell::Cell<T>::set>

(gdb) p/x $rsp+0x40
$1 = 0x7fffffffe350

(gdb) x/2xg  0x7fffffffe350
0x7fffffffe350:	0x0000000000000001	0x00000000000000c8          // 0xc8 被设置为新值
// refCell.set() 函数源码剖析
pub fn set(&self, val: T) {
    let old = self.replace(val);    // 设置新值
    drop(old);                      // 删除旧值
}

 

Cell 常用方法

以下实例将两个可变引用修改bag中的内容:

use std::cell::Cell;

#[derive(Debug)]
struct Bag {
    item: Box<u32>
}

fn main() {
    let mut bag = Cell::new(Bag { item: Box::new(1) });
    let hand1 = &mut bag;
    let hand2 = &mut bag;

    *hand1 = Cell::new(Bag { item: Box::new(2) });

}

 

由于借用规则限制,不支持同时多次修改两个可变借用。

error[E0499]: cannot borrow `bag` as mutable more than once at a time
  --> src/main.rs:15:17
   |
14 |     let hand1 = &mut bag;
   |                 -------- first mutable borrow occurs here
15 |     let hand2 = &mut bag;
   |                 ^^^^^^^^ second mutable borrow occurs here
16 |
17 |     *hand1 = Cell::new(Bag { item: Box::new(2) });
   |     ------ first borrow later used here

error: aborting due to previous error

 

可以通过将bag的值封装到Cell中来让它正常运转。

use std::cell::Cell;

#[derive(Debug)]
struct Bag {
    item: Box<u32>
}

fn main() {
    let bag = Cell::new(Bag { item: Box::new(1) });
    let hand1 = &bag;
    let hand2 = &bag;

    hand1.set(Bag { item: Box::new(2) });
    hand2.set(Bag { item: Box::new(3) });
}

 

RefCell

如果需要某个非Copy类型支持Cell的功能,可以使用RefCell类型。

它采用了和借用类似的读/写模式,但是将借用检查移动到运行时,这很方便,但不是零成本。RefCell 分发值的引用不像 Cell类型那样按值返回。

RefCell 实例1: 基本使用

use std::cell::RefCell;

fn main() {
    let c = RefCell::new(5);

    {
        let r1 = c.borrow();            // &T
        let r2 = c.borrow();            // &T

        assert_eq!(*r1, 5);
        assert_eq!(*r1, *r2);
    }

    {
        let mut r = c.borrow_mut();     // &mut T
        *r = 10;
    }

    assert_eq!(c.into_inner(), 10);     // c 被释放
}
// RefCell 结构

(gdb) ptype c
type = struct core::cell::RefCell<i32> {
  borrow: core::cell::Cell<isize>,
  value: core::cell::UnsafeCell<i32>,
}

// 结构体示意图

   stack              heap
  +=====+           +============+
  | ptr | --------> | borrow_cnt |    RefCell<T>
  +=====+           +------------+    borrow_mut: count = -1
                    | value      |
                    +============+
                    
// RefCell::new(5) 初始化后,borrow计数为0

(gdb) info locals
c = core::cell::RefCell<i32> {
  borrow: core::cell::Cell<isize> {
    value: core::cell::UnsafeCell<isize> {
      value: 0
    }
  },
  value: core::cell::UnsafeCell<i32> {
    value: 5
  }
}

(gdb) x/2xg 0x7fffffffe1d0
0x7fffffffe1d0:	0x0000000000000000	0x0000000000000005


// borrow_mut调用borrow_cnt()后,将计数置为-1

c = core::cell::RefCell<i32> {
  borrow: core::cell::Cell<isize> {
    value: core::cell::UnsafeCell<isize> {
      value: -1
    }
  },
  value: core::cell::UnsafeCell<i32> {
    value: 5
  }
}

// 调用borrow_cnt后,结构体发生改变
// let mut r = c.borrow_mut();

(gdb) ptype r
type = struct core::cell::RefMut<i32> {
  value: *mut i32,
  borrow: core::cell::BorrowRefMut,
}

// let mut r = c.borrow_mut(); 后,r变量引用置为负数,可通过r进行修改r指向堆上的值
r = core::cell::RefMut<i32> {
  value: 0x7fffffffe1d8,
  borrow: core::cell::BorrowRefMut {
    borrow: 0x7fffffffe1d0
  }
}

(gdb) x/xg 0x7fffffffe1d8                       // 值
0x7fffffffe1d8:	0x0000000000000005

(gdb) x/xg 0x7fffffffe1d0                       // 一个负数补码
0x7fffffffe1d0:	0xffffffffffffffff

 

实例2: 改造Rc

改造Rc,使其可变。

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let rc = Rc::new(RefCell::new(5));
    let rc2 = rc.clone();

    {
        let mut r = rc.borrow_mut();
        *r = 100;
    }

    assert_eq!(*rc2.borrow(), 100);
}
(gdb) info locals
r = core::cell::RefMut<i32> {
  value: 0x55555559f928,
  borrow: core::cell::BorrowRefMut {
    borrow: 0x55555559f920
  }
}

rc2 = alloc::rc::Rc<core::cell::RefCell<i32>> {
  ptr: core::ptr::non_null::NonNull<alloc::rc::RcBox<core::cell::RefCell<i32>>> {
    pointer: 0x55555559f910
  },
  phantom: core::marker::PhantomData<alloc::rc::RcBox<core::cell::RefCell<i32>>>
}

rc = alloc::rc::Rc<core::cell::RefCell<i32>> {
  ptr: core::ptr::non_null::NonNull<alloc::rc::RcBox<core::cell::RefCell<i32>>> {
    pointer: 0x55555559f910
  },
  phantom: core::marker::PhantomData<alloc::rc::RcBox<core::cell::RefCell<i32>>>
}

// let mut r = rc.borrow_mut(), r borrow指向rc2(rc) RefCell.value地址
(gdb) x/4xg 0x55555559f910
0x55555559f910:	0x0000000000000002	0x0000000000000001
0x55555559f920:	0xffffffffffffffff	0x0000000000000005

// *r = 100, 更改r.borrow 指向heap内容,即rc2(rc).value位置的值
(gdb) x/4xg 0x55555559f910
0x55555559f910:	0x0000000000000002	0x0000000000000001
0x55555559f920:	0xffffffffffffffff	0x0000000000000064

 

实例3: borrow_mut 和 borrow使用实例

use std::cell::RefCell;

#[derive(Debug)]
struct Bag {
    item: Box<u32>
}

fn main() {
    let bag = RefCell::new(Bag { item: Box::new(1) });
    let hand1 = &bag;
    let hand2 = &bag;
    *hand1.borrow_mut() = Bag { item: Box::new(2) };
    *hand2.borrow_mut() = Bag { item: Box::new(3) };
    let borrowed = hand1.borrow();
    println!("{:?}", borrowed);
}

 

尝试在统一作用域中调用上述两种方法:

use std::cell::RefCell;

#[derive(Debug)]
struct Bag {
    item: Box<u32>
}

fn main() {
    let bag = RefCell::new(Bag { item: Box::new(1) });
    let hand1 = &bag;
    let hand2 = &bag;
    *hand1.borrow_mut() = Bag { item: Box::new(2) };
    *hand2.borrow_mut() = Bag { item: Box::new(3) };
    println!("{:?} {:?}", hand1.borrow(), hand1.borrow_mut());
}
root@8d75790f92f5:~/rs/ddd/src# cargo r
   Compiling ddd v0.1.0 (/root/rs/ddd)
    Finished dev [unoptimized + debuginfo] target(s) in 3.87s
     Running `/root/rs/ddd/target/debug/ddd`
thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:20:49
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

出现上述错误,是因为独占可变访问具有相同的所有权规则。但是,对于 RefCell,这会在运行时进行检查。对于这种情况,必须明确使用单独的代码块分隔借用,或者使用 drop 方法删除引用。

 

RefCell Vs. Cell

CellRefCell 更轻,性能更好,用法更方。但是Cell只能包装Copy类型,而RefCell可以包装任何类型,并且RefCell可以获取其内部包装对象的引用,并在运行时检测可变引用的唯一性。

具体数据结构:

// library/core/src/cell.rs

pub struct Cell<T: ?Sized> {
    value: UnsafeCell<T>,                                                       // 内部对象
}
// RefCell
type BorrowFlag = isize;

pub struct RefCell<T: ?Sized> {
    borrow: Cell<BorrowFlag>,                                                   // 对象引用类别和计数器
    borrowed_at: Cell<Option<&'static crate::panic::Location<'static>>>,        // 借用最开始&活跃的位置
    value: UnsafeCell<T>,                                                       // 内部对象
}
// &mut T
pub struct RefMut<'b, T: ?Sized + 'b> {
    value: &'b mut T,
    borrow: BorrowRefMut<'b>,
}

RefCell内部维护了一个包装对象的引用计数,当RefCell.borrow获取一个共享引用时,内部引用计数加1,当获取的引用离开作用域时,内部引用计数减1,当RefCell.borrow_mut获取一个可变引用时,首先检测引用计数是否为0,如果为0,正常返回,否则panic;其实RefCell.borrow也会做类似的检测,当已经获取了可变引用也是直接panic,当然为了避免panic,我们可以用RefCell.try_borrowRefCell.try_borrow_mut来获取一个Result类型。

因为CellRefCell两种类型都未实现Sync trait,所以这两种类型只能用于单线程中,不能跨线程操作,如果需要跨线程操作,就需要用到MutexRwLock了。

Cell 和 RefCell类型不是线程安全(thead-safety)的。这意味着 Rust 不允许用户在多线程环境中共享这些类型。

内部可变性的应用

使用两个整数和sum方法来扩展结构体,以缓存求和的结果,并返回缓存的值。

use std::cell::Cell;

struct Point {
    x: u8,
    y: u8,
    cached_sum: Cell<Option<u8>>
}

impl Point {
    fn sum(&self) -> u8 {
        match self.cached_sum.get() {
            Some(sum) => {
                println!("Got from cache: {}", sum);
                sum
            },
            None => {
                let new_sum = self.x + self.y;
                self.cached_sum.set(Some(new_sum));
                println!("Set cache: {}", new_sum);
                new_sum
            }
        }
    }
}

fn main() {
    let p = Point { x: 8, y: 9 , cached_sum: Cell::new(None) };
    println!("Summed result: {}", p.sum());
    println!("Summed result: {}", p.sum());
}