5分钟速读之Rust权威指南(四十一)高级类型

这一节我们介绍一些比较高级的类型特性,包括上一节讲到的newtype模式、类型别名、never类型、动态大小类型

使用newtype模式实现类型安全与抽象

上一节中我们使用newtype模式跳过了”孤儿规则“的限制,我们还可以使用newtype模式可以为类型的某些细节进行封装。例如,newtype可以暴露出一个与内部私有类型不同的公共API,从而限制用户可以访问的功能,下面实现一个只能添加成员的MyVec结构体:

struct MyVec<T>(Vec<T>);
impl<T> MyVec<T> {
  fn new() -> Self {
    MyVec(vec![])
  }
  // 只实现添加,不提供删除方法,所以不能删除
  fn push(&mut self, item: T) {
    self.0.push(item)
  }
}
let mut arr = MyVec::new();
arr.push(1);
arr.push(2);
arr.push(3);
复制代码

newtype模式还可以被用来隐藏内部实现。例如,我们可以提供People类型来封装一个用于存储人物ID及其名称的HashMap<u32,String>People类型的用户只能使用我们提供的公共API,比如一个添加名称字符串到People集合的方法,而调用该方法的代码不需要了解我们在内部赋予了名称一个对应的ID,未来ID生成规则我们可以随意改变,而不会影响到使用者:

use std::collections::HashMap;
struct People(HashMap<u32, String>);
impl People {
  fn new() -> Self {
    People(HashMap::new())
  }
  fn add(&mut self, name: String) {
    // 根据名字简单生成一个id
    let id: u32 = name.as_bytes().iter().map(|&x| x as u32).sum();
    // 存入到HashMap
    self.0.insert(id, name);
  }
}
let mut people = People::new();
people.add(String::from("xiaoming"));
// {860: "xiaoming"}
复制代码

使用类型别名创建同义类型

使用过TS的同学一定知道,使用type关键字可以为现有的类型生成另外的名称:

type Kilometers = u32;
let x: u32 = 5;
let y: Kilometers = 6;
println!("{}", x + y);
// 11
复制代码

类型别名最主要的用途是减少代码字符重复:

type Thunk = Box<dyn Fn()>;

// 1. Thunk作为参数
fn takes_long_type(f: Thunk) {
  f()
}
let f: Thunk = Box::new(|| println!("hi"));
takes_long_type(f); // "hi"

// 2. Thunk作为返回值
fn returns_long_type() -> Thunk {
  Box::new(|| println!("hello"))
}
let f2 = returns_long_type();
f2(); // "hello"
复制代码

对于Result<T, E>类型我们常常使用类型别名来减少代码重复,比如在std::io模块中的方法在返回值中返回Result<T, E>处理失败:

use std::io::Error;
use std::fmt;

pub trait Write {
  fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
  fn flush(&mut self) -> Result<(), Error>;

  fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
  fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
复制代码

我们使用类型别名来处理上面重复出现的Result<..., Error>

// 因为所有的E都是std::io::Error类型,
// 而T在不同的方法中返回的类型是不同的,
// 所以我们只需要把T类型作为类型参数传入即可
type Result<T> = std::result::Result<T, std::io::Error>;

pub trait Write {
  fn write(&mut self, buf: &[u8]) -> Result<usize>;
  fn flush(&mut self) -> Result<()>;

  fn write_all(&mut self, buf: &[u8]) -> Result<()>;
  fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
复制代码

永不返回的Never类型

rust有一个名为!的特殊类型,它在类型系统中的术语为空类型(empty type),因为它没有任何的值。我们倾向于叫它never类型,因为它在从不返回的函数中充当返回值的类型,比如下面函数bar永远不会返回值:

fn bar() -> ! {
}
复制代码

continue的返回类型也是!

let mut x = 0;
loop {
  let y: u32 = if x == 1 {
    x
  } else {
    x += 1;
    continue;
  };
  println!("y: {}", y);
}
复制代码

上面代码会陷入死循环,这不是重点,我们看y的类型是u32if分支中的x类型正确,而continue的返回值是!,这里的重点是类型!的表达式可以被强制转换为其他的任意类型,所以允许u32作为y的类型,不然肯定报错了。


另外,panic!宏的实现使用了never类型,这里以Option<T>unwrap函数为例:

impl<T> Option<T> {
  pub fn unwrap(self) -> T {
    match self {
      Some(val) => val,
      None => panic!("called `Option::unwrap()` on a `None` value"),
    }
  }
}
复制代码

上面代码中编译器知道valT类型,panic!!类型,这里!被转换为T类型,所以整个match表达式的结果是T类型。


loop的返回类型是!

let x /* ! */ = loop {
  print!("loop");
};

// 如果loop中存在break,那么x的类型就是空元祖: ()
let x /* () */ = loop {
  break;
};
复制代码

上面x变量后的注释中是被编译器推断的类型,可以在vscode编辑器里看到,大家可以去尝试下

动态大小类型和Sized trait

rust在编译时必须知道所有类型的大小,而类型系统中存在动态类型大小的概念,这些类型只有在运行时才能知道大小

str类型

str就是一个动态大小类型。只有在运行时才能确定字符串的长度,所以无法创建一个str类型的变量,或者使用str类型来作为函数的参数:

let s1: str = "abc"; // 报错,在编译时不知道s1大小
let s2: str = "abcd"; // 报错,在编译时不知道s2大小
fn foo(s3: str) {  // 报错,在编译时不知道s3的大小
}
复制代码

rust在编译阶段会根据类型分配内存,如果每个str拥有相同的内存,那么上面的s1s2应该拥有等量的内存,但实际上两个字符长的长度是不同的,我们一般使用指针来解决str类型的问题:

let s1: &str = "abc";
let s2: &str = "abcd";
fn foo(s3: &str) {
}
复制代码

str改为引用类型&str就可以编译通过了,原因在于每一个引用的大小是固定的,都各自包含一个指向数据在内存中的起始位置和数据的长度


除了&引用以外,使用智能指针也可以在编译期间确定大小:

use std::rc::Rc;
let b: Box<str> = Box::from("abc");
let r: Rc<str> = Rc::from("abc");
复制代码

Sized trait

rust还提供了一个特殊的Sized trait来确定一个类型的大小在编译时是否可知,编译时可计算出大小的类型会自动实现这个trait,rust还会为每一个泛型函数隐式地添加Sized约束:

fn generic<T>(t: T) {
}
// 编译后
fn generic<T: Sized>(t: T) {
}
复制代码

泛型函数默认只能用于在编译时已知大小的类型。可以在Sized前面加?来放宽这个限制:

fn generic<T: ?Sized>(t: &T) {
}
复制代码

?Sized的意思是:不确定T是不是Sized的。这个语法只能被用在Sized上,而不能被用于其他trait。另外,参数t类型由T修改为了&T。因为t类型可能不是Sized的,所以我们需要将它放置在某种指针的后面。在上面使用了引用,当然也可以智能指针。

封面图:跟着Tina画美国

关注「码生笔谈」公众号,阅读更多最新章节

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享