首页 > 其他分享 >Rust的Deref特征:让智能指针“透明”的关键

Rust的Deref特征:让智能指针“透明”的关键

时间:2024-02-27 23:57:55浏览次数:38  
标签:Box let deref Deref foo Rust 指针

除了上篇文章中介绍过的BorrowAsRef外,Rust中还有一个很常见的和引用相关的特征:Deref。不过,和BorrowAsRef两个特征不同,Deref其实是用于重载解引用运算符(也就是*)的特征;在为某个类实现了Deref特征后,对它使用*运算就会调用特征中重载的方法。

这篇文章不仅将介绍Deref特性,还将探讨Rust中一个极其重要的机制:Deref转换(Deref Coercion)。对于各领域的开发者来说,理解这一机制都大有裨益。

Deref:定义

Deref特征被用于解引用操作。实现了DerefDerefMut的类型被称为智能指针。通常,智能指针类型被用于改变所含值的所有权语义(如RcCow)或所含值的存储语义(如Box)。

Deref的定义很简单,只需要提供一个方法:

pub trait Deref {
    type Target: ?Sized;

    // Required method
    fn deref(&self) -> &Self::Target;
}

Deref 转换

如果Deref只有这点东西的话,看起来和AddNeg这样的纯运算符trait也没什么区别;但是,Deref特殊就特殊在Rust的一种机制:Deref转换。具体来说,Rust编译器会在许多时候隐式地使用Deref。看看下面的例子吧:

fn main() {
    let foo = Box::new(5i32);
    let bar: &i32 = &foo;
    println!("{}", bar);
}

请你花五秒钟阅读这段代码,然后告诉我它能不能正常编译?如果能,会输出什么?

答案是5,也就是被Box所包裹的值。这种情况看起来显然是不符合语法规则的:我们怎么能把一个类型为&Box<i32>的变量赋给&i32呢?其实,这就是Deref转换在悄悄发挥作用。回到我们的代码:

let bar: &i32 = &foo;

编译器注意到foo的类型Box<i32>i32不符合,但是Box<i32>实现了Deref<i32>;于是它尝试在foo上插入了Deref

let bar: &i32 = &(*(foo.deref()));

foo被执行了一次解引用后,类型由Box<i32>变为了i32,和bar的要求符合,于是编译没有失败,bar获得了foo中的值的引用。

回到Deref转换上来,根据Rust官方文档中的定义,如果一个类型T实现了Deref<Target = U>,那么对类型为T的变量v来说:

  • 在不可变的上下文中,*v相当于*Deref::deref(&v)
  • 类型为&T的值会被转换为&U
  • T隐式地实现了U中的所有方法(以&self为接收者)。

不管读者之前有没有意识到,其实我们已经无数次享受过Deref转换带来的便利了;例如split方法实现于str而不是String,但是我们仍然可以对String使用split

fn main() {
    let foo = String::from("Hello world");
    foo.split(" ").for_each(|s| println!("{s}"));
}

又或者first方法实现于[T]而不是Vec<T>,但我们仍然可以对Vec使用first

fn main() {
    let foo = vec![10, 20, 30];
    println!("{:?}", foo.first());
}

或者是最明显的:当我们在使用Mutex<T>的时候,调用lock方法之后返回的明明是一个类型为MutexGuard<T>的变量,我们却可以像使用T本身一样使用它:

use std::sync::{Mutex, MutexGuard};

fn main() {
    let foo = Mutex::new("hello world");
    let foo_guard: MutexGuard<&str> = foo.lock().unwrap();
    foo_guard.split(" ").for_each(|s| println!("{s}"));
}

除此以外,Deref转换也会连续进行,直到无法再继续Deref或匹配到正确的类型:

fn main() {
    let foo = Box::pin(String::from("Hello world"));  // foo: Pin<Box<String>>
    foo.split(" ").for_each(|s| println!("{s}"));
}

这段代码中,foo按照Pin<Box<String>> -> Box<String> -> String -> str的顺序连续进行,直到拥有split方法的str类型为止。

感谢Deref转换的存在,我们不需要写这样的代码:

fn main() {
    let foo = String::from("Hello world");
    &(*foo)[..].split(" ").for_each(|s| println!("{s}"));
}

个人认为,Deref转换这个机制的存在,使得Rust在保障安全性的同时,将语言的易用程度提高到了一个全新的高度,是Rust语言中我个人最喜欢的一颗语法糖。

实现Deref时需要注意的

Deref也不是万能妙具,不能也不该被随意滥用。Rust文档对实现Deref的行为提出了这样的警告:

Warning: Deref coercion is a powerful language feature which has far-reaching implications for every type that implements Deref. The compiler will silently insert calls to Deref::deref. For this reason, one should be careful about implementing Deref and only do so when deref coercion is desirable. See below for advice on when this is typically desirable or undesirable.

警告:Deref 转换是一种强大的语言功能,对每个实现了 Deref 的类型都会造成深远的影响。编译器会默默插入对 Deref::deref 的调用。因此,在实现 Deref 时应小心谨慎,只有在需要 Deref 转换时才应该使用。请参阅下文,了解什么情况下需要或不需要使用 Deref。

