Only Bounds
摘要
这篇博客文章介绍了'only bounds',这是对Rust泛型系统的一个提议改进,它用一组更丰富的大小特性代替了当前的Sized/?Sized层次结构,以适应动态大小类型和ARM's Scalable Vector Extension。
<p><a href="https://lobste.rs/s/srq2wf/only_bounds">评论</a></p>
查看缓存全文
缓存时间: 2026/06/09 14:46
# 仅限边界 · 小步快跑 源链接: https://smallcultfollowing.com/babysteps/blog/2026/06/09/only-bounds/ `only` 边界将是 Rust 中最具冲击力的变革之一,但你或许从未听闻。目前,Arm 团队(David Wood、Rémy Rakic 等人)正将其作为**Sized 层级与可扩展向量扩展**(https://rust-lang.github.io/rust-project-goals/2026/scalable-vectors.html)项目目标的一部分进行设计与开发。本文旨在探索这一特性,并回答一个关于设计细节的具体问题(即边界的“范围”,稍后会解释)。但在深入之前,我想先提供一些背景知识。
## 如今 Rust 泛型默认带有 `Sized` 边界
在当前的 Rust 中,每个类型参数(除了 `Self`)都带有一个名为 `Sized` 的默认边界:
```rust
// 所以这个函数...
fn identity(t: T) -> T { t }
// ...实际上是以下内容的简写:
fn identity(t: T) -> T where T: Sized, // <-- 默认添加!
{ t }
```
类型 `T` 实现 `Sized`,当编译器能够在编译时计算出 `T` 值的大小。这对几乎所有类型都成立,但有少数例外。考虑 `[u32]`,它表示“一些 `u32` 实例”。我们知道单个 `u32` 是 4 字节,但不知道有多少个 `u32` 时,就无法得知 `[u32]` 的大小。这意味着不能在栈上拥有一个 `[u32]` 类型的值(栈帧该多大?)。
## 通过 `?Sized` 选择退出
然而,如果你有一个像 `by_ref` 这样的函数,它只是通过引用(即指针)接收值,那你就不需要知道 `[u32]` 值有多大,因为你并不直接操作它。你可以使用一个不要求 `Sized` 的类型参数 `U`,但必须显式地从默认边界中“选择退出”:
```rust
fn by_ref(t: &U) where U: ?Sized, // <-- 从默认中退出
{ }
```
作为一段有趣的历史小花絮,这个系统早在 2014 年就被引入,以容纳**动态大小类型**(https://smallcultfollowing.com/babysteps/blog/2014/01/05/dst-take-5/)。在此之前,`&[u32]` 实际上是一个内置的、不可分割的类型;我们甚至一度将其写作 `[u32]/&`。¹(https://smallcultfollowing.com/babysteps/blog/2026/06/09/only-bounds/#fn:1)
## 但 `Sized` 与 `?Sized` 并不足以满足所有需求
`Sized` 与 `?Sized` 的设计已经相当不错地服务了我们,但它也开始显现其局限性。事实证明,“值拥有静态可计算的大小”与“每个值拥有在运行时才能计算的不同大小”并不能涵盖你可能想要的所有情况。例如,`extern` 类型的值在运行时也没有已知的大小。而 Arm 的可扩展向量扩展则希望描述这样一种 SIMD 类型:该类型的每个值都有相同的大小(不同于 `str` 和 `[T]`,后者每个值可以有不同长度),但这个大小直到运行时才能获知。
## 更丰富的 `Sized` 层级
我们真正想要的不是仅仅 `Sized` 或 `?Sized`,而是一个更丰富的层级结构。目前的计划大致如下:
```mermaid
flowchart TD
subgraph S["大小特质"]
Sized[["Sized (默认)"]] -- 扩展 --> MetadataSized
MetadataSized -- 扩展 --> MaybeSized
end
```
其中:
- `trait Sized` 表示所有值具有相同的大小,且该大小仅通过类型即可计算。
- `trait MetadataSized` 表示值可以具有不同的大小,且该大小可以通过值的引用所附带的元数据计算得出。例子包括 `[T]` 或 `dyn Trait`。
- `trait MaybeSized` 被所有值实现,但不提供关于值大小的任何信息。
两个注意事项:
1. 我排除了 Arm 的可扩展向量扩展如何适配这个问题,因为它是正交的。
2. 特质名称尚未确定。我使用的是我所理解的 libs-api 团队偏好的名称;它们不是我最喜欢的,但最终是拥有 stdlib 命名决定权的团队,所以我遵从他们的意见。²(https://smallcultfollowing.com/babysteps/blog/2026/06/09/only-bounds/#fn:2)
## 问题:`?Sized` 表示法无法扩展到这种层级
但现在我们遇到了一个问题。`?Sized` 表示法最初是基于这样一个想法:用户应该指定他们选择退出的是哪个默认边界——也就是说,`?` 意在表示“我不知道这是否是 `Sized`”(与默认情况相反,默认时你知道它是 `Sized`)。但“选择退出”一个边界在多层级结构中效果不佳。当你写 `?Sized` 时,它对应的是 `T: MetadataSized`(但不是 `T: Sized`)吗?如果我们后来想在 `T: MetadataSized` 和 `T: Sized` 之间再插入一个层级呢?那么我们要么必须改变 `T: ?Sized` 的含义(使其指向新的边界),要么就得让 `T: ?Sized` 在层级中下降 *两个* 级别。更令人头痛的是,当中间那一层还不稳定时我们该怎么办?显然 `T: ?Sized` 不应该指向一个不稳定的特质……如果我们决定移除它呢?
## 解决方案:`only` 边界
新的提案是使用 `T: only MetadataSized` 或 `T: only UnknownSized` 替代 `T: ?Sized`。一个 `only` 边界结合了两件事:
1. 像任何边界一样,它包含一个“最小需求”——即 `T: only MetadataSized` 意味着 `T` 至少要实现 `MetadataSized`。
2. 除此之外,它还禁用了某些*默认*边界——即我们*不会*添加默认的 `T: Sized` 边界。
名称 `only` 源自于这样一个事实:`T: Sized` 蕴含 `T: MetadataSized`。所以默认的 `T: Sized` 已经意味着 `T: MetadataSized` 是免费获得的;但当你写 `only MetadataSized` 时,你是在说“我不需要完整的层级,只要 `MetadataSized` 就够了”。
## `only` 边界像普通边界一样工作:要求你需要的东西
`only` 边界的一个好特性是它们更像常规的边界。`?` 边界说的是“我不需要这个”,而 `only` 边界说的是你*确实需要*什么。例如,如果你写一个函数只是引用 `T` 类型的值,而不关心它们的大小,你可以写:
```rust
fn by_ref(u: &U) where U: only MaybeSized, {}
```
如果你写一个函数*确实需要*计算 `V` 类型值的大小,你可以要求这个能力:
```rust
fn checks_size(v: &V) where V: only MetadataSized, {
std::mem::size_of_val(v)
}
```
## `only` 边界允许以后添加新的层级
`only` 边界的另一个好特性是,以后我们可以向层级中添加新层级,并且它们能正常工作。例如,假设我们想添加像 `Aligned` 这样的东西,其中*大小*在编译时未知,但*对齐*是已知的。我们可以将层级改为:
```rust
trait Sized: Aligned
trait Aligned: MetadataSized // <-- 新的!
trait MetadataSized: MaybeSized
trait MaybeSized
```
而带有 `U: only MaybeSized`(如 `by_ref`)和 `V: only MetadataSized`(如 `checks_size`)的函数将继续保持相同的需求。但新函数可以用 `T: only Aligned` 来编写,从而使用新的边界。并且不会与稳定化产生冲突;编写 `T: only Aligned` 的代码在中间层级最终确定之前可以被视为不稳定。
## `only` 边界正常组合
像任何其他边界一样,`only` 边界与其他边界组合形成整体需求。因此可以写出例如 `T: only MetadataSized + Sized`。这等价于 `T: Sized`,因此等价于默认行为,因而*有点*多余,但你完全可以这样写。类似地,鉴于 `trait Clone: Sized`,如果你写 `T: only MetadataSized + Clone`,那也是有点多余的:你不如直接写 `T: Clone`,两者等价。我们计划提供一个默认警告级别的 lint 来检测这种情况。
## 将 `only` 扩展到其他“默认边界族”(推测性)
`only` 边界的最终优势在于它们允许我们引入全新的*族系*的默认边界。一个例子是引入 `Move` 边界(https://smallcultfollowing.com/babysteps/blog/2025/10/21/move-destruct-leak/)的想法。请注意,这是一个独立的特性,并不包含在当前的 RFC(https://github.com/rust-lang/rfcs/pull/3729)中。
如今 Rust 中的所有类型都是“可移动的”和“可遗忘的”,这意味着只要停止使用原来的位置,你就可以将值从一个位置 memcpy 到另一个位置,*并且*你可以在不运行值析构函数的情况下回收它存储的内存。有一个显著的例外——当你固定一个值时,它就不能再被移动,并且在其内存被重用之前必须运行其析构函数——但除此之外,这是一个硬性规则。
这很烦人!问题在于,不能保证析构函数运行会阻碍许多不安全代码模式。例如,类似 `rayon` 的**作用域任务**依赖于析构函数来保证安全(https://smallcultfollowing.com/babysteps/blog/2016/10/02/observational-equivalence-and-unsafe-code/)。在同步代码中这是可行的,因为我们已经决定在不运行存储值析构函数的情况下展开栈帧是未定义行为,所以如果你在栈上放置一个局部变量,你可以确信其析构函数会运行。但这在 `async` 代码中行不通!而且有时在不运行析构函数的情况下展开也会很好。
解决方案是引入第二个默认特质族。与之前看到的 `Sized` 族不同,这个族定义了关于该类型的值如何使用的细粒度能力:
```mermaid
flowchart TD
subgraph A["可访问性特质"]
Forget[["Forget (默认)"]] -- 扩展 --> Leak
Leak -- 扩展 --> Destruct
Destruct -- 扩展 --> Access
Move[["Move (默认)"]] -- 扩展 --> Access
end
Copy -- 扩展 --> Move
```
这些特质的含义如下:
- `Forget`(默认)说你可以回收值的内存而不运行其析构函数。
- `Leak` 说你可以跳过运行值的析构函数,但前提是你从不重用该值所在的内存。
- `Destruct` 说如果你有一个此类型的值,你可以通过运行其析构函数来重用其所在的内存。
- `Copy`(已存在)说你可以 memcpy 该位置并继续使用原始位置;它不是真正的默认,但我把它包含进来是因为它相关。
- `Move`(另一个默认)说如果你停止使用原始位置,你可以将值 memcpy 到新位置。
- `Access` 是这个族的根。它表示一个可以“原地访问”的值(基本上任何值都可以)。
这会向编译器引入新的检查:
- 当你移动一个值(即 `a = b` 且之后不再使用 `b`)时,我们将检查该类型是否实现了 `Move`(而在今天,这总是允许的)。
- 当退出作用域时,我们将检查每个局部变量中的值要么已被移动,要么具有实现了 `Destruct` 的类型。
一些含义:
- 如果你的函数拥有一个 `T: only Destruct` 类型的值,那么你*必须在*函数返回前析构它。你不能移动它(因为不知道它是否实现 `Move`),也不能泄漏或遗忘它。
- 如果你的函数拥有一个 `T: only Move` 类型的值,那么你唯一能做的就是把它移到别处。你不能丢弃它(因为不知道它是否实现 `Destruct`)。
- 没有函数可以拥有一个 `T: only Access` 类型的值,因为你既不能移动它也不能丢弃它,因此无法返回。但你可以在(比如)`static` 中拥有这样的值。
## 在存在多个族的情况下,`only` 边界如何工作
写这篇博客的动机源于语言团队一次会议上的一个问题:在存在多个“默认特质族”(如我上文所述)的情况下,`only` 边界应该怎样工作?虽然当前的 RFC(https://github.com/rust-lang/rfcs/pull/3729)只关注 `Sized` 特质,但我们预计会在未来的 RFC 中考虑“可访问族”,因此我们希望确保不会做出任何难以同时覆盖两者的决策。
我设想的方式是这样的。每个默认特质都与一个或多个“族”相关联。当你有一个 `only` 边界时,它会从该特质关联的每个族中的所有默认特质中“选择退出”:
- `T: only Move` 从 `Forget`、`Leak`、`Destruct` 中退出——但不从 `Sized` 退出。
- `T: only Destruct` 从 `Forget`、`Leak` 和 `Move` 中退出——但不从 `Sized` 退出。
- `T: only MetadataSized` 从 `Sized` 中退出——但不从 `Forget` 或 `Move` 退出。
- `T: only MaybeSized` 从 `Sized` 中退出——但不从 `Forget` 或 `Move` 退出。
你可能还想要“重新加入”某些默认。例如,`T: only Move + Destruct` 是一个合理的事情。它意味着值可以被移动和析构,但不能被泄漏或遗忘。
## 示例
### `Option::map` 要求 `only Move`
`map` 是一个只需要 `Move` 的函数示例。你需要能够解构 `self`(这会将可选值*移动*到局部变量 `v` 中),然后调用闭包 `op`,这再次移动了包装的值 `v`:
```rust
impl<T> Option<T> {
fn map<U>(
self,
op: impl FnOnce(T) -> U,
) -> Option<U> {
match self {
Some(v) => Some(op(v)),
None => None,
}
}
}
```
有趣的一点是结果类型 `U`。仅使用我在这篇博客中写的内容,它需要是 `only Move`,因为结果将被移动到 `Some` 值中等等。但**原地初始化**(https://rust-lang.github.io/rust-project-goals/2026/in-place-init.html)将允许这个定义省略 `U: only Move` 边界,因为我们可以静态保证 `Option` 会被就地构造且之后不会被移动。
### `Option::or` 要求 `only Move + Destruct`
`Option` 上的 `a.or(b)` 方法在 `a` 是 `Some` 时返回 `a`,否则返回 `b`。这是一个有趣的例子,因为值 `b` 可能不被使用,因此要求 `only Move + Destruct` 边界。
```rust
impl<T> Option<T> {
fn or(
self,
alternate: Option<T>,
) -> Option<T>
where
T: Destruct, // <-- 因为它可能被丢弃
{
match self {
Some(v) => Some(v), // 丢弃 `alternate`
None => alternate, // 移动 `alternate`
}
}
}
```
### `Rc` 要求 `MaybeSized + Leak`
`Rc` 类型是一个我们希望从两个族中放宽边界的例子:
```rust
struct Rc<T: ?Sized> { ... }
```
我认为 `Rc` 合适的的最小边界是:
- `only MaybeSized`,因为虽然它可以存储 `MetadataSized` 或 `Sized` 的东西,但它不必如此,它也可以存储大小不可计算的东西(尽管这引出了如何释放它们的问题,但那是分配器关心的事)。
- `only Leak`,因为 `Rc` 值可能形成循环,因此我们永远无法保证析构函数会被运行。
有趣的是,即使其内容未实现 `Forget`,`Rc` 也能实现 `Forget`。
## 常见问题
### 当前 RFC 中到底包含什么?
这篇文章可能在这里有点令人困惑。**当前的 RFC**(https://github.com/rust-lang/rfcs/pull/3729)只关注提议的“Sized”特质。`Access` 族是一个推测性的未来扩展,我们正在探索,但阶段要早得多。
### 我可以将 `only` 用于*任何*特质吗?
一开始,计划是 `only` 只能用于众所周知的、*默认的*特质(例如 `Move`、`Sized` 等)。不过将来有一些想法将其泛化。
### 为什么不一次性从*所有*默认中退出?
曾经提出的一种替代方案是按类型参数进行退出。因此你可能要写类似:
```rust
fn foo<T:!>() // 某种语法
```
这将“退出”*所有*默认边界。显然我们需要讨论语法,但现在先忽略它。问题是退出*所有*默认是否比退出单个族更好。我倾向于按族退出,原因有两个:
- 首先,像 `T: only Move` 这样的例子表明,你很可能只想退出单个族而保留默认的 `Sized` 边界。我认为可能会有许多函数想要退出 `Sized` *或* `Forget` *但不是两者*。
- 你可能会想,我们可以通过 `Move: Sized` 来达到同样的效果,但我认为那会是个错误。值的大小必须动态计算的事实并不意味着它不能被移动——实际上,`[u32]` 可以被移动!因此,我们希望 `Move` 与 `Sized` 无关。
相似文章
关于整数的思考 (2023)
一篇博客文章,讨论了各种编程语言中整数类型的设计,认为 Rust 强制要求显式指定大小和符号的做法优于那些有默认 `int` 类型的语言。
安全变得简单 第1部分:单一所有权(并非)可选
本文介绍了一种基于线性类型和抽象解释的内存安全新方法,旨在比Rust更符合人机工程学原理地消除诸如释放后使用和内存泄漏等常见错误。
使用unsafe消除Go中的边界检查
本文解释了如何在Go中使用unsafe指针算术来消除编译器无法移除的边界检查,从而提升热点路径的性能。它讨论了边界检查的开销和传统的BCE方法,然后介绍了unsafe方法。
在Rust中的缓存感知数据布局:字段分区、伪共享与128字节规则
这篇博客文章解释了Rust中针对多线程结构的缓存感知数据布局,涵盖了字段分区和避免伪共享的128字节规则,并以一个SPSC环形缓冲区为例。
Rust 中的安全 SIMD,即使内部也安全
Rust 的 SIMD 抽象现在允许在不使用 unsafe 代码的情况下安全使用,这得益于 Rust 1.87 引入的 CPU 特性令牌,从而实现了简洁且可移植的向量操作。