Rust常用特型之Deref和DerefMut特型

在Rust标准库中,存在很多常用的工具类特型,它们能帮助我们写出更具有Rust风格的代码。

你可以通过在你的类型上实现std::ops::Derefstd::ops::DerefMut特型来自定义解引用操作例如*操作符和.操作符的行为。像Box<T>Rc<T>实现了这两个特型,所以他们的行为就像Rust内置指针类型一样。例如,如果你有一个Box<Complex>类型的值b,此时,*b 代表的是b指向的那个复数值。b.re代表该复数的实部.

如果对引用赋值或者借借出一个可变引用,那么Rust使用DerefMut特型。否则,只读就足够了,此时会使用Deref特型。

这两个特型的定义类似如下:

rust 复制代码
trait Deref {
  type Target: ?Sized;
  fn deref(&self) -> &Self::Target;
}
trait DerefMut: Deref {
  fn deref_mut(&mut self) -> &mut Self::Target;
}

derefderef_mut函数都使用&self引用作为参数并且返回一个&Self::Target的引用。 Target是该类型所拥有的,包含的或者指向的一个具体变量类型(显然,它不可能把它不知道的东西借出去,例如另一个结构体中的字段)。例如对Box<Complex>来说,这里的Target就是Complex。注意DerefMut拓展了Deref,这时显而易见的,如果你能解引用并修改它,那么你肯定能借出一个共享的引用而不修改。因为函数返回的引用的生命周期和&Self相同(生命周期三原则),只要函数返回的引用一直存在,那么Self本身就一直被借用。其实这里说的是只要是&Self有效,那么函数得到的引用就一直有效。

DerefDerefMut特型同时还扮演了其它角色。由于deref函数拿走了一个&Self引用但是返回了一个&Self::Target引用,Rust可以使用它来进行自动引用转换,将&Self转换成&Self::Target。换句话说,如果插入一个deref函数能消除类型不匹配的语法错误,Rust会自动为你插入。DerefMut也可以作相应的转换,只不过是可变引用。这个功能叫着deref coericoins(强制解引用),一个类型被强制表现为另一种类型。

尽管你也可以自己写一个类似强制转引用的功能,但是它们很方便:

  • 如果你有一个Rc<String>类型的值r,你想在它上面应用String::find函数,你可以简单的写成r.find(?),而不是(*r).find(?)。这是因为这个find函数调用隐式的借用r, 而Rc<T>实现了Deref<Target=T>,因此&Rc<String> 可以被解引用为&String.其实这里就是智能指针的应用(可以把智能指针当成普通内置指针使用)

  • 你可以在String上使用split_at函数,虽然该函数是定义在str字符串切片类型上的,因为String实现了Deref<Target=str>String并不需要重新实现split_at函数,因为你可以从&String强制解引用得到&str。 这里其实是Rust中的一个便利性,我们可以在非常底层的数据结构上定义函数,然后其它结构可以直接使用。最常见的就是Vec<T>,其绝大部分函数是定义在切片[T]上的,但是因为强制解引用,你可以非常简单的直接在向量上使用这些函数。我们看一下代码直观就更明显:

    rust 复制代码
    impl str {
        pub const fn len(&self) -> usize {
            self.as_bytes().len()
        }
      	
      	pub const fn is_empty(&self) -> bool {
            self.len() == 0
        }
    }

    我们可以看到,len及is_empty函数都是定义在str上的,其参数为&self,因此调用发生时,其实相当于是(&str).len。虽然我们这里是(&String).len(),但是由于自动解引用的存在,Rust会为我们自动插入deref函数,将&String转换成&str

  • 如果你有一个字节向量v(类型为Vec<u8>),而你又想将它传递为参数为字节切片&[u8]的函数,你可以直接传递&v即可。因为Vec<T>实现了Deref<Target=[T]>。这里和我上面刚说的定义在切片上的函数还有些不同,那个是解引用操作的,这里是函数参数传递,场景不一样。

