Deref 特性

在上一练习中,你其实没做太多工作,对吧?将

#![allow(unused)]
fn main() {
impl Ticket {
    pub fn title(&self) -> &String {
        &self.title
    }
}
}

修改为

#![allow(unused)]
fn main() {
impl Ticket {
    pub fn title(&self) -> &str {
        &self.title
    }
}
}

就是为了让代码编译通过并使测试成功。不过,你的脑海中或许会响起警钟。

看似不应行得通,却偏偏可行

让我们回顾一下事实:

  • self.title 是一个 String
  • 因此,&self.title 是一个 &String
  • 修改后的 title 方法输出的是 &str

你可能会期待编译错误,对吧?比如“预期 &String,发现 &str”之类的。然而,它就这么正常工作了。为何如此**?

Deref 特性来救援

Deref 特性是名为解引用强制(deref 强制)这一语言特性的背后机制。该特性在标准库的 std::ops 模块中定义:

#![allow(unused)]
fn main() {
// 我暂时简化了定义。
// 后面我们会看到完整定义。
pub trait Deref {
    type Target;
    
    fn deref(&self) -> &Self::Target;
}
}

type Target 是一个关联类型,它是实现特质时必须指定的具体类型占位符。

解引用强制

通过为类型 T 实现 Deref<Target = U>,你实际上是告诉编译器 &T&U 在某种程度上可以互换。具体而言,你会得到以下行为:

  • T 的引用会被隐式转换为对 U 的引用(即 &T 变为 &U
  • 你可以对 &T 调用所有在 U 上定义的接受 &self 作为输入的方法。

关于解引用操作符 * 还有一点,但我们目前不需要了解(如果好奇可以查阅std 的文档)。

String 实现了 Deref

String 通过 Target = str 实现了 Deref

#![allow(unused)]
fn main() {
impl Deref for String {
    type Target = str;
    
    fn deref(&self) -> &str {
        // [...]
    }
}
}

得益于这个实现和解引用强制,当需要时 &String 会自动转换为 &str

不要慎用解引用强制

解引用强制是一个强大的特性,但也可能导致混淆。自动类型转换会使代码更难读且难以理解。如果同一名称的方法既定义在 T 上也在 U 上,到底会调用哪个?

在课程的后续部分,我们将探讨解引用强制最安全的使用场景:智能指针。

参考资料

  • 本节的练习位于 exercises/04_traits/06_deref