前面我们学习了Box<T>, Rc<T>, RefCell<T>三个智能指针。 智能指针只是一种数据结构,它们的表现类似指针,同时有额外的元数据和功能。大多数智能指针拥有其所指向数据的所有权。 Rust中的智能指针通常使用结构体实现,其区别常规结构体的显著特征在于智能指针结构体会实现Deref trait和Drop trait,有的智能指针结构体还会实现DerefMut trait。

Deref trait使得可以将智能指针当做常规的不可变引用使用。Drop trait允许我们自定义当智能指针离开作用域时运行的代码,可以利用Drop实现Move语义,例如Vec实现了Drop当其离开作用域时会自动释放其指向的堆内存上的数据。 DerefMut trait使得可以将智能指针当做常规的可变引用使用。今天我们主要学习DerefDerefMut这两个trait。

1.常规引用和解引用运算符*

在学习Deref和DerefMut trait之前,先来看一下解引用运算符*如何用在常规引用上。

例1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let mut x = 5;
    let y = &x;
    if *y == 5 {
        println!("{:p} {}", y, *y); // 0x7ffeed618e9c 5
    }
    let z = &mut x;
    *z = 10;
    assert_eq!(x, 10);
}

例1中代码变量x在栈上存了一个i32类型的值5。变量yx的一个不可变引用。第4行if语句先对变量y进行解引用得到y所指向的值,再去判断其是否和数值5相等。 第7行变量zx的一个可变引用,第8行先对z进行解引用得到其所指向的值,并将值修改为10

2.智能指针和解引用运算符*

接下来试验一下直接将解引用操作符*直接用在智能指针Box上:

例2:

1
2
3
4
5
6
7
8
fn main() {
    let mut y = Box::new(5);
    if *y == 5 {
        println!("{:p} {}", y, *y); // 0x7fe61cc05e30 5
    }
    *y = 10;
    println!("{:p} {}", y, *y); // 0x7fe61cc05e30 10
}

例2代码变量y是一个智能指针Box<i32>类型,它指向了堆上的数值5。第3行直接对y进行解引用得到其指向的值,再去判断其是否和数值5相等。 第6行对y进行解引用得到其指向的值,并将值修改为10。从例2可以看出,智能指针Box<T>即可以被当做不可变引用使用,也可以被当做可变引用来使用。

进一步试验将解引用操作符*直接用在智能指针Rc上:

例3:

1
2
3
4
5
6
7
8
use std::rc::Rc;
fn main() {
    let y = Rc::new(5);
    if *y == 5 {
        println!("{:p} {}", y, *y);
    }
    // *y = 10; // trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::rc::Rc<i32>`
}

例3代码中第4行,直接对y进行解引用得到其指向的值,再去判断其是否和数值5相等,说明可以将Rc<T>当做不可变引用使用。第7行注释掉的代码如果去掉注释的话,会报trait DerefMut is required的编译错误。 从例3可以看出,智能指针Rc<T>只能被当做不可变引用使用,即Rc只是一个只读的引用计数器。

去查看文档,发现Box<T>既实现了std::ops::Deref,也实现了std::ops::DerefMutRc<T>只实现了std::ops::Deref

3.Deref trait

Deref用于不可变引用的解引用操作,如*v。 实现Deref trait允许我们重载不可变引用的解引用运算符*。实现了Deref trait的智能指针可以被当做常规引用来对待,为智能指针实现Deref trait以便于访问其背后的数据。

Deref trait的定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#[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;
}

Deref trait中的关联类型Target是解引用操作的结果的类型,deref方法实现具体的接引用操作。

3.1 函数和方法传参时的Deref强制转换

Rust为了提高在函数或方法传参时的便利性,提供了 Deref强制转换(Deref coercion) 功能,如果类型T实现了 Deref<Target = U> trait,并且xT的一个变量,那么:

  • 在不可变上下文中,*x等同于*Dref::deref(&x)。(注意T即不能是引用,也不能是裸指针)
  • 类型&T的值会被强制转换为&U的值。
  • 类型T隐含地实现了类型U的所有(不可变的)方法。

上面是Deref文档中对"Deref强制转换"的描述,理解起来有点费劲。其实就是在函数或方法传参时,实现了Deref的类型的引用会可以转换为通过Deref所能转换的类型的引用。当这种特定类型的引用作为形参传递给和形参类型不同的函数或方法时,Deref强制转换将自动发生。 就是说形参和实参不匹配时,会有一系列的deref方法被调用,会把我们的类型转换成参数所需的类型。

来看下面的例子。

例4:

1
2
3
4
5
6
7
8
fn hello(s: &str) {
    println!("hello {}", s);
}

fn main() {
    let x = Box::new(String::from("world"));
    hello(&x); // hello world
}

因为有"Deref强制转换"功能,上面例4中就可以使用Box<String>的引用传参调用hello函数。

  • Box<String>实现了Deref trait,Deref的关联类型Target为Stringderef实现返回的是&String。而String类型也实现了Deref trait,Deref trait关联类型为strderef实现返回的是&str
  • &Box<String>类型的实参传递给hello函数&str类型的实参时,“Deref强制转换会隐式调用2次deref函数,完成参数传递的类型转换。

例4的等同于下面调用2次deref函数的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::ops::Deref;

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

fn main() {
    let x = Box::new(String::from("world"));
    let s: &String = Deref::deref(&x);
    let s: &str = Deref::deref(s);
    hello(s);
}

如果没有Deref强制转换,我们自己编写的例4的代码可能是下面这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn hello(s: &str) {
    println!("hello {}", s);
}

fn main() {
    let x = Box::new(String::from("world"));
    let s: String = *x; // 将Box<String>解引用为String
    let s: &str = &s[..]; // 使用`&`和`[..]`获取整个String的字符串切片
    hello(s);
}

可以看出"Deref强制转换"的加入使得Rust程序员编写函数和方法调用代码时时无需增加过多的显式使用&*的引用和解引用。这个功能便于编写同时作用于引用或智能指针的代码,例4中的hello函数就是这样的。

当函数或方法传参所涉及到的类型实现了Deref trait,Rust在编译时会分析这些类型,并可以使用任意多次Deref::deref调用以获得匹配参数的类型。因为是发生在编译时,所以"Deref强制转换"并没有任何运行时损耗。

4.DerefMut trait

DerefMut用于可变引用的解引用操作,如*v = 1;。 实现DerefMut trait允许我们重载可变应用的解引用运算符*。实现了DerefMut trait的智能指针可以被当做常规可变引用来对待。

当前DerefMut trait的定义如下:

1
2
3
4
5
6
7
8
#[lang = "deref_mut"]
#[doc(alias = "*")]
#[stable(feature = "rust1", since = "1.0.0")]
pub trait DerefMut: Deref {
    /// Mutably dereferences the value.
    #[stable(feature = "rust1", since = "1.0.0")]
    fn deref_mut(&mut self) -> &mut Self::Target;
}

DerefMut tait继承了Deref trait,所以DerefMut也能参与"Deref强制转换功能”。

如果T实现了DerefMut<Target = U>,并且x是一个T类型的变量,那么:

  • 在可变上下文中,*x等同于*DerefMut::deref_mut(&mut x)。(注意T即不能是引用,也不能是裸指针)
  • 类型为&mut T的值会被强制转换为类型为&mut U的值
  • 类型T隐含地实现了类型U的所有(可变)方法

当Rust发现类型和trait实现满足以下三种情况时会进行Deref强制转换:

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

参考