文档也给出了这样的准则:何时可以实现Deref,何时不可以实现。

一般来说,如果出现以下情况,就应该实现Deref特性:

  1. 该类型的值与目标类型的值行为透明;
  2. 实现 deref 函数的成本较低;并且
  3. 该类型的用户不会对任何 deref 转换行为感到惊讶。

一般来说,如果出现以下情况,就不应该实现Deref特性:

  1. deref 实现可能意外失败;或
  2. 类型的某方法和目标类型的不一致;或
  3. 不希望将 deref 转换作为公共 API 的一部分。

AsRefBorrow的签名和Deref也很相似,在大多数情况下也需要同时实现它们中的一个或两个。

此外,Deref的实现在任何情况下都不应该失败,因为Deref转换的机制会使得这样的失败难以排查和定位。

最后,在介绍完Deref和Deref转换之后,也推荐大家看一下经典著作中对Deref的介绍;我对Rust了解不是很深入,对相关概念的介绍想必也肯定不如这些老师,因此建议大家还是延伸阅读一下:

  1. Deref 解引用 - Rust语言圣经(Rust Course)
  2. Treating Smart Pointers Like Regular References with the Deref Trait - The Rust Programming Language

标签:Box,let,deref,Deref,foo,Rust,指针
From: https://www.cnblogs.com/cinea/p/18038787

相关文章

  • 1 Rust初识
    Rust初识0.引言我学习Rust的初衷是为了开发WebAssembly,因为其的性能JavaScript快,而且可以编译成WebAssembly供浏览器使用。其实还有另一个原因,就是合我的专业(物联网应用开发)关联性很强,毕竟是要用到嵌入式开发的。加上我一直对像Java的编程语言,对于我来说,加上java的前......
  • [Rust] Cloning the value
    Followingcodehasborrowproblem:#[test]fnmain(){letvec0=vec![22,44,66];letvec1=fill_vec(vec0);assert_eq!(vec0,vec![22,44,66]);assert_eq!(vec1,vec![22,44,66,88]);}fnfill_vec(vec:Vec<i32>)->Vec<i......
  • [Rust] Specifying a function argument can be mutated
    Followingcodehascompileerror:#[test]fnmain(){letvec0=vec![22,44,66];letmutvec1=fill_vec(vec0);assert_eq!(vec1,vec![22,44,66,88]);}fnfill_vec(vec:Vec<i32>)->Vec<i32>{vec.push(88);vec}......
  • [Rust] Write macro
    Defineamacroanduseit:macro_rules!my_macro{()=>{println!("Checkoutmymacro!");};}fnmain(){my_macro!();} Noticethatyouhavetimedefine macrobeforemainfunction.Otherwiseitdoesn'twork. E......
  • Rust 无畏并发
    本文在原文基础上有删减,原文链接无畏并发。目录使用线程同时运行代码使用spawn创建新线程使用join等待所有线程结束将move闭包与线程一同使用使用消息传递在线程间传送数据信道与所有权转移发送多个值并观察接收者的等待通过克隆发送者来创建多个生产者共享状态并发互斥器......
  • [Rust] module with public and private methods
    Methods:modsausage_factory{//privatemethodfnget_secret_recipe()->String{String::from("Ginger")}//publicmethodpubfnmake_sausage(){get_secret_recipe();println!("sausage!&qu......
  • Rust开发日记
    Gettingstarted-RustProgrammingLanguage(rust-lang.org)  安装好配置环境变量Path:%CARGO_HOME%和%RUSTUP_HOME% 建立config文件,不要扩展名。[source.crates-io]registry="https://github.com/rust-lang/crates.io-index"#替换成你偏好的镜像源replace-......
  • 《安富莱嵌入式周报》第333期:F35战斗机软件使用编程语言占比,开源10V基准电源,不断电运
    周报汇总地址:http://www.armbbs.cn/forum.php?mod=forumdisplay&fid=12&filter=typeid&typeid=104 视频版:https://www.bilibili.com/video/BV1y1421f7ip目录:1、F35战斗机软件使用编程语言占比2、开源10V基准电源,不断电运行一年,误差小于1uV3、资讯(1)苹果开源配置语言Pkl......
  • 基于Rust的Tile-Based游戏开发杂记(01)导入
    什么是Tile-Based游戏?Tile-based游戏是一种使用tile(译为:瓦片,瓷砖)作为基本构建单位来设计游戏关卡、地图或其他视觉元素的游戏类型。在这样的游戏中,游戏世界的背景、地形、环境等都是由一系列预先定义好的小图片(即tiles)拼接而成的网格状结构。每个tile通常代表一个固定的尺寸区域,......
  • c++引用和指针
    指针和引用当我们需要在程序中传递变量的地址时,可以使用指针或引用。它们都可以用来间接访问变量,但它们之间有一些重要的区别。指针是一个变量,它存储另一个变量的地址。通过指针,我们可以访问存储在该地址中的变量。指针可以被重新分配,可以指向不同的变量,也可以为NULL。指针使用*......