Rust的智能指针

我爱海鲸 2024-03-07 23:31:37 rust学习

简介Box、Deref Trait、Drop Trait、Rc

介绍标准库中常见的智能指针

- Box<T>:在heap内存上分配值

-Rc<T>:启用多重所有权的引用计数类型

  • Ref<T>和RefMut<T>,通过RefCelk<T>访问:在运行时而不是编译时强制借用规则的类型

此外:

一内部可变模式( interior mutability pattern):不可变类型暴露出可修改其内部值的APl

一引用循环(reference cycles):它们如何泄露内存,以及如何防止其发生。

1、相关的概念

指针:一个变量在内存中包含的是一个地址(指向其它数据)

Rust中最常见的指针就是“引用”

引用:

―使用&

―借用它指向的值

一没有其余开销

—最常见的指针类型

智能指针

   智能指针是这样一些数据结构:

      行为和指针相似

      有额外的元数据和功能

引用计数(reference counting)智能指针类型

   通过记录所有者的数量,使一份数据被多个所有者同时持有

   并在没有任何所有者时自动清理数据

引用和智能指针的其它不同

引用:   只借用数据

智能指针:   很多时候都拥有它所指向的数据

智能指针的例子

String和Vec<T>

都拥有一片内存区域,且允许用户对其操作还拥有元数据(例如容量等)

提供额外的功能或保障((String 保障其数据是合法的UTF-8编码)

智能指针的实现

智能指针通常使用struct实现,并且实现了:

-Deref和 Drop这两个trait

Deref trait:允许智能指针struct的实例像引用一样使用

Drop trait:允许你自定义当智能指针实例走出作用域时的代码

2、使用Box<T>来指向Heap 上的数据

Box<T>

Box<T>是最简单的智能指针:

   允许你在 heap 上存储数据(而不是 stack)

   stack上是指向heap数据的指针

   没有性能开销

   没有其它额外功能

实现了Deref trait和 Drop trait

Box<T>的常用场景

在编译时,某类型的大小无法确定。但使用该类型时,上下文却需要知道它的确切大小。

当你有大量数据,想移交所有权,但需要确保在操作时数据不会被复制。

使用某个值时,你只关心它是否实现了特定的 trait,而不关心它的具体类型。

使用Box<T>在heap 上存储数据

fn main() {
    let b = Box::new(5);
    println!("b:{}",b);
}

使用Box赋能递归类型

在编译时,Rust需要知道一个类型所占的空间大小。

而递归类型的大小无法在编译时确定。

但Box类型的大小确定

在递归类型中使用Box就可解决上述问题。

函数式语言中的Cons List。

关于Cons List

Cons List是来自Lisp语言的一种数据结构。

Cons List里每个成员由两个元素组成。

当前项的值

下一个元素

Cons List里最后一个成员只包含一个Nil值,没有下一个元素。

Cons List并不是Rust的常用集合

通常情况下,Vec<T>是更好的选择

创建一个Cons List

use crate::List::{Cons,Nil};

fn main() {
    let l = Cons(1,Cons(2,Cons(3,Nil)));
}


enum List {
    Cons(i32,List),
    Nil
}

但是上面的代码会报错

这里会报出这个递归有无限的大小

Rust如何确定为枚举分配的空间大小

enum Message {
    Quit,
    Move{x:i32,y:i32},
    Write(String),
    ChangeColor(i32,i32,i32,i32)
}

它遍历枚举中的每一个变体从而找出枚举中占用最大空间的那一个变体

使用Box来获得确定大小的递归类型

Box<T>是一个指针,Rust知道它需要多少空间,因为:

指针的大小不会基于它指向的数据的大小变化而变化。

use crate::List::{Cons,Nil};

fn main() {
    let l = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}


enum List {
    Cons(i32,Box<List>),
    Nil
}

Box<T> :

   只提供了“间接”存储和 heap内存分配的功能

没有其它额外功能

没有性能开销

适用于需要“间接”存储的场景,例如Cons List

实现了Deref trait和 Drop trait

3、Deref Trait

实现Deref Trait使我们可以自定义解引用运算符*的行为。

通过实现Deref,智能指针可像常规引用一样来处理

解引用运算符

常规引用是一种指针

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

    assert_eq!(5,x);
    assert_eq!(5,*y);
}

把Box<T>当作引用

Box<T>可以代替上例中的引用

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5,x);
    assert_eq!(5,*y);
}

定义自己的智能指针

Box<T>被定义成拥有一个元素的tuple struct

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x:T) -> MyBox<T> {
        MyBox(x)
    }
}


fn main() {
    let x = 5;
    // let y = Box::new(x);
    let y = MyBox::new(x);

    assert_eq!(5,x);
    assert_eq!(5,*y);
}

上面的代码会报错,因为没有实现Deref Trait不能够进行解引用

