【Rust 笔记】11-实用特型

11 - 实用特型

特型简介
Drop解构函数。清除值时 Rust 自动运行的清除代码
Sized标记特型,针对编译时可以知道大小的类型(而不是像切片那样动态大小的类型)
Clone针对支持克隆值的类型
Copy标记特型,针对可以简单地对内存中包含的值进行逐字节复制来克隆的类型
DerefDerefMut智能指针类型的特型
Default针对有合理 “默认值” 的类型
AsRefAsMut转换特型,借用某种类型的引用
BorrowBorrowMut转换特型,类似 AsRefAsMut,但额外保证一致的散列、顺序和相等
FromInto转换特型,将某种类型的值转换为另一种类型
ToOwned转换特型,将引用转换为所有值

11.1-Drop

  • 当一个值的所有者离开时,Rust 会自动清除(Drop)这个值。清除值涉及释放相关的值、堆空间,以及该值拥有的系统资源。

  • Rust 标准特型 std::ops::Drop

    trait Drop {
        fn drop(&mut self);
    }
    
  • 清除会在各种条件下发生,包括变量超出作用域、表达式的值被 ; 操作符丢弃、截断向量时从末尾删除其元素,等等。

  • 针对自定义类型设计 Drop

    • 结构体和特型设计
    struct Appellation {
      name: String,
      nicknames: Vec<String>
    }
    
    trait Drop {
      fn drop(&mut self);
    }
    
    impl Drop for Appellation {
      fn drop(&mut self) {
        print!("Dropping {}", self.name);
        if !self.nicknames.is_empty() {
          print!(" (AKA {})", self.nicknames.join(", "));
        }
        println!("");
      }
    }
    
    • 测试代码:
    fn main() {
        let mut a = Appellation {
            name: "Zeus".to_string(),
            nicknames: vec!["cloud collector".to_string, "king of the gods".to_string()]
        };
        println!("before assignment");
        a.drop();
        a = Appellation {     // 第二次赋值后,第一次的赋值被清除
            name: "Hera".to_string(),
            nicknames:vec![]
        };
        println!("at end of block");
        a.drop();
    }                         // a离开作用域,第二次的赋值被清除
    
    • 结果输出
    before assignment
    Dropping Zeus (AKA cloud collector, king of the gods)
    at end of block
    Dropping Hera
    
  • 如果变量的值被转移到其他地方,导致变量在超出作用域时处于未初始化状态,Rust 则不会清除该变量,因为这个变量已经没有值需要清除了。此时,Rust 会用一个不可见的标志跟踪变量的状态,该标志表示变量的值是否需要被清除:

    let p;
    {
        let q = Appellation {
            name: "Cardamine hirsuta".to_string(),
            nicknames: vec!["shotweed".to_string(), "bittercress".to_string()]
        };
        if complicated_condition() {
            p = q;
        }
    }  // q的作用域在此处结束
    println!("Sproing!");
    // p的作用域在此处结束
    
  • 如果某类型实现了 Drop 特型,就不能再实现 Copy 特型。

    • 如果一个类型包含 Copy 特型,那么简单的字节对字节的赋值就会产生一个值的独立副本。
    • 在同一份数据多次调用同一个 drop 方法是错误的。
  • 标准前置模块中包含一个清除值的函数:

    fn drop<T>(_x: T) {  }
    
    • 接收一个参数的值,从调用者那里取得所有权,然后什么也不做。
    • Rust 会在超出作用域时,清除_x 的值,与清除其他变量的值一样。

