在Rust标准库中,存在很多常用的工具类特型,它们能帮助我们写出更具有Rust风格的代码。
你可以通过在你的类型上实现std::ops::Deref
和std::ops::DerefMut
特型来自定义解引用操作例如*
操作符和.
操作符的行为。像Box<T>
和Rc<T>
实现了这两个特型,所以他们的行为就像Rust内置指针类型一样。例如,如果你有一个Box<Complex>
类型的值b
,此时,*b
代表的是b
指向的那个复数值。b.re
代表该复数的实部.
如果对引用赋值或者借借出一个可变引用,那么Rust使用DerefMut
特型。否则,只读就足够了,此时会使用Deref
特型。
这两个特型的定义类似如下:
trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
deref
和deref_mut
函数都使用&self
引用作为参数并且返回一个&Self::Target
的引用。 Target是该类型所拥有的,包含的或者指向的一个具体变量类型(显然,它不可能把它不知道的东西借出去,例如另一个结构体中的字段)。例如对Box<Complex>
来说,这里的Target
就是Complex
。注意DerefMut
拓展了Deref
,这时显而易见的,如果你能解引用并修改它,那么你肯定能借出一个共享的引用而不修改。因为函数返回的引用的生命周期和&Self
相同(生命周期三原则),只要函数返回的引用一直存在,那么Self
本身就一直被借用。其实这里说的是只要是&Self
有效,那么函数得到的引用就一直有效。
Deref
和DerefMut
特型同时还扮演了其它角色。由于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]
上的,但是因为强制解引用,你可以非常简单的直接在向量上使用这些函数。我们看一下代码直观就更明显: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]>
。这里和我上面刚说的定义在切片上的函数还有些不同,那个是解引用操作的,这里是函数参数传递,场景不一样。
综上所述,强制解引用主要应用在三个场合:
- 智能指针,让智能指针变得和内置指针一样
- 共用函数定义,在底层结构上定义函数从而让其它类型直接使用。
- 函数多态,例如函数参数为
&[T]
,那么任何实现了Deref<Target=[T]>
的类型都可以直接将引用传递给参数。
如果需要,Rust会执行多重链式强制解引用 。例如你在Rc<String>
上调用split_at
函数,首先,将它&Rc<String>
解引用为&String
,接下来再将&String
解引用为&str
,这正是split_at
函数的拥有类型。
例如,如果你的类型如下所示:
struct Selector<T> {
elements: Vec<T>,
current: usize
}
虽然这里的类型涉及到了泛型,但相当简单。你的结构体包含了一个类型T的向量和一个记录当前位置的索引(current)。而这个结构体的功能就是实现一个指向当前元素的指针行为。
为了实现这个需求,Selector
类型需要实现Deref
和DerefMut
特型。
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]
}
}
代码相当简单了,我们就不说了。那怎么应用它呢?
let mut s = Selector { elements: vec!['x', 'y', 'z'],current: 2 };
*s = 'w';
assert_eq!(s.elements, ['x', 'y', 'w']);
这里s
定义为mut
的,这是因为我们要借用一个&mut T
并修改它的值。
Deref
和DerefMut
特型被设计用来实现实现智能指针类型,例如Box
,Rc
,Arc
等,它还用于某些特定类型。这些类型拥有其它类型的值,但是需要频繁通过引用来使用它内部拥有的其它类型的值。例如Vec<T>
和String
分别拥有[T]
和str
。当你只有让Target
的方法自动出现在你的类型上这么一个目的时(正如c++中基类的方法自动出现在子类中),你不应该为它设计实现Deref
和DerefMut
特型。它有可能并是总是像你期望的那样工作,并且滥用也容易导致困惑。
具体困惑是什么呢?下面提到了一点。
自动解引用功能有时会随着警告,这或许会让你感到不解。因为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
,因此传入函数是没有问题的。实际上,我的猜想是对的。看下面标准库源码,
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>
,那么问题来了,
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 forSelector<&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
,发现它是满足的。标准库有代码如下:
impl Display for str {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.pad(self)
}
}
注意,这里没有使用归一化,它没有插入deref
函数将&&str
变成&str
,因为它只是检查条件。而Rust又自动实现了文章来源:https://www.toymoban.com/news/detail-858473.html
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
,很显然。文章来源地址https://www.toymoban.com/news/detail-858473.html
到了这里,关于Rust常用特型之Deref和DerefMut特型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!