实现Deref Trait

标准库中的 Deref trait要求我们实现一个deref方法:

   该方法借用self

   返回一个指向内部数据的引用

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x:T) -> MyBox<T> {
        MyBox(x)
    }
}

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

    fn deref(&self) -> &T {
        &self.0
    }
}


fn main() {
    let x = 5;
    // let y = Box::new(x);
    let y = MyBox::new(x);

    assert_eq!(5,x);
    assert_eq!(5,*y);
}

函数和方法的隐式解引用转化(Deref Coercion)

隐式解引用转化(Deref Coercion)是为函数和方法提供的一种便捷特性

假设T实现了Deref trait:
- Deref Coercion可以把T的引用转化为T经过Deref 操作后生成的引用

当把某类型的引用传递给函数或方法时,但它的类型与定义的参数类型不匹配:

Deref Coercion就会自动发生

一编译器会对deref进行一系列调用,来把它转为所需的参数类型

  • 在编译时完成,没有额外性能开销
use std::ops::Deref;

fn hello(name: &str) {
    println!("Hello,{}",name);
}


fn main() {
    let m = MyBox::new(String::from("Rust"));

    // &m这个类型是&MyBox<String>
    // 实现了deref方法,能把MyBox的引用转化为String的引用
    // 同样的String也实现了deref方法,所以最后返回的就是&str
    // 如果没有deref,那么我们这个传值就要&(*m)[..]这么写
    hello(&m);

    hello("Rust");
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x:T) -> MyBox<T> {
        MyBox(x)
    }
}

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

    fn deref(&self) -> &T {
        &self.0
    }
}

解引用与可变性

可使用DerefMut trait重载可变引用的*运算符

在类型和 trait在下列三种情况发生时,Rust会执行deref coercion:

―当T: Deref<Target=U>,允许&T转换为&U

—当T: DerefMut<Target=U>,允许&mutT转换为&mut U

—当T: Deref<Target=U>,允许&mut T转换为&U

4、Drop Trait

实现 Drop Trait,可以让我们自定义当值将要离开作用域时发生的动作。

—例如:文件、网络资源释放等

―任何类型都可以实现 Drop trait

Drop trait只要求你实现drop 方法

―参数:对self的可变引用

Drop trait在预导入模块里( prelude)

struct CustomSmartPointer {
    data:String
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!",self.data);
    }
}

fn main() {
    let c = CustomSmartPointer{data:String::from("my stuff")};
    let d = CustomSmartPointer{data:String::from("other stuff")};
    println!("CustomSmartPointers created.")
}

 

这里我们确实可以看出调用了drop方法

使用std::mem::drop来提前drop 值

很难直接禁用自动的drop功能,也没必要

  • Drop trait的目的就是进行自动的释放处理逻辑

Rust不允许手动调用Drop trait 的drop方法

但可以调用标准库的std::mem::drop 函数,来提前drop 值

struct CustomSmartPointer {
    data:String
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!",self.data);
    }
}

fn main() {
    let c = CustomSmartPointer{data:String::from("my stuff")};
    drop(c);
    let d = CustomSmartPointer{data:String::from("other stuff")};
    println!("CustomSmartPointers created.")
}

那么会不会出现double free的这种情况呢?答案是不会的,因为rust的设计非常安全

5、RC<T>:引用计数智能指针

有时,一个值会有多个所有者

为了支持多重所有权:Rc<T>

  • reference couting(引用计数)
  • 追踪所有到值的引用
  • О个引用:该值可以被清理掉

Rc<T>使用场景

需要在heap 上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定哪个部分最后使用完这些数据

Rc<T>只能用于单线程场景

Rc<T>不在预导入模块( prelude)

Rc::clone(&a)函数:增加引用计数

Rc::strong_count(&a):获得引用计数

还有Rc::weak_count函数

两个List共享另一个List的所有权

enum List {
    Cons(i32,Box<List>),
    Nil
}

use crate::List::{Cons,Nil};

fn main() {
    let a = Cons(5, 
        Box::new(Cons(10,
            Box::new(Nil))));
    
    let b = Cons(3, Box::new(a));

    let c = Cons(3, Box::new(a));
}

我们对上图进行了代码的编写,但是它是不能通过代码编译的,会报一个所有权已经被移动的错误

enum List {
    Cons(i32,Rc<List>),
    Nil
}

use std::rc::Rc;

use crate::List::{Cons,Nil};

fn main() {
    let a = Rc::new(Cons(5, 
        Rc::new(Cons(10,
            Rc::new(Nil)))));
    
    let b = Cons(3, Rc::clone(&a));

    let c = Cons(3, Rc::clone(&a));
}

这样就不会报错了

现在我们来打印一下引用计数:

enum List {
    Cons(i32,Rc<List>),
    Nil
}

