【Rust自学】19.5. 高级类型

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=^・ω・^=)

19.5.1.使用newtype模式实现类型安全和抽象

19.2. 高级trait 中(具体来说是19.2.6. 使用newtype模式在外部类型上实现外部trait)我们就使用了newtype模式为Vector实现了Display trait。

在19.2.2. 默认泛型参数和运算符重载中我们还写过一个 MillimetersMeters结构体用来分别存储毫米和米的数据,由于两个数据并不能直接相加减也就避免了单位混用的问题。

我们还可以使用newtype模式来抽象出类型还有其他一些特性:

  • 新类型可以公开与私有内部类型的API不同的公共API
  • 新类型还可以隐藏内部实现(在 17.1.2. 封装 中提到过)

19.5.2. 类型别名

Rust 提供了声明类型别名的能力,以便为现有类型提供另一个名称(很像泛型)。

使用了类型别名需要type关键字。例如:

rust 复制代码
type Kilometers = i32;

我们把Kilometers称为i32近义词 。你可以像使用i32那样使用Kilometers

rust 复制代码
fn main() {
	type Kilometers = i32;

    let x: i32 = 5;
    let y: Kilometers = 5;

    println!("x + y = {}", x + y);
}
  • 因为Kilometersi32是相同的类型,所以我们可以将两种类型的值相加

类型同义词的主要用例是减少重复。例如,我们可能有一个像这样的冗长类型:

rust 复制代码
Box<dyn Fn() + Send + 'static>

在整个代码中将这种冗长的类型写入函数签名和类型注释可能会很烦人并且容易出错。如下例:

rust 复制代码
    let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

    fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
        // ...
    }

    fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
        // ...
    }

类型别名通过减少重复使该代码更易于管理,而且一个有意义的名称可以更好地传达意图。我们对上面的代码进行修改:

rust 复制代码
    type Thunk = Box<dyn Fn() + Send + 'static>;

    let f: Thunk = Box::new(|| println!("hi"));

    fn takes_long_type(f: Thunk) {
        // ...
    }

    fn returns_long_type() -> Thunk {
        // ...
    }

类型别名也常与Result<T, E>类型一起使用,以减少重复。如下例:

rust 复制代码
use std::fmt;
use std::io::Error;

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>;
}

I/O操作通常返回Result<T, E>以处理操作失败的情况。它的std::io::Error表示所有可能的I/O错误。std::io中的许多函数将返回Result<T, E>。其中Estd::io::Error

Result<..., Error>重复了很多次。因此,std::io使用了类型别名:

rust 复制代码
type Result<T> = std::result::Result<T, std::io::Error>;

Write特征函数签名最终看起来像这样:

rust 复制代码
use std::fmt; 

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<()>;
}

类型别名在这里有两个作用:

  • 它使代码更容易编写,并为我们提供了跨std::io的一致接口。
  • 因为它是一个别名 ,所以它本质上只是另一个Result<T, E> ,这意味着我们可以使用任何适用的方法Result<T, E>以及特殊语法,如?符(在 9.3.2. ?运算符 中有讲)。

19.5.3. never类型

Rust 有一个特殊类型叫!,这在类型理论术语中被称为空类型 ,因为它没有值。我们更喜欢称其为never类型,因为它写在函数返回值类型的位置。

举个例子:

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

这段代码被解读为"函数bar永不会返回 "。从不返回的函数称为发散函数

那么never类型有什么作用呢?让我们以第二章猜数游戏的一段代码为例:

rust 复制代码
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

这么写没问题,那如果我们这样写呢:

rust 复制代码
let guess = match guess.trim().parse() {
    Ok(_) => 5,
    Err(_) => "hello",
};

这段代码会出问题,因为match两个分支返回值的类型不一样 ,Rust作为强类型语言必须知道所值的准确类型。guess类型可能是i32&str,而Rust要求guess只能是一种类型。

也就是说,这种写法下 match下的所有分支的返回值类型都得一样

那么回看正确的代码:Ok返回的num类型是u32Err执行的continue返回类型是什么呢?如果是代表没有返回值的单元类型()Rust就无法判断guess的值到底是u32类型还是()类型。

这就是never类型的用武之地: continue有一个返回类型是!。也就是说,当 Rust查看guess的类型时,它会先查看两个match分支,前者的返回值为u32 ,后者的返回值值为!。因为!永远不可能有返回值值,Rust就明白guess的类型是u32

never类型对于panic!宏的作用也是如此。看看unwrap的定义:

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

Rust看到val是类型T,而panic!! ,所以match表达式返回值整体就是T。这段代码之所以有效,是因为panic!不返回值,而是结束程序。

实际上,loop也是!,因为loop执行的无尽循环不会结束,所以就不可能有返回值。然而,如果我们包含一个break ,情况就不是这样了,因为循环在到达break时就会终止。

