高级类型

Rust 类型系统具有一些我们目前已经提到但尚未提及的功能 但被讨论过。我们将从一般性地讨论 newtype 开始,同时研究原因 new类型可用作类型。然后,我们将继续讨论类型别名,这是一个功能 类似于 newtypes,但语义略有不同。我们还将讨论 这!type 和动态大小的类型。

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

注意:本节假设您已经阅读了前面的部分 “使用 newtype 模式来实现 external trait 类型。

newtype 模式对于我们讨论过的任务之外的任务也很有用 far,包括静态强制值永远不会混淆,以及 指示值的单位。您看到了一个使用 newtypes 来 表示示例 19-15 中的单位:回想一下MillimetersMeters结构体包装u32值。如果我们编写了一个带有 type 为Millimeters,我们无法编译一个 意外尝试调用值为Meters或 平原u32.

我们还可以使用 newtype 模式来抽象出一些实现 details 的类型:新类型可以公开一个不同于 私有内部类型的 API。

newtypes 还可以隐藏内部实现。例如,我们可以提供Peopletype 将HashMap<i32, String>存储人员 ID 与他们的名称相关联。使用 Code usingPeople只会与 public API 中提供的 API 中,例如将名称字符串添加到People收集;该代码不需要知道我们为i32ID 转换为名称 内部。newtype 模式是一种实现封装的轻量级方法 来隐藏实现细节,我们在“封装 隐藏实现 详细信息“部分。

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

Rust 提供了声明类型别名以给出现有类型的功能 另一个名字。为此,我们使用type关键词。例如,我们可以创建 别名Kilometersi32这样:

fn main() {
    type Kilometers = i32;

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

    println!("x + y = {}", x + y);
}

现在,别名Kilometers是 的同义词i32;与MillimetersMeters类型,我们在示例 19-15 中创建的Kilometers不是单独的, new 类型。类型为Kilometers将被视为与 type 的值i32:

fn main() {
    type Kilometers = i32;

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

    println!("x + y = {}", x + y);
}

因为Kilometersi32是相同的类型,我们可以将两者的值相加 类型,我们可以将Kilometersvalues 转换为采用i32参数。但是,使用这种方法,我们没有获得类型检查的好处 我们从前面讨论的 newType 模式中得到。换句话说,如果我们 捹Kilometersi32values 时,编译器不会给我们 一个错误。

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

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

在函数签名中编写此冗长的类型,并将其全部作为类型注释 over the code 可能很烦人且容易出错。想象一下,有一个充满 代码类似于示例 19-24 中的代码。

fn main() {
    let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

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

    fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
        // --snip--
        Box::new(|| ())
    }
}

示例 19-24:在许多地方使用 long 类型

类型别名通过减少重复性使此代码更易于管理。在 示例 19-25,我们引入了一个名为Thunk对于 verbose 类型,将 可以将 type 的所有用法替换为较短的别名Thunk.

fn main() {
    type Thunk = Box<dyn Fn() + Send + 'static>;

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

    fn takes_long_type(f: Thunk) {
        // --snip--
    }

    fn returns_long_type() -> Thunk {
        // --snip--
        Box::new(|| ())
    }
}

示例 19-25:引入类型别名Thunk减少 重复

这段代码更容易读写!为 选择有意义的名称 Type alias 也可以帮助传达您的意图(Thunk 是代码的代名词 以便稍后进行评估,因此它是 被存储)。

类型别名也常与Result<T, E>type 用于减少 重复。考虑一下std::io模块。I/O (输入输出) 作通常会返回Result<T, E>处理作 无法正常工作。此库具有std::io::Error表示所有 可能的 I/O 错误。中的许多函数std::io将会再来Result<T, E>其中,Estd::io::Error,例如 这Write特性:

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

Result<..., Error>重复了很多次。因此,std::io具有此类型 alias 声明:

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模块中,我们可以完全使用 限定别名std::io::Result<T>;也就是说,Result<T, E>使用E填写为std::io::Error.这Writetrait 函数签名结束 如下所示:

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