综上所述,强制解引用主要应用在三个场合:

  1. 智能指针,让智能指针变得和内置指针一样
  2. 共用函数定义,在底层结构上定义函数从而让其它类型直接使用。
  3. 函数多态,例如函数参数为&[T],那么任何实现了Deref<Target=[T]>的类型都可以直接将引用传递给参数。

如果需要,Rust会执行多重链式强制解引用 。例如你在Rc<String>上调用split_at函数,首先,将它&Rc<String>解引用为&String,接下来再将&String解引用为&str,这正是split_at函数的拥有类型。

例如,如果你的类型如下所示:

rust 复制代码
struct Selector<T> {
  elements: Vec<T>,
  current: usize
}

虽然这里的类型涉及到了泛型,但相当简单。你的结构体包含了一个类型T的向量和一个记录当前位置的索引(current)。而这个结构体的功能就是实现一个指向当前元素的指针行为。

为了实现这个需求,Selector类型需要实现DerefDerefMut特型。

rust 复制代码
use std::ops::{Deref, DerefMut};

impl<T> Deref for Selector<T> {
  type Target = T;
  fn deref(&self) -> &T {
    &self.elements[self.current]
  }
}

impl<T> DerefMut for Selector<T> {
  fn deref_mut(&mut self) -> &mut T {
    &mut self.elements[self.current]
  }
}

代码相当简单了,我们就不说了。那怎么应用它呢?

rust 复制代码
let mut s = Selector { elements: vec!['x', 'y', 'z'],current: 2 };
*s = 'w';
assert_eq!(s.elements, ['x', 'y', 'w']);

这里s定义为mut的,这是因为我们要借用一个&mut T并修改它的值。

DerefDerefMut特型被设计用来实现实现智能指针类型,例如Box,Rc,Arc等,它还用于某些特定类型。这些类型拥有其它类型的值,但是需要频繁通过引用来使用它内部拥有的其它类型的值。例如Vec<T>String分别拥有[T]str。当你只有让Target的方法自动出现在你的类型上这么一个目的时(正如c++中基类的方法自动出现在子类中),你不应该为它设计实现DerefDerefMut特型。它有可能并是总是像你期望的那样工作,并且滥用也容易导致困惑。

具体困惑是什么呢?下面提到了一点。

自动解引用功能有时会随着警告,这或许会让你感到不解。因为Rust虽然使用自动解引用来解决类型冲突,但是并不包含类型参数的条件绑定。例如,下面的代码工作的很好:

rust 复制代码
let s = Selector { elements: vec!["good", "bad", "ugly"],
current: 2 };
fn show_it(thing: &str) { println!("{}", thing); }
show_it(&s);

这是因为我们的Selector实现了Deref<Target=T>,而在上面使用场景的第三点函数多态时我们知道,传入&s会自动执行deref函数从而转换成为&str .这里稍微有一点混乱。(这里是书中写的,稍微有一点误导)。

真实的事情是这样的:

首先,s的类型为Selector<&str>(书中也是这么写的),那么这里的T应该是&str而不是str。所以它实现的是Deref<Target=&str>而不是Deref<Target=str>(书中是这样写的,未知原因)。所以书中写的相当于show_it(s.deref())这里deref函数返回的类型为&&str。但是传入函数是没有问题的,原因呢?估计是源码中会再进行自动解引用 ,将&&T解成&T

经过查询相关资料,Rust Course《Rust语言圣经》上是这样说的,Rust会 引用 归一化,也就是把&&&v当成&v。所以这里&&str实际上转成了&str,因此传入函数是没有问题的。实际上,我的猜想是对的。看下面标准库源码,

