当前位置: 首页 > article >正文

【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关键字。例如:

type Kilometers = i32;

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

fn main() {
	type Kilometers = i32;

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

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

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

Box<dyn Fn() + Send + 'static>

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

    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> {
        // ...
    }

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

    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>类型一起使用,以减少重复。如下例:

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使用了类型别名:

type Result<T> = std::result::Result<T, std::io::Error>;

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

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类型,因为它写在函数返回值类型的位置。

举个例子:

fn bar() -> ! {
	
}

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

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

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

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

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的定义:

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)这个动态大小类型为例:

let s1: str = "Hello there!";
let s2: str = "How's it going?";

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

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

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

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。

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

fn generic<T>(t: T) {
    // ...
}

它的实际写法是:

fn generic<T: Sized>(t: T) {
    // ...
}

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

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

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

type Job = Box<dyn FnOnce() + Send + 'static>;

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


http://www.kler.cn/a/530861.html

相关文章:

  • MongoDb user自定义 role 添加 action(collStats, EstimateDocumentCount)
  • 【Vite + Vue + Ts 项目三个 tsconfig 文件】
  • springboot 启动原理
  • 梯度提升用于高效的分类与回归
  • 【RAG】SKLearnVectorStore 避免使用gpt4all会connection err
  • JavaScript作用域详解
  • 人工智能导论-第3章-知识点与学习笔记
  • 求职刷题力扣DAY34--贪心算法part05
  • 深入剖析 Bitmap 数据结构:原理、应用与优化策略
  • UE PlayerController、AIController
  • UE5 蓝图学习计划 - Day 9:数组与跨蓝图通信
  • 服务SDK三方新版中央仓库和私服发布详解
  • Java 网络原理 ③-NAT || DHCP
  • 在K8S中,如何把某个worker节点设置为不可调度?
  • C语言可变参数
  • leetcode解题思路分析(一百六十三)1409 - 1415 题
  • 【1】快手面试题整理
  • C基础寒假练习(2)
  • AI模型升级版0.04
  • 【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.10 ndarray内存模型:从指针到缓存优化
  • DeepSeek横空出世,AI格局或将改写?
  • 《苍穹外卖》项目学习记录-Day11用户统计
  • selenium记录Spiderbuf例题C01
  • Rust中使用ORM框架diesel报错问题
  • ip属地是实时刷新吗还是网络刷新
  • AI模型升级版0.03