Rust常用特型之Deref和DerefMut特型

这篇具有很好参考价值的文章主要介绍了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特型。

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

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]上的,但是因为强制解引用,你可以非常简单的直接在向量上使用这些函数。我们看一下代码直观就更明显:

    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函数的拥有类型。

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

struct Selector<T> {
  elements: Vec<T>,
  current: usize
}

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

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

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并修改它的值。

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

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

自动解引用功能有时会随着警告,这或许会让你感到不解。因为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 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,发现它是满足的。标准库有代码如下:

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 ,很显然。文章来源地址https://www.toymoban.com/news/detail-858473.html

到了这里,关于Rust常用特型之Deref和DerefMut特型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Rust常用加密算法

    哈希运算(以Sha256为例) main.rs : Cargo.toml : 输出为: 6d65924d8e0580b9ac04d13da91c74c3ae28b08b4be4634ae06e647f42a88913 可以在线比对验证一下 验证数据完整性(使用HMAC) MAC(Message Authentication Code,消息认证码算法)是含有密钥散列函数算法,兼容了MD和SHA算法的特性,并在此基础上加上了密钥。

    2024年02月11日
    浏览(29)
  • Rust 常用集合(上)

    目录 1、使用 Vector 储存列表 1.1 新建 vector 1.2 更新 vector 1.3 读取 vector 的元素 1.4 遍历 vector 中的元素 1.5 使用枚举来储存多种类型 1.6 丢弃 vector 时也会丢弃其所有元素 2、使用字符串储存 UTF-8 编码的文本 2.1 什么是字符串? 2.2 新建字符串 2.3 更新字符串 2.3.1 使用 pus

    2024年02月03日
    浏览(32)
  • Rust vs Go:常用语法对比(六)

    题图来自 [1] 101. Load from HTTP GET request into a string Make an HTTP request with method GET to URL u, then store the body of the response in string s. 发起http请求 res has type *http.Response. buffer has type []byte. It is idiomatic and strongly recommended to check errors at each step. GET response: 200 Hello Inigo Montoya or or 102. Load from H

    2024年02月15日
    浏览(46)
  • Rust腐蚀服务器常用参数设定详解

    大家好我是艾西,一个做服务器租用的网络架构师上期我们分享了rust腐蚀服务器的windows系统搭建方式,其中启动服务器bat参数因为涉及的东西比较多所以想通过这篇文章给大家做一下详细的分享。 (注本文中xxxx即为http、xxxxx即为https、zzz即为www 因PT原因望大家理解) 服务器

    2024年04月14日
    浏览(38)
  • Rust vs Go:常用语法对比(五)

    题图来自 Rust vs Go 2023 [1] 81. Round floating point number to integer Declare integer y and initialize it with the rounded value of floating point number x . Ties (when the fractional part of x is exactly .5) must be rounded up (to positive infinity). 按规则取整 2.71828 3 82. Count substring occurrences 统计子字符串出现次数 1 Disjoint ma

    2024年02月15日
    浏览(46)
  • Rust vs Go:常用语法对比(三)

    题图来自 When to use Rust and when to use Go [1] 41. Reverse a string 反转字符串 输出 or 输出 ❤ roma tis rölod müspi mérol 42. Continue outer loop Print each item v of list a which in not contained in list b. For this, write an outer loop to iterate on a and an inner loop to iterate on b. 打印列表a中不包含在列表b中的每个项目

    2024年02月16日
    浏览(39)
  • Rust vs Go:常用语法对比(十)

    题图来自 Rust vs. Golang: Which One is Better? [1] 182. Quine program Output the source of the program. 输出程序的源代码 输出: 另一种写法: //go:embed 入门 [2] Quine 是一种可以输出自身源码的程序。利用 go:embed 我们可以轻松实现 quine 程序: 输出: or 输出: fn main(){print!(\\\"{},{0:?})}}\\\",\\\"fn main(){pri

    2024年02月15日
    浏览(36)
  • Rust vs Go:常用语法对比(二)

    21. Swap values 交换变量a和b的值 输出 a: 10, b: 3 or 输出 22. Convert string to integer 将字符串转换为整型 or 输出 or 输出 or 输出 23. Convert real number to string with 2 decimal places Given a real number x, create its string representation s with 2 decimal digits following the dot. 给定一个实数,小数点后保留两位小数

    2024年02月16日
    浏览(42)
  • Rust vs Go:常用语法对比(九)

    题图来自 Golang vs Rust - The Race to Better and Ultimate Programming Language 161. Multiply all the elements of a list Multiply all the elements of the list elements by a constant c 将list中的每个元素都乘以一个数 [4.0, 7.0, 8.0] 162. Execute procedures depending on options execute bat if b is a program option and fox if f is a program optio

    2024年02月15日
    浏览(36)
  • Rust vs Go:常用语法对比(十三)

    题图来自 Go vs. Rust: The Ultimate Performance Battle 241. Yield priority to other threads Explicitly decrease the priority of the current process, so that other execution threads have a better chance to execute now. Then resume normal execution and call function busywork . 将优先权让给其他线程 After Gosched, the execution of the current gorout

    2024年02月15日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包