use std::rc::Rc;

use crate::List::{Cons,Nil};
fn main() {
    let a = Rc::new(Cons(5, 
        Rc::new(Cons(10,
            Rc::new(Nil)))));

    println!("count after creating a={}",Rc::strong_count(&a));            
    
    let b = Cons(3, Rc::clone(&a));

    println!("count after creating b={}",Rc::strong_count(&a));    

    {
        let c = Cons(3, Rc::clone(&a));
        println!("count after creating c={}",Rc::strong_count(&a));    
    }        

    println!("counting after c goes out of scope = {}",Rc::strong_count(&a));
}

Rc::clone() vs类型的clone()方法

Rc::clone():增加引用,不会执行数据的深度拷贝操作

类型的clone():很多会执行数据的深度拷贝操作。

Rc<T>通过不可变引用,使你可以在程序不同部分之间共享只读数据

但是,如何允许数据变化呢?

6、RefCell<T>和内部可变性

内部可变性( interior mutability)

内部可变性是 Rust的设计模式之一

它允许你在只持有不可变引用的前提下对数据进行修改
一数据结构中使用了unsafe 代码来绕过Rust正常的可变性和借用规则

RefCell<T>

与Rc<T>不同,RefCell<T>类型代表了其持有数据的唯一所有权。

回忆一下:借用规则
在任何给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用
引用总是有效的

RefCell<T>与Box<T>的区别

Box<T> RefCell<T>
编译阶段强制代码遵守借用规则 只会在运行时检查借用规则
否则出现错误 否则触发panic

借用规则
在不同阶段进行检查的比较

 

编译阶段 运行时
尽早暴露问题 问题暴露延后,甚至到生产环境
没有任何运行时开销 因借用计数产生些许性能损失
对大多数场景是最佳选择 实现某些特定的内存安全场景(不可变环境中修改自身数据)
是Rust的默认行为  

RefCell<T>

与Rc<T>相似,只能用于单线程场景

选择Box<T>、Rc<T>、RefCell<T>的依据

 

  Box<T> Rc<T> RefCell<T>
同一数据的所有者 一个 多个 一个
可变、不可变借用

不可变借用

(编译时检查)

可变、不可变借用

(编译时检查)

可变性、借用检查

(运行时检查)

其中:即便RefCell<T>本身不可变,但仍能修改其中存储的值

内部可变性:可变的借用一个不可变的值

 

fn main() {
    let x = 5;
    let y = &mut x;
}

这里就会报一个不能将不可变的x借用为可用的

pub trait Messenger {
    fn send(&self,msg:&str);
}

pub struct LimitTracker<'a,T: 'a + Messenger>{
    messenger: & 'a T,
    value:usize,
    max:usize,
}

impl<'a,T> LimitTracker<'a,T>
where
    T:Messenger,
{
    pub fn new(messenger: &T,max:usize) -> LimitTracker<T> {
        LimitTracker{
            messenger,
            value:0,
            max,
        }
    }

    pub fn set_value(&mut self,value: usize) {
        self.value = value;
        let percentage_of_max = self.value as f64 / self.max as f64;
        if percentage_of_max > 1.0 {
            self.messenger.send("Error:You are over your quota");
        } else if percentage_of_max > 0.9 {
            self.messenger.send("Urgent warning:over 90%");
        } else if percentage_of_max > 0.75 {
            self.messenger.send("Warning:over 75%");
        }
    }
}

#[cfg(test)]
mod tests {
    use std::cell::RefCell;

    use super::*;

    struct MockMessenger {
        sent_messages:RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new()->MockMessenger {
            MockMessenger{
                sent_messages:RefCell::new(vec![])
            }
        }
    }

    impl Messenger for MockMessenger {
       fn send(&self,message:&str) {
           self.sent_messages.borrow_mut().push(String::from(message))
       }
    }

    #[test]
    fn it_send_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
        
        limit_tracker.set_value(80);
        assert_eq!(mock_messenger.sent_messages.borrow().len(),1);
    }

}

使用RefCell<T>在运行时记录借用信息

两个方法(安全接口):

borrow方法

   ·返回智能指针Ref<T>,它实现了Deref

borrow_mut方法

   返回智能指针RefMut<T>,它实现了Deref

RefCelk<T>会记录当前存在多少个活跃的Ref<T>和RefMut<T>智能指针:

   每次调用borrow:不可变借用计数加1

   任何一个Ref<T>的值离开作用域被释放时:不可变借用计数减1

   每次调用borrow_mut:可变借用计数加1

   任何一个RefMut<T>的值离开作用域被释放时:可变借用计数减1

以此技术来维护借用检查规则:

   任何一个给定时间里,只允许拥有多个不可变借用或一个可变借用。

将Rc<T>和 RefCell<T>结合使用来实现一个拥有多重所有权的可变数据   

