x-i18n: generated_at: “2026-03-01T15:01:14Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: 640f8f52f31cf573a6a1f64dbbdebfe7f36f54a3f42a8cf22c42e7658e75960e source_path: ch20-03-advanced-types.md workflow: 16
高级类型 (Advanced Types)
Rust 类型系统有一些我们到目前为止提到过但尚未讨论的特性。我们将从一般性的 newtype 开始讨论,研究为什么它们作为类型很有用。然后,我们将转向类型别名,这是一个类似于 newtype 但语义略有不同的特性。我们还将讨论 ! 类型和动态大小类型。
The Rust type system has some features that we’ve so far mentioned but haven’t
yet discussed. We’ll start by discussing newtypes in general as we examine why
they are useful as types. Then, we’ll move on to type aliases, a feature
similar to newtypes but with slightly different semantics. We’ll also discuss
the ! type and dynamically sized types.
使用 Newtype 模式实现类型安全和抽象 (Type Safety and Abstraction with the Newtype Pattern)
本节假设你已经阅读了前面的“使用 Newtype 模式实现外部特征”一节。Newtype 模式除了我们已经讨论过的任务外,还对其他任务非常有用,包括静态地强制执行值永远不会混淆,并指示值的单位。你在示例 20-16 中看到了使用 newtype 指示单位的例子:回想一下, Millimeters 和 Meters 结构体在 newtype 中包装了 u32 值。如果我们编写了一个带有 Millimeters 类型参数的函数,我们就无法编译一个由于疏忽而尝试使用 Meters 或普通 u32 值调用该函数的程序。
This section assumes you’ve read the earlier section “Implementing External
Traits with the Newtype Pattern”. The newtype pattern
is also useful for tasks beyond those we’ve discussed so far, including
statically enforcing that values are never confused and indicating the units of
a value. You saw an example of using newtypes to indicate units in Listing
20-16: Recall that the Millimeters and Meters structs wrapped u32 values
in a newtype. If we wrote a function with a parameter of type Millimeters, we
wouldn’t be able to compile a program that accidentally tried to call that
function with a value of type Meters or a plain u32.
我们也可以使用 newtype 模式来抽象掉一个类型的一些实现细节:新类型可以暴露一个与其内部私有类型的 API 不同的公有 API。
We can also use the newtype pattern to abstract away some implementation details of a type: The new type can expose a public API that is different from the API of the private inner type.
Newtype 还可以隐藏内部实现。例如,我们可以提供一个 People 类型来包装一个存储人的 ID 及其姓名的 HashMap<i32, String> 。使用 People 的代码将仅与我们提供的公有 API 交互,例如向 People 集合添加姓名字符串的方法;那段代码不需要知道我们在内部为姓名分配了一个 i32 ID。Newtype 模式是实现封装以隐藏实现细节的一种轻量级方式,我们在第 18 章的“隐藏实现细节的封装”部分讨论过这一点。
Newtypes can also hide internal implementation. For example, we could provide a
People type to wrap a HashMap<i32, String> that stores a person’s ID
associated with their name. Code using People would only interact with the
public API we provide, such as a method to add a name string to the People
collection; that code wouldn’t need to know that we assign an i32 ID to names
internally. The newtype pattern is a lightweight way to achieve encapsulation
to hide implementation details, which we discussed in the “Encapsulation that
Hides Implementation
Details”
section in Chapter 18.
类型同义词和类型别名 (Type Synonyms and Type Aliases)
Rust 提供了声明“类型别名 (type alias)”的能力,为现有类型赋予另一个名称。为此,我们使用 type 关键字。例如,我们可以像这样为 i32 创建别名 Kilometers :
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-04-kilometers-alias/src/main.rs:here}}
}
现在别名 Kilometers 是 i32 的一个“同义词 (synonym)”;与我们在示例 20-16 中创建的 Millimeters 和 Meters 类型不同, Kilometers 不是一个单独的、新的类型。具有 Kilometers 类型的值将被视为与 i32 类型的值相同:
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-04-kilometers-alias/src/main.rs:there}}
}
因为 Kilometers 和 i32 是相同的类型,我们可以将这两种类型的值相加,也可以将 Kilometers 值传递给接收 i32 参数的函数。然而,使用这种方法,我们无法获得前面讨论过的 newtype 模式所带来的类型检查优势。换句话说,如果我们在某处混淆了 Kilometers 和 i32 的值,编译器将不会给出错误。
Because Kilometers and i32 are the same type, we can add values of both
types and can pass Kilometers values to functions that take i32
parameters. However, using this method, we don’t get the type-checking benefits
that we get from the newtype pattern discussed earlier. In other words, if we
mix up Kilometers and i32 values somewhere, the compiler will not give us
an error.
类型同义词的主要用例是减少重复。例如,我们可能有一个冗长的类型,如下所示:
Box<dyn Fn() + Send + 'static>
在函数签名和作为类型注解在代码各处编写这个冗长的类型可能会让人厌烦且容易出错。想象一下,如果一个项目中充满了像示例 20-25 中那样的代码。
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-25/src/main.rs:here}}
}
类型别名通过减少重复来使这段代码更易于管理。在示例 20-26 中,我们为这个冗长的类型引入了一个名为 Thunk 的别名,并可以用较短的别名 Thunk 替换该类型的所有用法。
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-26/src/main.rs:here}}
}
这段代码读写起来都容易得多!为类型别名选择一个有意义的名称也可以帮助传达你的意图( thunk 是一个用于表示稍后要求值的代码的术语,因此对于一个被存储的闭包来说是一个合适的名称)。
This code is much easier to read and write! Choosing a meaningful name for a type alias can help communicate your intent as well (thunk is a word for code to be evaluated at a later time, so it’s an appropriate name for a closure that gets stored).
类型别名也经常与 Result<T, E> 类型一起使用,以减少重复。考虑标准库中的 std::io 模块。I/O 操作经常返回 Result<T, E> 来处理操作失败的情况。该库有一个 std::io::Error 结构体,代表所有可能的 I/O 错误。 std::io 中的许多函数都将返回 Result<T, E> ,其中 E 是 std::io::Error ,例如 Write 特征中的这些函数:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-05-write-trait/src/lib.rs}}
Result<..., Error> 被重复了很多次。因此, std::io 有这个类型别名声明:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-06-result-alias/src/lib.rs:here}}
因为这个声明位于 std::io 模块中,我们可以使用完全限定别名 std::io::Result<T> ;也就是说,这是一个将 E 填充为 std::io::Error 的 Result<T, E> 。 Write 特征的函数签名最终看起来像这样:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-06-result-alias/src/lib.rs:there}}
类型别名在两个方面提供了帮助:它使代码更易于编写,“并且”它在整个 std::io 中为我们提供了一个一致的接口。因为它是一个别名,所以它只是另一个 Result<T, E> ,这意味着我们可以对其使用任何适用于 Result<T, E> 的方法,以及像 ? 运算符这样的特殊语法。
The type alias helps in two ways: It makes code easier to write and it gives
us a consistent interface across all of std::io. Because it’s an alias, it’s
just another Result<T, E>, which means we can use any methods that work on
Result<T, E> with it, as well as special syntax like the ? operator.
永不返回的 Never 类型 (The Never Type That Never Returns)
Rust 有一种特殊的类型,名为 ! ,在类型理论术语中被称为“空类型 (empty type)”,因为它没有任何值。我们更喜欢称之为“永不类型 (never type)”,因为它在函数永远不会返回时充当返回类型的占位。这里有一个例子:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-07-never-type/src/lib.rs:here}}
这段代码读作“函数 bar 永不返回 (returns never)”。永不返回的函数被称为“发散函数 (diverging functions)”。我们无法创建 ! 类型的值,所以 bar 永远不可能返回。
This code is read as “the function bar returns never.” Functions that return
never are called diverging functions. We can’t create values of the type !,
so bar can never possibly return.
但是对于一种你永远无法创建其值的类型,它有什么用处呢?回想一下示例 2-5 中的代码,它是数字猜谜游戏的一部分;我们在示例 20-27 中重现了其中一小部分。
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-05/src/main.rs:ch19}}
当时,我们跳过了这段代码中的一些细节。在第 6 章的“ match 控制流结构”一节中,我们讨论了 match 分支必须全部返回相同的类型。所以,例如,以下代码就无法工作:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-08-match-arms-different-types/src/main.rs:here}}
这段代码中 guess 的类型必须既是整数“又是”字符串,而 Rust 要求 guess 只有一个类型。那么, continue 返回什么呢?在示例 20-27 中,我们怎么被允许从一个分支返回 u32 而另一个分支以 continue 结束呢?
The type of guess in this code would have to be an integer and a string,
and Rust requires that guess have only one type. So, what does continue
return? How were we allowed to return a u32 from one arm and have another arm
that ends with continue in Listing 20-27?
正如你可能已经猜到的, continue 具有一个 ! 值。也就是说,当 Rust 计算 guess 的类型时,它查看两个 match 分支,前者具有 u32 的值,而后者具有 ! 的值。因为 ! 永远不会有值,所以 Rust 决定 guess 的类型是 u32 。
As you might have guessed, continue has a ! value. That is, when Rust
computes the type of guess, it looks at both match arms, the former with a
value of u32 and the latter with a ! value. Because ! can never have a
value, Rust decides that the type of guess is u32.
描述这种行为的正式方式是, ! 类型表达式可以被强制转换为任何其他类型。我们被允许以此 match 分支以 continue 结束,是因为 continue 不返回值;相反,它将控制权移回到循环顶部,所以在 Err 的情况下,我们从未给 guess 分配值。
The formal way of describing this behavior is that expressions of type ! can
be coerced into any other type. We’re allowed to end this match arm with
continue because continue doesn’t return a value; instead, it moves control
back to the top of the loop, so in the Err case, we never assign a value to
guess.
Never 类型对 panic! 宏也很有用。回想一下我们在 Option<T> 值上调用以产生一个值或引发恐慌的 unwrap 函数,其定义如下:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-09-unwrap-definition/src/lib.rs:here}}
在这段代码中,发生了与示例 20-27 中的 match 相同的事情:Rust 看到 val 具有类型 T ,而 panic! 具有类型 ! ,因此整个 match 表达式的结果是 T 。这段代码之所以能工作,是因为 panic! 不产生值;它结束了程序。在 None 的情况下,我们不会从 unwrap 返回值,因此这段代码是有效的。
In this code, the same thing happens as in the match in Listing 20-27: Rust
sees that val has the type T and panic! has the type !, so the result
of the overall match expression is T. This code works because panic!
doesn’t produce a value; it ends the program. In the None case, we won’t be
returning a value from unwrap, so this code is valid.
最后一个具有 ! 类型的表达式是一个循环:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-10-loop-returns-never/src/main.rs:here}}
在这里,循环永远不会结束,所以 ! 是表达式的值。然而,如果我们包含了一个 break ,这就不是真的了,因为循环会在执行到 break 时终止。
Here, the loop never ends, so ! is the value of the expression. However, this
wouldn’t be true if we included a break, because the loop would terminate
when it got to the break.
动态大小类型与 Sized 特征 (Dynamically Sized Types and the Sized Trait)
Rust 需要知道其类型的某些细节,例如为特定类型的值分配多少空间。这使得其类型系统的一个角落起初有些令人困惑:即“动态大小类型 (dynamically sized types)”的概念。这些类型有时被称为 DST 或“不定大小类型 (unsized types)”,它们让我们能编写使用那些只有在运行时才能知道其大小的值的代码。
Rust needs to know certain details about its types, such as how much space to allocate for a value of a particular type. This leaves one corner of its type system a little confusing at first: the concept of dynamically sized types. Sometimes referred to as DSTs or unsized types, these types let us write code using values whose size we can know only at runtime.
让我们深入了解一下名为 str 的动态大小类型的细节,我们在整本书中都一直在使用它。没错,不是 &str ,而是 str 本身,是一个 DST。在许多情况下,例如存储用户输入的文本时,我们直到运行时才能知道字符串有多长。这意味着我们不能创建一个 str 类型的变量,也不能接收一个 str 类型的参数。考虑以下无法工作的代码:
Let’s dig into the details of a dynamically sized type called str, which
we’ve been using throughout the book. That’s right, not &str, but str on
its own, is a DST. In many cases, such as when storing text entered by a user,
we can’t know how long the string is until runtime. That means we can’t create
a variable of type str, nor can we take an argument of type str. Consider
the following code, which does not work:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-11-cant-create-str/src/main.rs:here}}
Rust 需要知道为特定类型的任何值分配多少内存,并且一个类型的所有值必须使用相同数量的内存。如果 Rust 允许我们编写这段代码,这两个 str 值将需要占用相同数量的空间。但它们有不同的长度: s1 需要 12 字节的存储空间,而 s2 需要 15 字节。这就是为什么无法创建一个持有动态大小类型的变量。
Rust needs to know how much memory to allocate for any value of a particular
type, and all values of a type must use the same amount of memory. If Rust
allowed us to write this code, these two str values would need to take up the
same amount of space. But they have different lengths: s1 needs 12 bytes of
storage and s2 needs 15. This is why it’s not possible to create a variable
holding a dynamically sized type.
那么,我们该怎么办呢?在这种情况下,你已经知道答案了:我们将 s1 和 s2 的类型设为字符串切片 ( &str ) 而不是 str 。回想第 4 章“字符串切片”一节可知,切片数据结构仅存储起始位置和切片的长度。所以,虽然 &T 是一个存储 T 所在内存地址的单一值,但字符串切片是“两个”值: str 的地址及其长度。因此,我们可以在编译时知道字符串切片值的大小:它是 usize 长度的两倍。也就是说,我们始终知道字符串切片的大小,无论它引用的字符串有多长。通常,这就是在 Rust 中使用动态大小类型的方式:它们有一个存储动态信息大小的额外元数据片段。动态大小类型的金科玉律是:我们必须始终将动态大小类型的值放在某种指针之后。
So, what do we do? In this case, you already know the answer: We make the type
of s1 and s2 string slice (&str) rather than str. Recall from the
“String Slices” section in Chapter 4 that the
slice data structure only stores the starting position and the length of the
slice. So, although &T is a single value that stores the memory address of
where the T is located, a string slice is two values: the address of the
str and its length. As such, we can know the size of a string slice value at
compile time: It’s twice the length of a usize. That is, we always know the
size of a string slice, no matter how long the string it refers to is. In
general, this is the way in which dynamically sized types are used in Rust:
They have an extra bit of metadata that stores the size of the dynamic
information. The golden rule of dynamically sized types is that we must always
put values of dynamically sized types behind a pointer of some kind.
我们可以将 str 与各种指针结合使用:例如 Box<str> 或 Rc<str> 。实际上,你以前见过这种情况,但是使用的是另一种动态大小类型:特征。每一个特征都是一个我们可以通过使用特征名称来引用的动态大小类型。在第 18 章的“使用特征对象实现不同类型间的抽象行为”一节中,我们提到为了将特征用作特征对象,我们必须将它们放在指针之后,例如 &dyn Trait 或 Box<dyn Trait> ( Rc<dyn Trait> 也可以)。
We can combine str with all kinds of pointers: for example, Box<str> or
Rc<str>. In fact, you’ve seen this before but with a different dynamically
sized type: traits. Every trait is a dynamically sized type we can refer to by
using the name of the trait. In the “Using Trait Objects to Abstract over
Shared Behavior” section in Chapter 18, we mentioned that to use traits as trait
objects, we must put them behind a pointer, such as &dyn Trait or Box<dyn Trait> (Rc<dyn Trait> would work too).
为了处理 DST,Rust 提供了 Sized 特征来确定一个类型的大小在编译时是否已知。对于大小在编译时已知的所有事物,此特征会自动实现。此外,Rust 会隐式地为每个泛型函数添加一个 Sized 约束。也就是说,像这样的泛型函数定义:
To work with DSTs, Rust provides the Sized trait to determine whether or not
a type’s size is known at compile time. This trait is automatically implemented
for everything whose size is known at compile time. In addition, Rust
implicitly adds a bound on Sized to every generic function. That is, a
generic function definition like this:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-12-generic-fn-definition/src/lib.rs}}
实际上被视为好像我们编写了这段代码:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-13-generic-implicit-sized-bound/src/lib.rs}}
默认情况下,泛型函数将仅适用于在编译时具有已知大小的类型。然而,你可以使用以下特殊语法来放宽此限制:
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-14-generic-maybe-sized/src/lib.rs}}
?Sized 上的特征约束意味着“ T 可能是也可能不是 Sized ”,这种记法覆盖了泛型类型在编译时必须具有已知大小的默认设置。具有此含义的 ?Trait 语法仅对 Sized 可用,不对任何其他特征可用。
A trait bound on ?Sized means “T may or may not be Sized,” and this
notation overrides the default that generic types must have a known size at
compile time. The ?Trait syntax with this meaning is only available for
Sized, not any other traits.
还要注意,我们将 t 参数的类型从 T 更改为 &T 。因为该类型可能不是 Sized 的,所以我们需要在某种指针后面使用它。在这种情况下,我们选择了一个引用。
Also note that we switched the type of the t parameter from T to &T.
Because the type might not be Sized, we need to use it behind some kind of
pointer. In this case, we’ve chosen a reference.
接下来,我们将讨论函数和闭包!
Next, we’ll talk about functions and closures!