rust学习十五.3、智能指针相关的Deref和Drop特质

 

一、前言

智能指针为什么称为智能指针? 大概因为它所包含的额外功能。

这些额外的功能使得编码或者运行时让指针看起来更有效、并体现某些“智”的特征,所以,我猜测这应该是rust发明人这么称呼此类对象为智能的原因。

 

据前面有关章节所述,我们知道智能指针多基于结构体(struct)扩展实现。

我们知道,struct大体上相当于OOP的Class Object(类对象)。struct可以有自身方法,也可以实现特质。

所以,智能指针的所谓”智“(如果称为”魔力“之类也可以),多数来源于这些特质。 如果为了便于理解,也可以把这些特质称为插件,插件越多,一个武器的功能就越强大。

好比多功能道具,可以装上很多头,实现乱七八糟的功能。

 

这些特质中,有两个需要重点介绍:Deref,Drop

 

无论是Box还是其它智能指针,都面临2个问题:如何访问指针指向的数据,如何释放指针所指向的数据。

访问数据涉及到编码的方便性,释放指针涉及到内存安全问题。

编码方便性要求rust提供一些更友好的书写方式,以便工程师能够更容易编写相对容易阅读的代码。要知道rust本身的语法已经够丑陋了,不能让指针把语法变得更加丑陋。

二、概念准备

引用(Reference -- ref)

指针本身就是引用。 前面有关文章中提到 &var,这个&就是构建一个指针,用于引用var的值。

解除引用(DeReference -Defef)

顾名思义,就是解除对xxx的引用。

但这实际有两个歧义:

1.不间接引用,而是直接用

2.不再引用,也无法用

结合有关书籍上下文和rust的意思,应该理解为:不采用引用的方式,而是直接使用xxx值

rust中使用 * 表示解除引用。

丢掉/清理(Drop/clean- Drop)

和引用行为有关的另外一个概念。当引用完成后,解除对这些资源的控制,就称为丢掉。

三、Deref --解除引用

书籍中关于Deref的作用概括:方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针

3.1Deref的Box实现

#[lang = "deref"]
#[doc(alias = "*")]
#[doc(alias = "&*")]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "Deref"]
pub trait Deref {
    /// The resulting type after dereferencing.
    #[stable(feature = "rust1", since = "1.0.0")]
    #[rustc_diagnostic_item = "deref_target"]
    #[lang = "deref_target"]
    type Target: ?Sized;

    /// Dereferences the value.
    #[must_use]
    #[stable(feature = "rust1", since = "1.0.0")]
    #[rustc_diagnostic_item = "deref_method"]
    fn deref(&self) -> &Self::Target;
}
//-------------------------------------------------------
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized, A: Allocator> Deref for Box<T, A> {
    type Target = T;

    fn deref(&self) -> &T {
        &**self
    }
}

type Target: ?Sized;  -- >表示目标是一个不固定大小的类型

Box的deref的返回是 &**self。

特别需要注意的是:deref函数的实现是比较怪异。

我们要理解:&**self是如何变成 &T的

  1. 首先&self是指向Box的一个引用
  2. *self,解除引用,直接得到Box。*self实际是*(&self)。
  3. *(*self)=*(Box),Box本身包含一个指针,所以得到T
  4. &(**self)=&(*(*(&Self)))=&(*Box)=&(T)

最终deref让我们得到Box指针的实际数据的引用。

--

注意:由于rust的一个约定,阅读对象方法体需要特别注意

rust对象方法通常形如 fn  xxxx(&self){}。

这存在一个默认的对自身的引用,而不是自身。但是我们又在方法体中写self.而这个self其实是&self。

 

3.2编译器对Deref的支持

很多时候,某个对象(struct,enum等)实现某个特质的时候,我们通过对象实例去调用特质方法来使用特质。

当Rust的做法是通过编译器的努力,让工程师可以不直接调用deref就能够执行deref,从而编写更加简单的代码,并实现deref的主要目的:智能指针可以被当作常规引用来对待

许多语言的编译器都类似的行为,越是新的语言,越是新版本的编译器,体现越明显。例如java对匿名函数的支持,对郎打表达式的支持。

那么做的原因,即使为了减轻工程师的负担:便利地得到某种功能,而不需要使用复杂的操作

 

更具体一点就是:工程师可以不要再写*号,在多个场景中,rustc编译器会尝试替工程师调用deref。

根据书上描述的规则如下:

  • T: Deref<Target=U> 时从 &T&U
  • T: DerefMut<Target=U> 时从 &mut T&mut U
  • T: Deref<Target=U> 时从 &mut T&U

需要我们理解的是第3条:根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译);将不可变引用转换为可变引用则需要初始的不可变引用是数据唯一的不可变引用,而借用规则无法保证这一点。

因此,Rust 无法假设将不可变引用转换为可变引用是可能的。

然而不能理解也不重要,总之这是一个规定.

 

