Rust 从 0 到 1- 智能指针 -Deref trait
实现 Deref trait 使我们可以自定义解引用操作符的行为(*,dereference operator,不是乘法运算符或通配符)。实现了 Deref trait 的智能指针可以被看作普通的引用,适用于引用的代码同样也可以作用于智能指针。
下面我们首先看看解引用操作对于普通的引用时如何工作的,接着尝试定义一个类似 Box<T> 的类型,并分析在我们新定义的类型中解引用运算符为什么不能像普通引用一样工作。我们会讨论如何实现 Deref trait 才能使得智能指针以类似普通引用的方式工作。最后,我们会讨论 Rust 中的强制隐式转换(deref coercions)功能以及它是如何让我们可以不用关心它是普通引用或智能指针的。
解引用操作
普通的引用是一个指针,我们可以把指针理解为指向储存在某处值的箭头。下面的例子中,我们创建了一个 i32 类型值的引用,接着使用解引用运算符来访问其所引用的数据:
变量 x 存放了一个 i32 类型的值,5。y 是 x 的一个引用。我们可以断言 x 等于 5。然而,如果希望同样对 y 的值做出断言,必须使用 * 来访问引用所指向的值(即,dereference,解引用)。我们可以通过解引用 y,访问 y 所指向的整型值并将其与 5 做比较。
如果我们尝试对 y 直接进行断言:assert_eq!(5, y);,会得到类似下面的编译错误:
数值和数值的引用是不同的类型,在 Rust 中它们不允许直接比较,因此必须使用解引用操作获得引用所指向的值。
解引用 Box<T>
如果我们使用 Box<T> 重写上面的例子,也一样可以使用解引用操作,参考下面的例子:
上面的例子中 y 的值为一个 box 实例,它指向 x 值的拷贝 ,即存储在堆上的 5。在断言中,我们可以像解引用普通引用一样,通过解引用来获得 box 所指向的值。下面我们将通过实现一个类似 box 的智能指针类型来讨论 Box<T> 为什么可以做到这一点。
自定义智能指针
让我们通过创建一个类似标准库中 Box<T> 类型的智能指针,切身体会下智能指针与普通引用的不同。接着我们将介绍如何为其增加解引用的能力。
Box<T> 实际上就是一个包含一个元素的元组结构体,因此我们也使用同样的方式创建我们自定义的 MyBox<T> 类型,包括定义于 Box<T> 的 new 函数:
MyBox 是一个包含 T 类型元素的元组结构体。MyBox::new 函数获取一个 T 类型的参数并返回一个包含传入值的 MyBox 实例。将前面例子中的 Box<T>替换为我们自定义的 MyBox<T> 。尝试编译,将得到类似下面的错误:
MyBox<T> 不能被解引用,因为我们尚未为其实现这个功能。为了可以使用 * 操作符进行解引,我们需要实现 Deref trait。
实现 Deref trait
Deref trait,由标准库提供,要求实现 deref 方法,其仅包含一个参数,self 的引用(方法默认的第一个参数),并返回一个内部数据的引用。参考下面的例子:
type Target = T; 语法定义了 Deref trait 中使用的关联类型(associated type)。现在我们先忽略它,后面的章节会进行详细的讨论。deref 方法中通过 &self.0 返回了 MyBox 中存储的值的引用。现在我的例子可以编译通过了,并且断言 assert_eq!(5, *y); 也可以通过!
没有 Deref trait 的话,编译器只能解引用使用 & 操作符的引用。deref 方法让编译器可以处理任何实现了 Deref trait 的值,它通过调用其 deref 方法得到如何进行解引用的 & 引用。即,当我们输入 *y 时,Rust 事实上在底层运行了如下代码:
Rust 会将 * 替换为先调用 deref 方法再进行解引用的操作,这让我们可以写出一致的代码而不用对实现了 Deref trait 的类型进行特殊的处理(手动调用 deref 方法)。
deref 方法返回值的引用,以及 * (y.deref()) 括号外边的仍然需要解引用的操作和所有权相关。如果 deref 方法直接返回值而不是其引用,所有权将从 self 移动走,而在我们前面的例子里或是大多数使用解引用操作的场景,我们并不希望获取所有权。
注意,我们在代码中使用 * 时, * 替换为先调用 deref 方法再进行解引用的操作,只会发生一次。因为对 * 的替换不会无限递归(我理解是,如果解引用以后的值所属类型也实现了 deref 方法,不会再调用其 deref 方法), 在上面的例子中 ,在 i32 类型的值时就终止了,其值就是 5,与 assert_eq!(5, *y); 中的值相匹配。
函数和方法的强制隐式转换
强制隐式转换(deref coercions)是 Rust 为函数或方法传参提供的一种便利。强制隐式转换只作用于实现了 Deref trait 的类型,它会将原类型转换为另一种类型的引用。举例来说,强制隐式转换可以把 &String 转换为 &str,这是因为 String 类型实现了 Deref trait ,并返回了 str 类型。当我们所传给函数或方法的参数值的引用类型与其定义所不同时,就会自动进行强制隐式转换。一系列的 deref 方法会被调用,把我们提供的参数值的类型转换成其所定义的类型。
强制隐式转换让我们可以减少在进行函数和方法调用时显式的进行引用和解引用。同时也让我们编写的代码可以同时作用于引用和智能指针。参考下面的例子,我们定义了一个参数为 &str 类型的函数:
我们可以使用字符串切片作为 hello 函数的参数,如 hello("Rust");。但是强制隐式转换使 MyBox<String> 类型的值也可以做为 hello 函数的参数,参考下面的例子:
上例中,&m 为 MyBox<String> 类型值的引用。因为 MyBox<T> 实现了 Deref trait,Rust 会通过调用 deref 方法将 &MyBox<String> 变为 &String。而标准库提供的 String 类型也实现了 Deref trait,并会返回字符串切片,因此,Rust 再次调用 deref 方法将 &String 变为 &str,这样就与 hello 函数的定义相匹配了。
如果 Rust 没有强制隐式转换机制,为了将 &MyBox<String> 类型的值传递给 hello 函数做为参数,我们需要编写类似下面的代码:
上面的代码中,我们首先通过 (*m) 将 MyBox<String> 解引用,获得 String 类型的值;然后通过 & 和 [..] 操作得到整个 String 类型值的字符串切片,从而与 hello 函数的定义相匹配。这些代码看上去更加难以阅读和理解。强制隐式转换使得 Rust 可以自动的处理这些转换,同时这些分析和转换都是发生在编译时的,因此不会在运行时带来任何额外的开销。
强制隐式转换与可变性
和我们使用 Deref trait 重载不可变引用的 * 操作类似,Rust 提供了 DerefMut trait 用于重载可变引用的 * 操作。
Rust 在类型和 trait 实现满足以下三种情况时会进行强制隐式转换:
&T 转换为 &U, T: Deref<Target=U> 。
&mut T 转换为 &mut U,T: DerefMut<Target=U> 。
&mut T 转换为 &U, T: Deref<Target=U> 。
前两种情况除了可变性之外是相同的:如果类型 T 实现了返回 U 类型的 Deref trait,则可以直接从 &T 转换为 &U(或是 &mut T 转换为 &mut U)。
第三个情况有所不同:Rust 会将可变引用转为不可变引用,但是反过来是不行的,也就是说不可变引用永远也不能转为可变引用。这是因为根据借用规则,如果有一个可变引用,其在作用里必须是唯一的,否则程序是无法编译通过的。将一个可变引用转换为不可变引用不存在打破这个规则的可能,但是将不可变引用转换为可变引用则可能打破这个规则。
版权声明: 本文为 InfoQ 作者【山】的原创文章。
原文链接:【http://xie.infoq.cn/article/0621fb9d79710dd8b3f09b937】。文章转载请联系作者。
评论