给岁月以文明,而不是给文明以岁月

文章分类

关于博主

头像不见了

Rust中的解引用trait:Deref


/ 阅读 173

1、 std::ops::Deref 定义

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

定义很简单,只有一个deref方法,返回一个Target类型的引用。实现了Deref的类型在解引用(即*操作符)时会自动调用,所以一般情况下我们不会手动调用deref方法。

2、示例

我们定义了两个结构体,一个 MyBox<T>(T) 是一个一元元组结构体,一个 Rustacean是一个普通的有字段的结构体,接着分别为两个结构体实现Deref

MyBoxderef实现是返回结构体元组中的元素引用&self.0,而Rustaceanderef实现的是返回字段name的引用&self.name

use std::ops::Deref;

struct MyBox<T>(T);

struct Rustacean {
    name: String,
    age: u8,
}

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

impl Deref for Rustacean {
    type Target = String;
    fn deref(&self) -> &Self::Target {
        &self.name
    }
}

fn main() {
    let ref_int32 = &5i32;
    assert_eq!(5, *ref_int32);
    // assert_eq!(5, ref_int32);
    
    let box_4 = MyBox(4i32);
    println!("{}", *box_4);

    let r1 = Rustacean { name: "John".into(), age: 24u8};
    println!("{}", *r1);
}

//output: 4
//        John

声明一个MyBox变量box_4Rustacean变量r1,分别对两个变量使用解引用*并打印出解引用得到的值。

*作为解引用操作符和C语言类似,示例中ref_int32是一个i32的引用,对其解引用得到值 5,C语言中只能对一个指针使用解引用操作符,Rust中可以对引用类型 & 和实现了 Deref 的类型进行解引用。比如执行*box_4,发现box_4不是引用类型,但是它实现了Deref,所以会执行*(Deref::deref(&box_4)),得到 4

Deref多用于智能指针,所以一般只在只能指针类型上使用

3、解引用强制多态

Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:

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

比如接上面例子,声明变量name&String类型,因为实现了Rustacean : Deref<Target=String>,所以&Rustacean可以转换为&String。可以理解为做了一次Deref::deref(&Rustacean) -> &String

let name: &String = &r1;
println!("{}", name);
// John

fn print_name(name: &String) {
    println!("{}", name);
}

print_name(&r1);
// John

还有一点是当 T: Deref<Target=U> 时,相当于T隐式实现了U的不可变(即第一个参数是&self)方法。这点也好理解,因为U的不可变方法里面的&self的类型就是就是&U,而解引用强制多态&T->&U,所以对U能用的不可变方法对T都能用。

也即当在T类型变量上执行T类型不存在的方法fnxx()时,相当于执行(Deref::deref(&T)).fnxx(),当然这在编译器就能检查出调用方法的正确性。

例如Rustacean类型的r1能调用String类型的len()函数,返回 4("John"的长度)。

println!("{}", r1.len());
// 4

更详细的内容参考标准库std::ops::Deref文档

4、Unit类型的Deref

在学习Rust的过程宏的时候,阅读lazy_static!代码时,发现lazy_static是使用()类型的Deref实现的。

原理是当使用lazy_static!{ pub static ref USERNAME : String = String::from("John"); }时,宏会生成一个同名struct USERNAME;,而这个struct()类型的,()类型特殊之处在于它既是值也是类型,它的值和类型都是()。接着宏会为USERNAME实现Deref<Target=String>,而它的deref()函数保证多次调用只在第一次生成静态值,后面调用只是返回该静态值的引用。

这样在后面使用USERNAME时实际上使用的是struct USERNAME,不过由于它是()所以本身可以认为就是变量,所以它实现了String的所有不可变方法,就可以用USERNAME.len()了。形式上好像是在使用前面定义的静态变量,实际上不是。实际上是当调用用USERNAME.len(),会执行USERNAMEderef方法,返回内部的静态变量,这样就能保证在第一次用USERNAME时执行deref()时生成一次静态值,做到了lazy static。

lazy_static!函数宏会生成如下代码:

struct USRENAME;

impl std::ops::Deref for USERNAME {
    fn deref(&self) -> &String {
        static ONCE: std::sync::Once = std::sync::Once::new();
        static mut VALUE: *mut String = 0 as *mut String;

        unsafe {
            ONCE.call_once(|| VALUE = String::from("John"));
            &*VALUE
        }
    }
}

上面说的String类型可以换成任何实际的类型。