11.2-Sized

  • 固定大小的类型(sized type):其值在内存中都具有相同大小。

    • 每个 u64 占 8 个字节。
    • 每个 (f32, f32, f32) 元组占 12 个字节。
    • 对于枚举类型,其值始终占能保存其最大变体的空间。
    • Vec<T> 拥有分配在堆上的大小可变的缓冲区,但是 Vec 值本身值包含一个指向该缓冲区的指针、缓冲区的容量及其长度。因此 Vec<T> 也是固定大小的类型。
  • 非固定大小类型(unsized type):其值在内存的大小并不固定。如 str[T] 类型表示大小不确定的值的集合,所以它们是非固定大小的。

    • 字符串切片类型 str(注意没有 &)是非固定大小的。字符串字面量 "diminutive""big" 是对 19 和 3 字节 str 切片的引用。
    • [T](同样没有 &)这样的数组切片类型也是非固定大小的,即共享引用 &[u8] 可以指向任意大小的 [u8] 切片。
  • 对特型目标的引用也是非固定大小类型。

    • 特性目标是一个指向实现了给定特型的某个值的指针。如 &str::io::WriteBox<std::io::Write> 都是指向了实现 Write 特型的某个值的指针。
    • 引用目标可能是一个文件、一个网络套接口,或者实现了 Write 的自定义类型。
    • 因为实现 Write 的类型是可以扩充的,所有 Write 作为类型被认为是非固定大小的,即其值的大小可变。
  • 结构体的最后一个字段可能是非固定大小的。此时,结构体本身也是非固定大小的。

    • Rc<T> 引用计数指针在内部被实现为一个指向私有类型 RcBox<T> 的指针,该类型用于保存类型 T 及其引用计数。

      struct RcBox<T: Sized> {
          ref_count: usize,
          value: T,  //
      }
      
    • value 字段的值是 T,对它保存的 Rc<T> 的引用计数。

    • Rc<T> 解引用为一个对这个字段的指针。

    • ref_count 字段保存引用计数。

  • 指向非固定大小值的指针都是胖指针,占两个字宽。

    • 胖指针既包含指向切片的指针,也包含切片的长度。
    • 特型目标也包含一个指向方法实现的虚拟表的指针。
  • 所有固定大小的类型都实现了 std::marker::Sized 特型,这个特型没有方法或关联类型。

    • Rust 为其适用的所有类型自动实现了这个特型。
    • 开发者不能自己实现。
  • 应用场景:绑定类型变量。

    • T: Sized 绑定,要求 T 必须是一个编译时大小已知的类型。
    • 这种特型称为标记特型(marker trait),可以将某些类型标记为具有关注的特征。
  • 大多数泛型变量默认被 Rust 限制为使用 Sized 类型。

    • struct S<T> {...} 会被 Rust 理解为 struct S<T: Sized> {...}
    • 如果开发者写成 struct S<T: ?Sized> {b: Box<T>}(其含义是 “不一定是 Sized”),那么 Rust 会允许使用 S<str>S<Write>,定义为胖指针;还允许使用 S<i32>S<String>,定义为普通指针。
  • 不知是否固定大小(questionably sized):类型变量具有 ?Sized 绑定,使得这个类型可能是 Sized,也可能不是。

11.3-Clone

  • std::clone::Clone 特型:适用于可以复制自身的类型。

    trait Clone: Sized {
        fn clone(&self) -> Self;
        fn clone_from(&mut self, source: &Self) {  // 将self修改为source的一个副本。
            *self = source.clone()
        }
    }
    
  • Rust 不自动克隆值,而是要求明确调用一个方法:

    • 克隆一个值通常涉及创建该值所拥有一切内容的副本及分配内存,因此 clone 无论在时间消耗还是内存占用,都可能比较昂贵。
    • 如克隆 Vec<String> 不仅仅要复制向量,还要复制其包含的所有 Sring 元素。
    • Rc<T>Arc<T> 这样的引用计数指针属于例外,克隆它们智慧简单地递增相应的引用计数,然后返回新指针。
  • Clone 特型的类型举例:许多执行复制操作的有意义的类型,都实现了 Clone

    • 原始类型 booli32
    • 容器类型 StringVec<T>HashMap
  • Clone 特型的类型举例:有些类型实现复制操作没有意义。

    • std::sync::Mutex
    • std::fs::File 在操作系统没有必要资源时复制会失败。但它提供了一个 try_clone 方法,这个方法返回可以报告错误的 std::io::Result<File>
  • 克隆必须万无一失。