示例:

 
use std::ops::Deref;
 
fn hello_ref_string(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    //1.0 Box的隐式转换  -- &Box转为 &String,不需要写复杂的 &(*Box)
    let eng=String::from("英国佬");
    let eng_box=Box::new(eng);
    hello_ref_string(&eng_box);  // 隐式转换,把 &Box转为 &String。 通过这种引用可以省掉 * 符号

    //2.0 如果愿意,也可以使用*符号显式转换。

    let german=String::from("德国佬");
    let german_box=Box::new(german);
    hello_ref_string(&*german_box);  // 这相当于*解除引用后得到T,再&T得到&T 

    //3.0  也可以显示调用deref方法

    let french=String::from("法国佬");
    let french_box=Box::new(french);
    hello_ref_string(french_box.deref());  // 显式调用Deref方法,得到&T
}

 rustc的隐式转换,就是为了避免我们写 frech_box.deref()。工程师可以直接写 &frech_box就达到隐式调用deref的效果。

四、Drop

如书所言,Drop大概是rust最重要的一个特质,它负责在离开作用域后,为对象自动释放相关资源(文件,网络,内存...)。

然而Drop的妙处有不少:

  1. 可以为rust的任意类型实现Drop
  2. rust通过编译器的方式,会为对象插入离开特定作用域的代码-- 即调用对象的Drop实现 
  3. 如果有多个对象需要释放,那么rust编译器会自动决定需要释放的顺序(通常是创建对象时间的逆序,但不明确是否绝对)
  4. Drop 无法被禁止,也无需禁止,因为它的初衷就是为了自动清理
  5. Drop 也不能显示调用,但可以使用std::mem::drop()函数来提前释放资源. 这个函数其实就是主要调用Drop特质
  6. 所有权系统确保引用总是有效的,也会确保 drop 只会在值不再被使用时被调用一次

通过这个机制,rust避免了工程师插入手动释放资源代码问题:麻烦、可能会忘记、可能释放顺序存在错误等等。

这些规则都挺好理解。

据本人所知,rust的自有智能指针(String,Vec,Box,Rc,Ref,RefMut)都实现了Drop,但为什么enum没有实现?

此问题,暂时不考虑了。

现添一个例子(模仿书本),演示Drop的调用机制,和手动调用drop函数:

#[derive(Debug)]
struct Student {
    name: String,
    age: u32,
}
impl Drop for Student {
    fn drop(&mut self) {
        println!("释放{}({}岁)的资源",self.name,self.age);
    }
}
fn test_drop() {
    let mut mao = Student {
        name: String::from("高温i"),
        age: 20,
    };
    mao.age = 21;
}
fn main() {
    test_drop();
    let lu = Student {
        name: String::from("卢俊义"),
        age: 40,
    };
    println!("{:#?}", lu);
    std::mem::drop(lu);  //手动释放。如果不释放,那么程序退出的时候也会被自动调用一次。 
                         //drop 函数主要作用就是调用Drop特质,所以这里手动释放也行。
    println!("main 结束");
}

执行结果:

从执行结果可以验证几个结论:

  1. drop是自动调用的
  2. 离开任意作用域都可能触发drop,就看变量的作用范围。有的作用于某个方法、函数,有的作用于主函数
  3. 手动调用drop(),也会自动Drop特质方法

 

最后,看看Box的Drop实现:

#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Box<T, A> {
    #[inline]
    fn drop(&mut self) {
        // the T in the Box is dropped by the compiler before the destructor is run

        let ptr = self.0;

        unsafe {
            let layout = Layout::for_value_raw(ptr.as_ptr());
            if layout.size() != 0 {
                self.1.deallocate(From::from(ptr.cast()), layout);
            }
        }
    }
}

 

Box的其中一个成员(内存分配器)可以执行资源释放--具体而言就是堆内存释放。

五、小结

当学习了Deref和Drop两个特质之后,对于智能指针的“智能”更有体会了。

以下是个人的一些初步体会:

  1. rust通过实现Deref和Drop大大方便了对数据的引用和对资源的释放,换言之,以往在类似c++那用的事情变得相对简单了
  2. rust通过适当的性能牺牲达到相对的内存安全以及相对高的性能,某种程度上是可以接受的。
  3. 如果在特定的应用中,对于特定功能的性能比较执着,那么也可以考虑继续采用硬件代码/c/c++之类的语言编写
  4. 智能指针的存在,是否意味着,在面向具体业务的编码中,大部分类型应该采用智能指针?

 

小结:

  1. 通过实现Deref,智能指针可以被当作常规引用来对待
  2. 通过实现Drop,可以解决智能指针资源的释放问题(前提是代码写对了)
  3. Deref的deref()可以手动调用,而Drop的drop()是不允许手动调用的

 

posted @ 2024-12-31 17:33  正在战斗中  阅读(33)  评论(0编辑  收藏  举报