Rust 语言学习之旅(6)

22-09-29 banq

智能指针
在本章中,我们将揭开智能指针的神秘面纱。 让我们探索一下能够让我们与最底层内存打交道的这些数据结构。 Ferris 说:“读完这一章之后,即使您觉得仍然不能编写管理底层内存的代码也不用觉得不知所措。 本章主要是向您介绍一些有用的工具并简要了解他们如何工作!”

引用本质上只是表示内存中某些字节起始位置的数字。 它唯一的目的就是表示特定类型的数据存在于何处。 引用与数字的不同之处在于,Rust 将验证引用自身的生命周期不会超过它指向的内容(否则我们在使用它时会出错!)。

指针
引用可以转换成一个更原始的类型,指针(raw pointer)。 像数字一样,它可以不受限制地复制和传递,但是Rust 不保证它指向的内存位置的有效性。 有两种指针类型:

  • *const T - 指向永远不会改变的 T 类型数据的指针。
  • *mut T - 指向可以更改的 T 类型数据的指针。

指针可以与数字相互转换(例如usize)。指针可以使用 unsafe 代码访问数据(稍后会详细介绍)。
内存细节:
  • Rust中的引用在用法上与 C 中的指针非常相似,但在如何存储和传递给其他函数上有更多的编译时间限制。
  • Rust中的指针类似于 C 中的指针,它表示一个可以复制或传递的数字,甚至可以转换为数字类型,可以将其修改为数字以进行指针数学运算。


fn main() {
    let a = 42;
    let memory_location = &a as *const i32 as usize;
    println!("Data is here {}", memory_location);
}

输出:
Data is here 140732857748572

解引用
访问或操作 由引用(例如&i32)指向的数据的过程称为解除引用。有两种方式通过引用来访问或操作数据:
  • 在变量赋值期间访问引用的数据。
  • 访问引用数据的字段或方法。

Rust 有一些强大的运算符可以让我们做到这一点。

运算符 *
* 运算符是一种很明确的解引用的方法。

let a: i32 = 42;
let ref_ref_ref_a: &&&i32 = &&&a;
let ref_a: &i32 = **ref_ref_ref_a;
let b: i32 = *ref_a;



内存细节:
  • 因为 i32 是实现了 Copy 特性的原始类型,堆栈上变量 a 的字节被复制到变量 b 的字节中。

fn main() {
    let a: i32 = 42;
    let ref_ref_ref_a: &&&i32 = &&&a;
    let ref_a: &i32 = **ref_ref_ref_a;
    let b: i32 = *ref_a;
    println!("{}", b)
}


运算符 .
.运算符用于访问引用的字段和方法,它的工作原理更加巧妙。

let f = Foo { value: 42 };
let ref_ref_ref_f = &&&f;
println!("{}", ref_ref_ref_f.value);

哇,为什么我们不需要在ref_ref_ref_f之前添加***?这是因为 . 运算符会做一些列自动解引用操作。 最后一行由编译器自动转换为以下内容。

println!("{}", (***ref_ref_ref_f).value);


除了能够使用&运算符创建对现有类型数据的引用之外, Rust 给我们提供了能够创建称为智能指针的类引用结构。我们可以在高层次上将引用视为一种类型,它使我们能够访问另一种类型. 智能指针的行为与普通引用不同,因为它们基于程序员编写的内部逻辑进行操作. 作为程序员的你就是智能的一部分。通常,智能指针实现了 Deref、DerefMut 和 Drop 特征,以指定当使用 * 和 . 运算符时解引用应该触发的逻辑。

use std::ops::Deref;
struct TattleTell<T> {
    value: T,
}
impl<T> Deref for TattleTell<T> {
    type Target = T;
    fn deref(&self) -> &T {
        println!("{} was used!", std::any::type_name::<T>());
        &self.value
    }
}
fn main() {
    let foo = TattleTell {
        value: "secret message",
    };
  // 取消引用立即发生在这里
     // 在 foo 被自动引用之后
     // 函数 `len`
    println!("{}", foo.len());
}


智能不安全代码
智能指针倾向于经常使用不安全的代码。如前所述,它们是与 Rust 中最低级别的内存进行交互的常用工具。什么是不安全代码? 不安全代码的行为与普通 Rust 完全一样,除了一些 Rust 编译器无法保证的功能。不安全代码的主要功能是解引用指针。 这意味着将原始指针指向内存中的某个位置并声明“此处存在数据结构!” 并将其转换为您可以使用的数据表示(例如将*const u8 转换为u8)。 Rust 无法跟踪写入内存的每个字节的含义。 因为 Rust 不能保证在用作 指针 的任意数字上存在什么,所以它将解引用放在一个 unsafe { ... } 块中。
智能指针广泛地被用来解引用指针,它们的作用得到了很好的证明。

fn main() {
    let a: [u8; 4] = [86, 14, 73, 64];
  // 这是一个原始指针。 获取内存地址
     // 作为数字的东西是完全安全的
    let pointer_a = &a as *const u8 as usize;
    println!("Data memory location: {}", pointer_a);

// 将我们的数字变成指向 f32 的原始指针是
     // 也很安全。
    let pointer_b = pointer_a as *const f32;
    let b = unsafe {
         // 这是不安全的,因为我们告诉编译器
         // 假设我们的指针是一个有效的 f32 并且
         // 将它的值取消引用到变量 b 中。
         // Rust 没有办法验证这个假设是否正确。
        *pointer_b
    };
    println!("I swear this is a pie! {}", b);
}