类型别名在两个方面提供帮助:它使代码更易于编写,并且它提供 US 的一致界面std::io.因为它是一个别名,所以它是 只是另一个Result<T, E>,这意味着我们可以使用任何适用于Result<T, E>以及特殊语法(如?算子。

Never 返回的 Never 类型

Rust 有一个名为!这在类型论术语中被称为空类型,因为它没有值。我们更喜欢将其称为 never 类型,因为当函数永远不会 返回。下面是一个示例:

fn bar() -> ! {
    // --snip--
    panic!();
}

此代码被读取为“函数bar永远不会返回。返回 never 称为发散函数。我们不能创建以下类型的值!所以bar永远不可能回来。

但是,您永远无法为其创建值的类型有什么用呢?从 示例 2-5,部分猜数字游戏;我们复制了一些 在示例 19-26 中。

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

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

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

示例 19-26:一个match臂的末端为continue

当时,我们跳过了这段代码中的一些细节。在第 6 章中“这match控制流作员”部分,我们讨论了matcharms 必须都返回相同的类型。所以,对于 示例,以下代码不起作用:

fn main() {
    let guess = "3";
    let guess = match guess.trim().parse() {
        Ok(_) => 5,
        Err(_) => "hello",
    };
}

的类型guess在此代码中,必须是一个整数和一个字符串, 而 Rust 要求guess只有一个类型。那么continue返回?我们是如何允许返回u32从一只手臂和另一只手臂 结尾为continue在示例 19-26 中?

您可能已经猜到了,continue具有!价值。也就是说,当 Rust 计算guess,它会查看两个 match 臂,前者具有 的值u32而后者则带有!价值。因为!永远不能有 值,Rust 决定guessu32.

描述此行为的正式方式是!能 被强迫进入任何其他类型。我们被允许结束这一切matcharm 替换为continue因为continue不返回值;相反,它会移动控制 返回到循环的顶部,因此在Errcase 中,我们从不为guess.

never 类型与panic!宏。回想一下unwrap我们调用的函数Option<T>值来生成值或 panic 此定义:

enum Option<T> {
    Some(T),
    None,
}

use crate::Option::*;

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

在此代码中,与match示例 19-26 中:Rust 看到那个val具有Tpanic!具有!,那么结果 整体matchexpression 为T.此代码之所以有效,是因为panic!不会产生值;它将结束程序。在None案子,我们不会 从unwrap,因此此代码有效。

具有!是一个loop:

fn main() {
    print!("forever ");

    loop {
        print!("and ever ");
    }
}

在这里,循环永远不会结束,所以!是表达式的值。但是,此 如果我们包含break,因为循环将终止 当它到达break.

动态大小的类型和Sized特性

Rust 需要知道有关其类型的某些详细信息,例如要 allocate 的值。这留下了它的类型的一个角落 系统一开始有点令人困惑:动态大小类型的概念。 有时称为 DST未调整大小的类型,这些类型允许我们编写 使用我们只能在运行时知道其大小的值进行编码。

让我们深入研究一个名为str哪 我们在整本书中一直在使用。没错,不是&strstr上 它自己的是 DST。我们无法知道字符串在 runtime 之前有多长,这意味着 我们不能创建类型为str,我们也不能采用str.请考虑以下代码,该代码不起作用:

fn main() {
    let s1: str = "Hello there!";
    let s2: str = "How's it going?";
}

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

那么我们该怎么办呢?在这种情况下,您已经知道答案:我们制作类型 之s1s2一个&str而不是str.从 “String 中召回 Slices“部分,其中 structure 只存储切片的起始位置和长度。所以 虽然&T是一个值,用于存储其中T位于一个&str两个值:的str及其 长度。因此,我们可以知道&str值:它是 长度是 a 的两倍usize.也就是说,我们总是知道 a 的大小&str不 无论它引用的字符串有多长。一般来说,这是进入 在 Rust 中使用了哪些动态大小的类型:它们有一个额外的 元数据,用于存储动态信息的大小。黄金法则 动态大小的类型是我们必须始终将动态大小的值 类型。

我们可以结合str使用各种指针:例如,Box<str>Rc<str>.事实上,您以前已经见过这种情况,但动态 sized type: traits.每个 trait 都是一个动态大小的类型,我们可以用 使用 trait 的名称。在第 17 章的“使用 trait 对象 允许不同的值 Types“ 部分,我们提到要使用 trait 作为 trait 对象,我们必须 将它们放在指针后面,例如&dyn TraitBox<dyn Trait> (Rc<dyn Trait>也可以)。

为了使用 DST,Rust 提供了Sizedtrait 来确定 类型的大小在编译时是已知的。此 trait 是自动实现的 对于在编译时已知大小的所有内容。此外,Rust 隐式添加 Bound onSized到每个通用函数。也就是说,一个 泛型函数定义如下:

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

实际上被视为我们编写了以下内容:

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

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

fn generic<T: ?Sized>(t: &T) {
    // --snip--
}

绑定在?Sized的意思是”T可能是也可能不是Sized“和这个 表示法会覆盖泛型类型必须具有已知大小的默认值 编译时。这?Trait语法仅适用于Sized,而不是任何其他特征。

另请注意,我们切换了t参数从T&T. 因为类型可能不是Sized,我们需要在某种 指针。在本例中,我们选择了一个参考。

接下来,我们将讨论函数和闭包!

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准