rust 复制代码
impl<T: ?Sized> Deref for &T {
    type Target = T;

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

它为&T实现了Deref<Target=T>,虽然这里不拥有T,但是指向了T。(见前面特型定义)。所以&&T会自动解引用为&T。这么一来,s.deref()返回的&&str又被解引用成为了&str,最终满足函数参数的类型。

所以这里书中讲的稍微不对,漏了一点东西。而我们一定不放过,要有打破沙锅问到底的精神。

接下来讲,如果你把show_it改成一个泛型函数,而类型参数T的限定为<T:Display>,那么问题来了,

rust 复制代码
use std::fmt::Display;
fn show_it_generic<T: Display>(thing: T) { println!("{}", thing);
}
show_it_generic(&s);

编译器会告诉你Selector<&str>没有实现std::fmt::Display,虽然&str实现了。

这里就是让人困惑的地方,Selector<&str>不强制解引用为&str了么?怎么还会有错误?

实际上,你传递的参数类型为&Selector<&str>,而函数的参数为&T,所以T必须是Selector<&str>。其实这里是这样的,T的类型是

Selector<&str>没有错的。可能这里需要涉及到Display的相关内容。这里暂时略过。

Rust会检查T是否满足Display约束,因为在检查约束时并不会应用强制解引用,显然是无法通过检查的。

解决办法也很简单,一是使用as操作符来手动解引用, show_it_generic(&s as &str);;另一种是按编辑器所建议,使用

show_it_generic(&*s);,这里因为有了*操作,所以会进行自动解引用操作得到&str,然后再加上前面的&就形成了&str,从而满足条件T:Display.

这里有一点点不解的是,明明我们传递的是&s,为什么T的类型为Selector<&str>呢?

经过研究,个人认为:其实T的类型仍然为&Selector<&str>,我们它把简化为&U:Display 而,&U:Display的条件为 U:Display,因此给出的错误提示为Selector<&str>未实现Display,这里我们仔细看这句话就会明白:

help: the trait std::fmt::Display is not implemented for Selector<&str>, which is required by &Selector<&str>: std::fmt::Display

我们的函数参数检查 后面半句,但是后面半句需要的条件为前面半句,因为所以书中讲的还是有一些误导(或者简化)。

我们可以在函数体中增加如下代码:println!("type is {}", std::any::type_name::<T>()),它用来打印类型名称,当我们修改代码使用&s as &str时,打印出来的类型为&str而不是str,这进一步印证了我的猜想。

这里还发现了上面表述的一点不对,如果我们使用&*s,打印出来的T的类型为&&str,奇怪吧。因为*号操作符相当于调用deref函数(不是普通引用的解引用时的*,那里的操作是获取引用指向的值),所以*s得到的是&str,你不会得到str,因为str是无固定大小类型的,编译通不过。*s再加一个&就得到了&&str。它是否满足Display条件呢,Rust会移除&&直接检查str,发现它是满足的。标准库有代码如下:

rust 复制代码
impl Display for str {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.pad(self)
    }
}

注意,这里没有使用归一化,它没有插入deref函数将&&str变成&str,因为它只是检查条件。而Rust又自动实现了

impl<T> Display for &T where T:Display + ?Sized 。所以只要str实现了Display,不管前面加多少个&&都是实现了的。这也正是前面 the trait std::fmt::Display is not implemented for Selector<&str>, which is required by &Selector<&str>: std::fmt::Display 的原因,我们可以直接略去前面所有的&&而直接检查T是否实现了。那么有没有&T实现了Display而T没有实现的呢?当然没有,绝对不可能,看上面的实现:``impl Display for &T where T:Display + ?Sized ,很显然。

相关推荐
q5673152315 分钟前
在 Bash 中获取 Python 模块变量列
开发语言·python·bash
许野平40 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
也无晴也无风雨44 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
狂奔solar1 小时前
yelp数据集上识别潜在的热门商家
开发语言·python
blammmp1 小时前
Java:数据结构-枚举
java·开发语言·数据结构
何曾参静谧2 小时前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++
暗黑起源喵2 小时前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong2 小时前
Java反射
java·开发语言·反射
Troc_wangpeng2 小时前
R language 关于二维平面直角坐标系的制作
开发语言·机器学习
努力的家伙是不讨厌的2 小时前
解析json导出csv或者直接入库
开发语言·python·json