use std::{cell::RefCell, rc::Rc};
use crate::List::{Cons,Nil};

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>,Rc<List>),
    Nil
}

fn main() {
    let value = Rc::new(RefCell::new(5));
    let a = Rc::new(Cons(Rc::clone(&value),Rc::new(Nil)));
    let b = Cons(Rc::new(RefCell::new(6)),Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)),Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}",a);
    println!("b after = {:?}",b);
    println!("c after = {:?}",c);

}

其它可实现内部可变性的类型

Cellk<T>:通过复制来访问数据

Mutex<T>:用于实现跨线程情形下的内部可变性模式

7、循环引用可导致内存泄漏

Rust可能发生内存泄漏

Rust的内存安全机制可以保证很难发生内存泄漏,但不是不可能。

例如使用Rc<T>和 RefCelK<>就可能创造出循环引用,从而发生内存泄漏:―每个项的引用数量不会变成0,值也不会被处理掉。

use std::{cell::RefCell, option, rc::Rc};
use crate::List::{Cons,Nil};

#[derive(Debug)]
enum List {
    Cons(i32,RefCell<Rc<List>>),
    Nil
}

impl List {
    fn trail(&self)->Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_,item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc :: new(Cons(5,RefCell :: new( Rc :: new(Nil))));

    println!( "a inlitial rc count = {}" ,Rc :: strong_count(&a));
    println! ( "a next item = {:?}",a.trail());

    let b = Rc :: new(Cons(10,RefCell :: new(Rc :: clone(&a))));
    println!( "a rc count after b creation = {}",Rc :: strong_count(&a));
    println! ("b initial rc count = {}",Rc :: strong_count(&b));
    println!( "b next item = {:?}", b.trail());

    if let Some(link) = a.trail() {
        *link.borrow_mut() = Rc :: clone(&b);
    }

    println!( "b rc count after changing a = {}",Rc :: strong_count(&b));
    println!("a rc count after changing a = {}",Rc :: strong_count(&a));

}

如果我们在这后面加上:

    // Uncomment the next line to see thatI we have a cycle;it will overflow the stack.
    println!("a next item = {:?}",a.trail());

这行代码

程序就直接崩了

防止内存泄漏的解决办法

依靠开发者来保证,不能依靠Rust

重新组织数据结构:一些引用来表达所有权,一些引用不表达所有权

   循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系

   而只有所有权关系才影响值的清理

防止循环引用
把Rc<T>换成Weak<T>

Rc:clone为Rc<T>实例的strong_count加1,Rc<T>的实例只有在strong_count为0的时候才会被清理

Rc<>实例通过调用Rc.:downgrade方法可以创建值的Weak Reference(弱引用)

返回类型是Weak<T>(智能指针)

Rc<T>使用weak_count来追踪存在多少Weak<T>。

weak_count不为О并不影响Rc<T>实例的清理

Strong vs Weak

Strong Reference(强引用)是关于如何分享Rc<T>实例的所有权

Weak Reference(弱引用)并不表达上述意思使用

Weak Reference并不会创建循环引用:

   当Strong Reference数量为0的时候,Weak Reference会自动断开

在使用Weak<T>前,需保证它指向的值仍然存在:

   在 Weak<>实例上调用upgrade方法,返回Option<Rc<T>>

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

#[derive(Debug)]
struct Node {
    value: i32,
    parent:RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main(){
    let leaf = Rc::new(Node{
        value:3,
        parent:RefCell::new(Weak::new()),
        children:RefCell::new(vec![]),
    });

    println!( "leaf parent = {:?}", leaf.parent.borrow().upgrade());

    println!(
        "leaf strong = {}, weak = {}",Rc :: strong_count(&leaf),
        Rc :: weak_count(&leaf)
    );
    // println!( "leaf parent = {:?}", leaf.parent.borrow().upgrade());

    {
        let branch = Rc::new(Node{
            value: 5,
            parent: RefCell :: new(Weak :: new( )),
            children:RefCell::new(vec![Rc::clone(&leaf)]),
        });
    
        *leaf.parent.borrow_mut() = Rc :: downgrade(&branch);
    
        // println!( "leaf parent = {:?}", leaf.parent.borrow().upgrade());

        println!(
            "branch strong = {}, weak = {}",Rc :: strong_count(&branch),
            Rc :: weak_count(&branch)
        );

        println!(
            "leaf strong = {}, weak = {}",Rc :: strong_count(&leaf),
            Rc :: weak_count(&leaf)
        );
    }
        
    println!( "leaf parent = {:?}", leaf.parent.borrow().upgrade());
 
    println!(
        "leaf strong = {}, weak = {}",Rc :: strong_count(&leaf),
        Rc :: weak_count(&leaf)
    );

}    

 

你好:我的2025