11.4-Copy

  • 赋值会转移值,而不是复制值。转移值更有利于跟踪变量所拥有的资源。

  • 不拥有任何资源的简单类型可以是 Copy 类型,这种类型的赋值会生成值的副本,而不是转移值并让原始变量变成未初始化。

  • 如果类型实现了 std::marker::Copy 标记特型,那么它就是 Copy 类型。

    trait Copy: Clone { }
    
  • 自定义类型实现它也很简单:

    impl Copy for MyType {  }
    
  • 实现 Copy 的类型必须遵守的规则:

    • Rust 只允许类型在字节对字节的深度复制能满足要求的情况下实现 Copy
    • 那些可能拥有任意资源,比如堆缓冲区或操作系统勾柄的类型,不能实现 Copy
    • 任何实现 Drop 特型的类型不能是 Copy。如果一个类型需要特殊的清理代码,那就一定需要特殊的复制代码,因此不能是 Copy 类型。
  • 特型派生:

    • #[derive(Copy)] 让类型派生 Copy
    • #[derive(Clone, Copy)] 让类型同时派生 CloneCopy

11.5-DerefDerefMut

  • std::ops::Derefstd::ops::DerefMut 特型可以修改解引用操作符 *. 在自定义类型上的行为。

    • Box<T>Rc<T> 这样的指针类型实现了这两个特型。如果有一个 Box<Complex> 类型的值 b,那么 *b 引用的就是 b 指向 Complex 的值,而 b.re 引用的是其实数部分。
    • 上下文赋值,以及从引用目标借用可修改的引用,那么会使用 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 引用。
    • TargetSelf 包含、拥有或引用的资源。对于 Box<Complex> 来说,Target 的类型就是 Complex
    • DerefMutDeref 的扩展:如果可以解引用并修改资源,那么就可以借用一个对它的共享引用。
    • 这两个方法返回的引用具有与 &self 一样长的生命期,所以 self 会在返回引用的生命期内始终保持被借用。
  • 解引用强制转型(deref coercion):一种类型被 “强制” 表现出另一种类型的行为。比如实现 DerefMut 可以实现对可修改引用的类型转换。

    • 对于 Rc<String> 的值 r,如果要对它调用 String::find,那么可以简单地写作 r.find('?')。此时 &Rc<String> 被强制转换为 &String,因为 Rc<T> 实现了 Deref<Target=T>
    • 可以在 String 值上使用 Split_at 等方法,即使 Split_atStr 切片类型的方法,因为 String 实现了 Deref<Target=str>。此时 &String 强制转型为 &str
    • 如果字节向量 v,为它传递一个期待字节切片 &[u8] 的函数,那么可以将 &v 作为参数,因为 Vec<T> 实现了 Deref<Target=[T]>
    • Rust 不会尝试解引用强制转型去满足类型变量绑定。

11.6-Default

  • 有默认值的类型都实现了 std::default::Default 特型:

    trait Default {
      fn default() -> Self;
    }
    
  • StringDefault 的实现如下所示:

    impl Default for String {
      fn default() -> String {
        String::new()
      }
    }
    
  • Default 可以表示大量参数集合(大部分参数通常不需要改变)的结构体生成默认值。

  • 如果类型 T 实现了 Default,那么标准库会自动为 Rc<T>Arc<T>Box<T>Cell<T>RefCell<T>Cow<T>Mutex<T>RwLock<T> 实现 Default

    • 如果元组类型的所有元素类型都实现了 Default,且该元组类型也实现了 Default,那么这个元组默认会持有每个元素的默认值。
    • 如果结构体的所有字段都实现了 Default,则可以使用#[derive(Default)] 自动为结构体实现 Default
    • 任何 Option<T> 的默认值都是 None

11.7-AsRefAsMut

如果一个类型实现了 AsRef<T>,那么就可以向它借用一个 &T。即 AsRef 是共享引用,同样 AsMut 就是可修改引用。

trait AsRef<T: ?Sized> {
  fn as_ref(&self) -> &T;
}

trait AsMut<T: ?Sized> {
  fn as_mut(&mut self) -> &mut T;
}

11.8-BorrowBorrowMut

  • 如果一个类型实现了 Borrow<T>,那么它的 borrow 方法可以从自身有效地借用一个 &T。不同于 AsRef:只有当 &T 与它所借用的值具有相同的散列和比较特性时,一个类型才可以实现 Borrow<T>

    trait Borrow<Borrowed: ?Sized> {
      fn borrow(&self) -> &Borrowed;
    }
    
  • 应用场景:

    • 常用于处理散列表或树中的键。
    • 处理由于其他原因将被散列或比较的值。
  • String 实现了 AsRef<str>AsRef<u8>AsRef<Path>,但这三种目标类型通常会有不同的散列值。只有 &str 切片能保证与对应 的 String 有一样的散列化结果,因此 String 只实现了 Borrow<str>

