Rust for cpp devs - 智能指针

与 cpp 类似,Rust 也有智能指针。Rust 中的智能指针与引用最大的不同是,智能指针 own 内存,而引用只是借用。

一般来讲,Rust 中的智能指针通过 struct 实现,并实现了 DerefDrop 两个 trait。

  • Deref 允许智能指针表现得跟引用一样,因此代码可以在引用和智能指针上复用。

  • Drop 允许自定义对象在离开作用域时的行为。

事实上,StringVec<T> 类型都是智能指针。

标准库中最常见的智能指针是:

  • Box<T> 用于在 heap 上分配内存,类似于 std::unique_ptr

  • Rc<T> 用于引用计数(reference counting)对象,类似于 std::shared_ptr

  • Ref<T>RefMut<T> 强制在运行时(而非编译时)执行借用规则

使用 Box<T> 指向堆中的数据

最简单的智能指针就是 Box<T>,它将数据存放在堆上而非栈上。除此之外,它不存在别的 overhead,当然,也不具备其余的功能。常用的场景:

  • 当不能在编译时确定对象大小时(例如递归的数据结构)
  • 当希望传递 ownership 并保证不产生拷贝时
  • 当 own 一个只知道 trait 而不知道具体类型的值时(例如,Box<dyn std::error::Error>

下面的例子讲了如何创建 Box<T> 实例,以及如何使用。可以看出用法和引用没区别。

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

使用 Box<T> 的好处是让链表一类的数据结构成为可能。如果不使用智能指针,对于这类的递归数据结构,我们无法预先知道其大小,也就无法为对象分配内存。

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

以上代码中,由于 Cons 结构体包含了 List 枚举类型,而 List 可能是另一个 Cons。这导致无限的递归。

但是,智能指针的大小是固定的。因此可以用智能指针将这个定义改为:

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

自己实现 Box<T>

为了更好地理解智能指针,我们实现一个 MyBox<T> 让他拥有跟 Box<T> 类似的行为。

Box<T> 一样,我们用一个 tuple struct 实现:

struct MyBox<T>(T);

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

自定义 Deref

如果按以下代码来使用它:

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

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

会报错 * 操作符无法解引用。这是因为我们没有实现 Deref trait。

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        return &self.0;
    }
}

Rust 将 *y 替换为 *(y.deref()),这有些类似于 cpp 中 std::unique_ptr* 的重载。

Deref 的强制隐式转换

Deref 不只这么简单的解引用一个作用。

对于实现了 Deref 接口的对象,Rust 可以进行隐式转换。规则是:

一个类型为 T 的对象 foo,如果 T 实现了 Deref<Target=U>,那么,foo 的智能指针或者引用在使用时会尝试调用 deref() 直到类型匹配。

例如:

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

fn main() {
    let s = String::from("world");
    let s_ptr = MyBox::new(s);
    hello(&s_ptr);
}

可以看出,虽然 &s_ptr 的类型是 &MyBox<String>,在发现与 hello 函数类型不匹配时,转成了 &str。具体流程是:

  • 通过 MyBox::deref()&MyBox<String> 转为了 &String,类型尚未匹配。

  • 通过String::deref()String 也是智能指针类型)将 &String 转为了 &str,类型匹配。

这种隐式转换可以让开发者降低负担,并且不会引入 overhead,因为都在编译时完成。

自定义 Drop

Drop trait 可以自定义离开作用域时变量的行为,类似于析构函数。它一般用于回收资源,例如关闭文件等。Box<T> 会回收堆上占用的内存。

我们可以为之前的结构体实现 Drop trait,这样在销毁时可以打印信息:

impl<T: Display> Drop for MyBox<T> {
    fn drop(&mut self) {
        println!("destroying {}", self.0);
    }
}

手动调用 std::mem::drop

由于 Drop::drop 的意义是自动管理内存,Rust 不允许手动调用以避免 double free 问题。但是,标准库提供了 std::mem::drop 用于在离开作用域之前强制 drop。

由于 std::mem::drop 在 prelude 模块中,我们可以直接调用:

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

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

    drop(y);  // manually drop y here
    let s = String::from("world");
    hello(&s);
}

结果是:

destroying 5
Hello, world

可以看出,程序先回收了 y 再离开作用域。而且,即使我们提前回收了 y,程序也没有因为 double free 而 crash。

使用 Rc<T> 来共享对象

Rc<T> 可以用引用计数的方式来共享对象。类似于 cpp 中的 std::shared_ptr

例如,我们需要构造如下一个链表:

sharing ownership of a list

由于 a 链表被 bc 共享,但是在 Rust 中,我们要求了每个值只有一个 owner。因此下面的代码无法编译通过:

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)); // a is moved
    let c = Cons(4, Box::new(a));
}

这时需要引入 Rc<T> 智能指针了。我们将 List 中的 Cons 类型改成持有一个 Rc<List>

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

此外,当我们需要共享 List 的实例时,我们调用 Rc::clone 来增加其引用计数:

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

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));

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

Rc::clone(&a) 和普通的 a.clone() 相比,由于没有做 deep copy,只是将引用计数加1,因此非常 cheap。我们使用 Rc::strong_count(&a) 可以看到 a 链表的引用计数是3,被 a, b, c 三个变量共同持有。类似的,在 Drop trait 中定义了当变量离开作用域时,引用计数减少 1,减少到 0 则回收。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容