Rust 学习笔记(十一)|智能指针

从这一节开始,正式开始中级Rust特性的入门学习,主要包括:智能指针,多线程,异步,Unsafe Rust等。

Rust中最常用的指针就是引用,它和其他语言的指针相比多了借用规则这一特性。智能指针之所以“智能”,是因为它除了指针的基本功能之外,它还存在其他的元数据以及其他的特殊功能。引用只是借用数据,而智能指针很多时候是拥有数据的所有权的。在Rust中,智能指针和引用一样,都实现了DerefDrop这两个trait。

其实之前学习的StringVec<T>就是智能指针。它们除了指向堆内存上存放具体数据的指针之外,还有“容量”和“长度”这两个概念,这就是它们和普通指针相比多出的“元数据”。

Rust中的智能指针有很多,入门主要了解DerefDrop两个trait以外,了解以下几种智能指针即可:

  • Box<T>:指向存储在堆内存上的数据;
  • Rc<T>:允许一个变量拥有多个所有者;
  • RefCell<T>Ref<T> RefMut<T>):允许在运行时(而不是编译时)才检查借用规则。

1 Box<T>

Box<T> 在内存中的结构

Box<T>是最简单的智能指针,它的主要作用就是指向一个存储在堆内存上的数据。使用Box::new()方法创建一个Box<T>智能指针,并通过参数创建一个存储在堆内存上的数据。

fn main() {
    let x: Box<i32> = Box::new(5);
    
    println!("x: {x}");
}

Box<T>在功能上比较接近C/C++的指针,它有确定的大小,并且可以看作一个变量,变量内存放的是指向堆内存上指定数据的地址。所以我们可以使用Box<T>方便地创建一个链表数据结构:

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

enum List {
    Cons(i32, Box<List>),   // 第二个数据类型是指向List变体的智能指针
    Nil,
}

// Box::new() 生成一个指向数据的指针,数据存放在堆内存上
fn main() {
    let l = Cons(1, Box::new(Cons(2, Box::new(Nil))));
}

这里的Nil不是Rust内置的关键字,它只是一个结束标记,作为List枚举的一个变体存在。main函数中创建的链表有三个节点,前面两个节点的数据是i32,并包含一个(智能)指针指向下一个节点,最后一个节点是Nil

Box<T>有以下三点常用场景(本节只涉及第一个场景):

  1. 在编译时,某类型的大小无法确定,但使用该类型时,上下文又需要知道它的确切大小;
  2. 当拥有大量数据,想移交所有权,但是需要确保在操作时数据不会被复制;
  3. 使用某个值时,只关心它是否实现了特定的trait,而不关心它的具体类型(有点类似泛型的trait bound)。

2 Deref trait 和 Drop trait

2.1 Deref trait

定义Deref trait可以使用户自定义解引用运算符*的行为,并且可以使得智能指针可以被当作常规的引用一样处理。

常规的引用就是一种指针,可以使用解引用运算符*

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

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

可以使用Box<T>替换上面程序中的引用:

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

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

Box<T>可以被看作只有一个元素的元组结构体。基于这个原则,我们可以实现自定义的MyBox<T>,并实现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;    // 定义Deref的关联类型,关联类型是一类特殊的泛型

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

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

    assert_eq!(x, 5);
    assert_eq!(*y, 5);    // *(y.deref())
}

Deref trait的另一个重要作用是函数和方法的隐式解引用转化(Deref Coercion),它是为函数或者方法提供的一种便捷特性。假设T实现了Deref trait,那么Deref Coercion就会把T的引用转化为T经过Deref操作生成后的引用。当把某类型的引用传递给函数或者方法时,但是引用的类型和函数/方法定义的类型不匹配,那么Deref Coercion就会自动发生,编译器会在编译阶段堆deref进行一系列调用,来把它转化为所需的参数类型。这个过程在编译期就会完成,所以不会产生任何的运行时开销。

在之前的学习中,如果一个函数/方法的参数是字符串切片,那么它也可以接收一个字符串的引用作为参数。这是因为String实现了Deref这个trait,且其中的方法就是将&String类型转化为&str类型,所以参数类型可以匹配。

可以使用DerefMut trait重载可变引用的解引用运算符*。当类型和trait遵循下面三种情况时,Rust会执行deref coercion:

  • T:Deref<Target = U>,允许将&T转换为&U
  • T:DerefMut<Target = U>,允许将&mut T转换为&mut U
  • T:Deref<Target = U>,允许将&mut T转换为&U(反之不可)

2.2 Drop trait

类型实现Drop trait后,我们就可以自定义当它的值离开作用域的动作。任何类型都可以实现Drop trait,它只要求实现一个drop方法,它的参数是&mut self

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!("CustomSmartPointer Created!");
}

我们不能手动调用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!("CustomSmartPointer Created!");
}

3 引用计数 Rc<T>

Rc<T>类型是一个用于引用计数的智能指针(reference counting),它除了指针的基本作用外,还记录着指向某一块数据的引用的数量。它指向的数据允许多个不可变引用同时指向该数据,这种设计使得它可以实现图数据结构。

当我们需要在heap上分配数据,但是这些数据被程序的多个部分读取(只读),但是在编译时又无法确定哪个部分最后使用完这些数据,这时候就可以考虑使用Rc<T>,否则考虑使用所有权转移。