11.9-FromInto

  • std::convert::Fromstd::convert::Into 特型表示类型转换,即消费一种类型的值,然后返回另一种类型的值。

    • AsRefAsMut 特型是从一种类型借用另一种类型的引用;
    • FromInto 则是取得它们参数的所有权,转换类型,再把结果的所有权返回给调用者。
    // 这两个特型的定义是对称的
    trait Into<T>: Sized {
      fn into(self) -> T;
    }
    trait From<T>: Sized {
      fn from(T) -> Self;
    }
    
  • 标准库中的每种类型 T 都实现了 From<T>Into<T> 特型。

  • 使用 Into 可以让然乎更灵活地接收参数:

    use std::net::Ipv4Addr;
    fn ping<A>(address: A) -> std::io::Result<bool> where A: Into<Ipv4Addr> {
      let ipv4_address = address.into();
      ...
    }
    
    • ping 函数可以接收 Ipv4Addr 作为参数,也可以接收 u32[u8; 4] 的数组。

    • u32[u8; 4] 的数组都实现了 Into<Ipv4Addr> 特型。

    • 调用上述特型:

      println!("{:?}", ping(Ipv4Addr::new(23, 21, 68, 141))); // 传入Ipv4Addr
      println!("{:?}", ping([66, 146, 219, 98]));  // 传入[u8; 4]
      println!("{:?}", ping(0xd076eb94_u32));      // 传入u32
      
  • From 只实现了 From<[u8; 4]>From<u32>

    let addr1 = Ipv4Addr::from([66, 146, 219, 98]);
    let addr2 = Ipv4Addr::from(0xd076eb94_u32);
    
  • 转换过程中可以使用原始值的资源来构建转换后的值。

    let text = "Beautiful Soup".to_string();
    let bytes: Vec<u8> = text.into();
    

11.10-ToOwned

  • Clone 实现了引用目标的克隆;

  • ToOwned 则实现了引用自身的克隆。它可以把引用转换为所有型的值:

    trait ToOwned {
      type Owned: Borrow<Self>; 
      fn to_owned(&self) -> Self::Owned;  // 可以返回能够借用为&Self的任何类型
    }
    
  • 可以从 Vec<T> 借用一个 &[T],因此 [T] 可以实现 ToOwned<Owned=Vec<T>>

    • str 实现了 ToOwned<Owned=String>
    • Path 实现了 ToOwned<Owned=PathBuf>

11.11-BorrowToOwned 实例

  • 一个函数到底应该按引用还是按值接收参数,在某些情况下在程序运行时才能决定到底是借用还是取得所有权更合适。针对此问题,std::borrow::Cow 可以实现写时克隆(clone on write):

    enum Cow<'a, B: ?Sized + 'a> where B: ToOwned {
      Borrowed(&'a B),
      Owned(<B as ToOwned>::Owned),
    }
    
    • Cow<B> 可以借用对 B 的一个共享引用;
    • 也可以拥有一个值,然后再借用这样的一个引用。
  • Cow 的用途:返回静态分配的字符串常量或者计算的字符串。

    • 将一个错误枚举转换为一条消息:大多数变体可以用固定大小的字符串来处理,但有些也需要在消息中包含额外的数据。可以返回一个 Cow<'static, str>

      use std::path::PathBuf;
      use std::borrow::Cow;
      fn describe(error: &Error) -> Cow<'static, str> {
        match *error {
          Error::OutOfMemory => "out of memory".into(),
          Error::StackOverflow => "stack overflow".into(),
          Error::MachineOnFire => "machine on fire".into(),
          Error::Unfathomable => "machine bewildered".into(),
          Error::FileNotFound(ref path) => {
            format!("file not found: {}", path.display()).into()
          }
        }
      }
      
    • describe 的调用者如果不需要修改返回值,可以把 Cow 当成一个 &str

      println!("Disaster has struck: {}", describe(&error));
      
    • 当需要一个所有型值的调用者,也可以很容易获取:

      let mut log: Vec<String> = Vec::new();
      ...
      log.push(describe(&error).into_owned());
      
    • Cow 可以把 descirbe 及其调用者得以把内存分配推迟到必要的时刻。


详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十三章
原文地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

phial03

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值