19.5.4. 动态大小和和Sized trait

Rust 需要了解有关其类型的某些详细信息,例如为特定类型的值分配多少空间。这是得动态大小类型(dynamically sized types) 这个概念有些迷惑人。它有时被称为DSTunsized types,这些类型允许我们使用只能在运行时知道其大小的值来编写代码。

我们使用str(不是&str也不是String)这个动态大小类型为例:

rust 复制代码
let s1: str = "Hello there!";
let s2: str = "How's it going?";

在运行时之前我们无法知道字符串有多长,这意味着我们无法创建str类型的变量,所以上面的代码例是不能运行的。

Rust 需要知道为特定类型的任何值分配多少内存,并且同一类型的所有值必须使用相同的内存量。如果Rust允许我们编写这段代码,那么这两个str值将需要占用相同的空间量。但它们的长度不同: s1需要12个字节的存储空间,而s2需要 15 个字节。这就是为什么无法创建保存动态大小类型的变量的原因。

那么我们该怎么办呢?一般来说,将s1s2的类型设为&str而不是str就能解决问题:

rust 复制代码
let s1: &str = "Hello there!";
let s2: &str = "How's it going?";

切片数据结构只存储切片的起始位置和长度 。因此,虽然&T是一个存储了内存地址的单个值T位于, &str是两个值(在 4.5. 切片(Slice) 有讲):

  • str的地址(usize)
  • str的长度(usize)

因此,我们可以在编译时知道&str值的大小:它是usize长度的两倍。也就是说,我们总是知道&str的大小,无论它引用的字符串有多长。

一般来说,Rust中使用动态大小类型的最好方式是:它们有一个额外的元数据来存储动态信息的大小 。动态大小类型的黄金法则是,我们必须始终将动态大小类型的值放在某种指针后面

我们可以将str与各种指针组合:例如Box<str>Rc<str>。而trait实际上也是动态大小类型。为了使用动态大小类型,Rust提供了Sized trait来确定类型的大小在编译时是否已知 。对于编译时大小已知的所有内容,都会自动实现此trait。此外,Rust隐式地​​为每个泛型函数添加了Sized trait。

也就是说,像这样的通用函数定义:

rust 复制代码
fn generic<T>(t: T) {
    // ...
}

它的实际写法是:

rust 复制代码
fn generic<T: Sized>(t: T) {
    // ...
}

默认情况下,泛型函数仅适用于编译时大小已知的类型 。但是也可以使用?Sized特殊语法来放宽此限制:

rust 复制代码
fn generic<T: ?Sized>(t: &T) {
    // ...
}
  • ?Sized意味着" T可能实现也可能没实现Sized trait",也就是T可能是动态大小类型也可能不是。这种表示方法不需要泛型类型在编译时必须具有已知大小 这个默认条件。有这种含义的?Trait语法仅适用于Sized trait ,没有任何其他trait。
  • 我们将t参数的类型从泛型T切换为&T。因为类型可能没实现Sized trait,就是动态大小类型,所以我们需要用指针包裹动态大小类型。

使用动态大小类型的最好场景是与trait配合时:有时候我们会要求某些数据必须实现某些trait或是指定的生命周期,但不知道具体是什么类型,所以就可以使用指针包裹动态类型的写法。如下例:

rust 复制代码
type Job = Box<dyn FnOnce() + Send + 'static>;

这个例子就使用了类型别名指针包裹动态类型 的写法,Job可以是任何同时能实现FnOnce() trait、Send trait和'static生命周期的类型

相关推荐
ChinaRainbowSea14 分钟前
八. Spring Boot2 整合连接 Redis(超详细剖析)
java·数据库·spring boot·redis·后端·nosql
MATLAB代码顾问20 分钟前
MATLAB实现多种群遗传算法
开发语言·matlab
晚秋贰拾伍38 分钟前
每天学点小知识之设计模式的艺术-策略模式
运维·设计模式·系统安全·运维开发·策略模式
叫我DPT41 分钟前
Go 中 defer 的机制
开发语言·后端·golang
幻想趾于现实1 小时前
C# 装箱和拆箱(以及 as ,is)
开发语言·c#
我们的五年1 小时前
【Linux网络编程】:守护进程,前台进程,后台进程
linux·服务器·后端·ubuntu
谢大旭2 小时前
ASP.NET Core自定义 MIME 类型配置
后端·c#
好好学Java吖3 小时前
【二分题目】
java·开发语言
米码收割机3 小时前
【PHP】基于 PHP 的图片管理系统(源码+论文+数据库+图集)【独一无二】
开发语言·数据库·php
yyytucj3 小时前
优化 PHP-FPM 参数配置:实现服务器性能提升
服务器·开发语言·php