图数据结构

Rc<T>只适用于单线程场景,多线程场景下有另外的智能指针。

Rc<T>不在预导入模块中,并且有以下几个常用函数:

  • Rc::clone(&a):增加引用计数,不会深度拷贝数据
  • Rc::strong_count(&a)Rc::weak_count(&a)):获取引用计数值

下面看一个例子:

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

enum List {
    Cons(i32, Rc<List>),
    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(4, Rc::clone(&a));
}
use std::rc::Rc;
use crate::List::{Cons, Nil};

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

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

    println!("Rc a strong count: {}", Rc::strong_count(&a));

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

4 RefCell<T> 和内部可变性

RefCell<T>是对数据保有唯一所有权的智能指针,和Box<T>类似,但是 它的特殊功能是不在编译时检查借用规则,而是在运行时检查。如果在运行时违反了借用规则,那么程序就会panic。

借用规则:在一个给定的时刻,数据只能拥有任意数量的不可变引用或者唯一的可变引用;引用总是有效的。

RefCell<T>的最大作用是在特定的内存安全环境下,在不可变环境中改变数据的内部数据。

Rc<T>类似,RefCell<T>只适用于单线程场景。

三种智能指针的适用场景

下面看一个例子:

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 hare over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.8 {
            self.messenger.send("Warning: You've used up over 80% of your quota!")
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

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

    impl Messenger for MockMessenger {
        fn send(&self, msg: &str) {     
            self.sent_messages.push(String::from(msg));     // 报错,无法使用不可变引用改变变量
        }
    }

    #[test]
    fn it_send_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.len(), 1);
    }
}

对于上面的程序,我们想使用一个不可变借用改变内部的数据值,这时就可以使用RefCell<T>

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 hare over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.8 {
            self.messenger.send("Warning: You've used up over 80% of your quota!")
        }
    }
}

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

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,        // 将数据套在一个RefCell<T>内
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),    // 修改构造函数
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, msg: &str) {
            self.sent_messages.borrow_mut().push(String::from(msg));     // borrow_mut() 返回可变借用
        }
    }

    #[test]
    fn it_send_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);     // borrow() 返回不可变借用
    }
} 

在上面的例子中,我们使用了RefCell<T>的两个安全接口:

  • borrow():返回智能指针Ref<T>,内部数据的不可变借用
  • borrow_mut():返回智能指针RefMut<T>,内部数据的可变借用

RefCell<T>会记录当前存在多少个活跃的Ref<T>RefMut<T>。每次调用borrow(),不可变借用数+1,借用离开作用域时计数-1;每次调用borrow_mut(),可变借用数+1,借用离开作用域时计数-1。在运行时,Rust就依靠这个技术实现借用规则的检查,当程序违反借用规则时,程序就会panic。

另一个常用的场景是将Rc<T>和RefCell<T>结合使用,这样数据就可以在编译时定义多个可变的借用了,但是它们依然会在运行时进行借用规则检查。

use std::cell::RefCell;
use std::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);        // Cons(RefCell { value: 15 }, Nil)
    println!("{:?}", b);        // Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
    println!("{:?}", c);        // Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
}

除了RefCell<T>,Rust还提供了Cell<T>(通过复制来访问数据)、Mutex<T>(用于跨线程情景下的内部可变性模式)等可以实现内部可变性的类型。

5 循环引用导致的内存溢出

使用Rc<T>RefCell<T>可以形成“a引用b,同时b也引用了a”的循环引用,这样当变量走出作用域时,两个变量的引用计数都不是0,所以都不会被释放:

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

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

impl List {
    fn tail(&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 initial rc count: {}", Rc::strong_count(&a));
    println!("a next item: {:?}", a.tail());

    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!("a next item: {:?}", b.tail());

    if let Some(link) = a.tail() {
        *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));

    // println!("a next item:{:?}", a.tail());      // stack overflow
}

解决内存泄漏的方法:要么靠开发人员检查代码,要么重构代码逻辑,将引用分为持有所有权和不持有所有权两种:把Rc<T>换成Weak<T>

Rc::clone()会把Rc<T>的实例的strong_count加一,Rc<T>的实例只有在strong_count为0的时候才会被清理。Rc<T>实例通过调用Rc::downgrade方法可以创建值的Weak Reference(弱引用),返回值就是Weak<T>。调用该方法会使得weak_count加1。Rc<T>使用weak_count来追踪存在多少Weak<T>,weak_count不为0不影响Rc<T>实例的清理。

看下面这个例子:创建一个树结构,使得所有的节点都可以指向它的父节点,同时可以指向所有的子节点。

use std::cell::RefCell;
use std::hint::black_box;
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,
        children: RefCell::new(vec![]),
        parent: RefCell::new(Weak::new()),
    });

    println!("leaf parent: {:?}", leaf.parent.borrow().upgrade());  // upgrade() 把 Weak<T>转化为Rc<T>

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),     // 树干指向叶子
        parent: RefCell::new(Weak::new()),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);     // 叶子指向树干
    println!("leaf parent: {:?}", leaf.parent.borrow().upgrade());
}
转载声明:

除特殊声明外,本站所有文章均由 debussy 原创,均采用 CC BY-NC-SA 4.0 协议,转载请注明出处:Include Everything 的博客
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