高级类型

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

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

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

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

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

newtypes 还可以隐藏内部实现。例如,我们可以提供一个类型来包装存储人员 ID 的 a 与他们的名称相关联。使用的代码只会与 public API,例如向集合中添加名称字符串的方法;该代码不需要知道我们为 names 分配了 ID 内部。newtype 模式是一种实现封装的轻量级方法 来隐藏实现细节,我们在“封装 隐藏实现 详细信息“部分。PeopleHashMap<i32, String>PeoplePeoplei32

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

Rust 提供了声明类型别名以给出现有类型的功能 另一个名字。为此,我们使用关键字。例如,我们可以创建 别名设置为如下所示:typeKilometersi32

fn main() {
    type Kilometers = i32;

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

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

现在,别名是 ;与我们在示例 19-15 中创建的 and 类型不同,它不是一个单独的 new 类型。具有 type 的值将被视为与 type 的值 :Kilometersi32MillimetersMetersKilometersKilometersi32

fn main() {
    type Kilometers = i32;

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

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

因为 和 是相同的类型,所以我们可以将两者的值相加 类型,我们可以将值传递给接受参数的函数。但是,使用这种方法,我们没有获得类型检查的好处 我们从前面讨论的 newType 模式中得到。换句话说,如果我们 mix up 和 values 的 一个错误。Kilometersi32Kilometersi32Kilometersi32

类型同义词的主要用例是减少重复。例如,我们 可能有一个像这样的 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 中,我们引入了一个以 verbose 类型命名的别名,并且 可以用较短的 alias 替换该类型的所有用法。ThunkThunk

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 是代码的代名词 以便稍后进行评估,因此它是 被存储)。

类型别名也常与类型一起使用,用于减少 重复。考虑 standard 库中的 module。I/O (输入输出) 作通常会返回 A 来处理作 无法正常工作。这个库有一个代表所有 可能的 I/O 错误。中的许多函数将返回 is ,例如 性状:Result<T, E>std::ioResult<T, E>std::io::Errorstd::ioResult<T, E>Estd::io::ErrorWrite

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

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

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

因为这个声明在模块中,所以我们可以完全使用 限定别名 ;即,填写为 的 a 。trait 函数签名以 如下所示:std::iostd::io::Result<T>Result<T, E>Estd::io::ErrorWrite

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 在所有 .因为它是一个别名,所以它是 Just another ,这意味着我们可以使用任何与它一起使用的方法,以及像 Operator 这样的特殊语法。std::ioResult<T, E>Result<T, E>?

Never 返回的 Never 类型

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

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

此代码被读取为“the function returns never”。返回 never 称为发散函数。我们无法创建该类型的值,因此永远不可能返回。bar!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:一个 arm 以 continue 结尾的匹配项

当时,我们跳过了这段代码中的一些细节。在第 6 章的 “match Control Flow Operator” 一节中,我们讨论了 arms 必须全部返回相同的类型。因此,对于 示例,以下代码不起作用:match

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

此代码中的 type of 必须是一个整数和一个字符串, 而 Rust 要求 它们只有一个类型。那么 return 是什么呢?我们是如何从一只手臂返回 a 并拥有另一只手臂的 以示例 19-26 结尾?guessguesscontinueu32continue

正如您可能已经猜到的那样,具有价值。也就是说,当 Rust 计算 的类型 ,它查看两个 match 分支,前者具有 value of 的 value 和后者的 value.因为永远不能有 value 时,Rust 决定 的类型为 。continue!guessu32!!guessu32

描述此行为的正式方式是,类型的表达式可以 被强迫进入任何其他类型。我们允许用 because 结束这个 arm 不返回值;相反,它会移动控制 回到循环的顶部,所以在这种情况下,我们永远不会给 分配一个值。!matchcontinuecontinueErrguess

never 类型对宏也很有用。回想一下我们在 values 上调用以产生值或 panic 的函数 此定义:panic!unwrapOption<T>

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"),
        }
    }
}

在这段代码中,发生的事情与示例 19-26: Rust 中发生的事情相同 看到 具有 type 和 type ,因此结果 的表达式为 。此代码之所以有效,是因为不会生成值;它将结束程序。在这种情况下,我们不会 从 返回一个值,因此此代码有效。matchvalTpanic!!matchTpanic!Noneunwrap

具有该类型的最后一个表达式是 :!loop

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

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

在这里,循环永无止境,表达式的值也是如此。但是,此 如果我们包含一个 ,则不是真的,因为循环会终止 当它到达 .!breakbreak

动态大小的类型和 Sized 特征

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

让我们深入研究一个名为 的动态大小的类型的详细信息,该类型 我们在整本书中一直在使用。没错,不是 ,而是 on 它自己的是 DST。我们无法知道字符串在 runtime 之前有多长,这意味着 我们不能创建 类型 的变量 ,也不能采用 类型的参数。请考虑以下代码,该代码不起作用:str&strstrstrstr

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

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

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

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

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

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

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

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

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

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

绑定在 上的 trait 表示 “ 可能是也可能不是 ”,而 this 表示法会覆盖泛型类型必须具有已知大小的默认值 编译时。具有此含义的语法仅适用于 ,而不适用于任何其他特征。?SizedTSized?TraitSized

另请注意,我们将参数的类型从 切换到 。 因为类型可能不是 ,所以我们需要在某种 指针。在本例中,我们选择了一个参考。tT&TSized

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

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