写点什么

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

用户头像
码生笔谈
关注
发布于: 刚刚
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画美国


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

发布于: 刚刚阅读数: 2
用户头像

码生笔谈

关注

欢迎关注「码生笔谈」公众号 2018.09.09 加入

前端、Rust干货分享,一起成为更好的Javascripter & Rustacean

评论

发布
暂无评论
5分钟速读之Rust权威指南(四十一)高级类型