|
1 | | -# marker trait 与 auto trait |
| 1 | +# Marker Trait 与 Auto Trait |
2 | 2 |
|
3 | | -> 原文:https://users.rust-lang.org/t/understanding-the-marker-traits/75625 |
| 3 | +在 Rust 的世界里,Trait 通常用来定义类型可以执行的**行为**(比如 `Display` Trait 定义了如何打印)。但有一类特殊的 Trait,它们内部没有任何方法,它们的存在不是为了定义行为,而是为了给类型贴上一个“标签”,表明这个类型具有某种**属性**。这就是 **标记 Trait (Marker Trait)**。 |
4 | 4 |
|
5 | | -在某种意义上,标记特征(marker trait)只是一个内部没有任何项的 trait。即使没有任何特殊的编译器支持,这有时也是有用的(例如 [sealed traits](https://rust-lang.github.io/api-guidelines/future-proofing.html))。 |
| 5 | +这篇文章将带你深入理解标记 Trait 和与之密切相关的自动 Trait (Auto Trait),它们是 Rust 安全性和并发能力的重要基石。 |
6 | 6 |
|
7 | | -从另一种意义上说,有一个不稳定的 `#[marker]` 属性,您可以将其放在 marker trait 上,以便选择加入 RFC 12684 的重叠实现(也是不稳定的)。 |
| 7 | +## 1. 什么是标记 Trait (Marker Trait)? |
8 | 8 |
|
9 | | -还有一种意思是 `std::marker` 中的东西。 |
10 | | -其中大多数是标记特征,许多也是 [auto traits](https://github.com/rust-lang/rust/issues/13231)(另一个不稳定的特性),而且几乎所有这些 trait 都有特殊的编译器行为。 |
| 9 | +从最基础的定义来看,标记 Trait 就是一个**空的 Trait**。 |
11 | 10 |
|
12 | | -`Send`、 `Sync` 和 `Unpin` 都是自动特征(auto trait),这意味着如果某个结构包含了全部实现该 trait 的字段,则该结构也会自动实现该 trait。这就是检查的范围。 |
13 | | -但是,您可以通过实现 `!Send`、 `!Sync` 或 `!Unpin` 来选择退出该 trait(opt out of the trait)。 |
14 | | -您还可以通过将 `PhantomPinned` 放入您的结构中来选择退出 `Unpin`(因为它是 `!Unpin` )。 |
15 | | -auto traits 的概念可能有一天会变得不那么特别(即稳定)。另一方面,我看到一些人对实现这种稳定持怀疑态度;时间会证明一切(time will tell)。 |
| 11 | +```rust |
| 12 | +// 一个最简单的标记 Trait |
| 13 | +trait MyMarkerTrait {} |
| 14 | +``` |
| 15 | + |
| 16 | +它的作用就像护照上的签证或产品上的“合格”标签。一个类型实现了这个 Trait,并不意味着它能“做”什么新事情,而是意味着它“是”什么,或者说它满足了某种特定的条件或属性。 |
16 | 17 |
|
17 | | -`Copy` 不是自动特征,但是实现 `Copy` 的能力是类似的 —— 你的所有字段也必也是具有 `Copy` 特征的。 |
18 | | -此外,您不能为实现 `Drop` 的类型实现 `Copy` 。它具有额外的特殊(语言级别)行为,因为 `Copy` 值的移动不是破坏性的(破坏性)(您仍然可以使用原始值)。 |
| 18 | +这种模式即便没有编译器的特殊支持也很有用。一个常见的例子是 [**"Sealed Trait"** 模式](https://rust-lang.github.io/api-guidelines/future-proofing.html),库的作者可以通过定义一个私有的标记 Trait,并要求公共 Trait 也必须实现这个私有 Trait,来防止库外部的用户为这个公共 Trait 实现自己的类型。 |
19 | 19 |
|
20 | | -`Sized` 是编译器通过 trait 公开的类型的一个固有属性(intrinsic property)。您声明泛型参数的位置基本上都有一个隐式的 `Sized` 约束。 |
21 | | -但是可以通过 `?Sized` 约束来移除该约束。 `Sized` 也用于表示“非动态特征”(non-dyn Trait), |
22 | | -尽管在我看来这是一种 hack,独特的编译器支持的标记特性将是更好的解决方案。 |
| 20 | +## 2. 编译器“魔法”加持:特殊的标记 Trait |
23 | 21 |
|
24 | | -我相信所有其他实验标记特征都是实现细节机制,以实现语言功能,比如缩小大小(例如,从数组到切片,或从基本类型到 `dyn Trait`),模式匹配(例如,不能依赖于 `Eq trait` 的实现)等。 |
25 | | -有时检查文档仍然可以帮助解释语言行为(例如,为什么你不能将深度嵌套的类型强制转换为 `dyn Trait`)。 |
| 22 | +Rust 中有一些在 `std::marker` 模块内的标记 Trait,它们被编译器赋予了特殊的意义和行为。 |
26 | 23 |
|
27 | 24 | 还有其他不在 `std::marker` 模块中的自动/标记特征,比如 [UnwindSafe](https://doc.rust-lang.org/stable/std/panic/trait.UnwindSafe.html) 。 |
28 | 25 |
|
29 | | -还有其他非标记特征在语言中具有特殊作用,例如 Drop。 |
| 26 | +它们是 Rust 语言核心特性的一部分。让我们来逐一认识其中最重要的几个。 |
30 | 27 |
|
31 | | -[`PhantomData<T>`](https://doc.rust-lang.org/std/marker/struct.PhantomData.html) 是一种标记类型(marker type),具有特殊的编译器行为,“就像它拥有一个 `T` ”一样。 |
32 | | -它是一个标记,因为它的大小为零,不影响对齐(alignment),通过重要的标准特征,所以你仍然可以 `derive` 它们,等等。 |
33 | 28 |
|
34 | | -其实,还是有点取决于你的意思。没有 `Send` 能力的运行时检查;trait 的实现(或不实现)是编译时的决定。 |
| 29 | +### 2.1. [自动 Trait (Auto Traits)]((https://github.com/rust-lang/rust/issues/13231)) - `Send` 和 `Sync` |
35 | 30 |
|
36 | | -但是,如果标记特征是 `dyn` 安全的(如果一个标记特征有一个 supertrait 是`dyn`不安全的,那么它可能是 `dyn` 不安全的 —— 例如 `Copy`),则没有规则可以将其转换为 `dyn Trait`。 |
37 | | -这可能是你在运行时与之交互的东西,例如向下转换时。 |
| 31 | +这是最常见的一类特殊标记 Trait。 |
38 | 32 |
|
39 | | -事实上, auto traits 在这里也很特别,因为你不能有 `dyn NonAutoOne + NonAutoTwo`,但你可以有 `dyn NonAuto + Send + Sync + Unpin + AnyNumberOfAutoTraits`。 |
40 | | -`dyn Error` 与 `dyn Error + Send + Sync` 是不同的类型。 |
| 33 | +* **`Send`**: 如果一个类型 `T` 实现了 `Send`,意味着它的**所有权可以安全地从一个线程转移到另一个线程**。可以把它想象成一个“可邮寄”的包裹。 |
| 34 | +* **`Sync`**: 如果一个类型 `T` 实现了 `Sync`,意味着它可以在多个线程之间安全地**共享引用** (`&T`)。可以把它想象成一份存储在云端的文档,多人可以同时安全地“只读”它。 |
41 | 35 |
|
42 | | -此外,由于标记特征还可以具有 supertrait 或其他约束,它仍然可以作为一个指标,来调用某些方法[调用某些方法](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e5020a79a405f32792bec51031efa586): |
| 36 | +**什么是“自动” (Auto)?** |
| 37 | + |
| 38 | +`Send` 和 `Sync` 的“自动”特性意味着:**如果一个结构体或枚举的所有字段(成员)都实现了 `Send`,那么这个结构体/枚举也会自动地实现 `Send`。`Sync` 也是同理。** |
| 39 | + |
| 40 | +这极大地提升了便利性。你不需要手动为你的每一个数据结构去 `impl Send`,编译器会为你自动推导。 |
43 | 41 |
|
44 | 42 | ```rust |
45 | | -pub fn f<T: Copy>(t: T) -> T{ |
46 | | - t.clone() |
| 43 | +// String 和 i32 都实现了 Send 和 Sync |
| 44 | +struct MyData { |
| 45 | + name: String, |
| 46 | + count: i32, |
47 | 47 | } |
| 48 | +// 因此,MyData 会自动实现 Send 和 Sync,无需我们手动编写! |
48 | 49 | ``` |
49 | 50 |
|
50 | | -*(我猜这就是“编译之外的重要”的部分含义)* |
| 51 | +**选择退出 (Opt-out)** |
51 | 52 |
|
52 | | -当你有一个复杂的约束,又不想进行太多的重复操作时,就会很有用: |
| 53 | +然而,某些类型天生就不是线程安全的,比如原始指针 `*mut T`。如果你的结构体包含了这样的字段,编译器就会正确地推断出你的结构体**不是** `Send` 或 `Sync` 的。 |
53 | 54 |
|
54 | 55 | ```rust |
55 | | -pub trait DoesALot: This + That + Clone + Send + Deref {} |
56 | | -impl<T: This + That + Clone + Send + Deref> DoesALot for T {} |
| 56 | +use std::rc::Rc; |
| 57 | + |
| 58 | +// Rc<T> 设计为单线程使用,它没有实现 Send 和 Sync |
| 59 | +struct NotThreadSafe { |
| 60 | + data: Rc<String>, // Rc 不是 Send/Sync |
| 61 | +} |
| 62 | +// 因此,NotThreadSafe 也不会自动实现 Send 和 Sync |
57 | 63 | ``` |
58 | 64 |
|
59 | | -还需要提一下的事情是,虽然 `Send` 和 `Sync` 作为自动特征具有特殊行为,但实际的线程安全部分主要由库代码处理。特别是 `std::thread::spawn` 具有约束: |
| 65 | +在极少数情况下,你可能需要手动告诉编译器,你的类型(即使它内部的字段都是 `Send`/`Sync`)由于某些逻辑原因,不应该是线程安全的。这时你可以使用负向实现(Negative Impl): |
| 66 | + |
| 67 | +```rust |
| 68 | +use std::marker::PhantomData; |
| 69 | + |
| 70 | +struct MySpecialType<T> { |
| 71 | + // ... 字段都是 Send/Sync |
| 72 | + _marker: PhantomData<*const T>, // 使用 PhantomData 模拟包含不安全指针 |
| 73 | +} |
| 74 | + |
| 75 | +// 即使 MySpecialType 的字段都是 Send,我们也可以手动选择退出 |
| 76 | +// impl !Send for MySpecialType {} // 注意:这目前是不稳定语法 |
| 77 | +``` |
| 78 | + |
| 79 | +### 2.2. 有条件的标记 Trait - `Copy` |
| 80 | + |
| 81 | +`Copy` Trait 表明一个类型的值在赋值时,会进行**按位复制 (bitwise copy)**,而不是**移动 (move)**。像 `i32`、`f64`、`bool` 这些简单的栈上类型都是 `Copy` 的。 |
| 82 | + |
| 83 | +`Copy` 与 `Send`/`Sync` 有两个关键不同: |
| 84 | + |
| 85 | +1. **它不是自动的**:你必须显式地使用 `#[derive(Copy)]` 来实现它(当然,前提是满足条件)。 |
| 86 | +2. **实现有严格条件**: |
| 87 | + * 一个类型的所有字段都必须实现 `Copy`。 |
| 88 | + * 该类型不能实现 `Drop` Trait。因为如果一个类型需要自定义的清理逻辑(`Drop`),那么简单的按位复制就会导致资源管理问题(如二次释放)。 |
| 89 | + |
| 90 | +```rust |
| 91 | +#[derive(Clone, Copy)] // 必须同时 derive Clone,因为 Copy 依赖 Clone |
| 92 | +struct Point { |
| 93 | + x: i32, |
| 94 | + y: i32, |
| 95 | +} |
| 96 | + |
| 97 | +// Vec<T> 拥有堆上的内存,需要管理,它没有实现 Copy |
| 98 | +// struct PointVec { |
| 99 | +// points: Vec<Point>, |
| 100 | +// } |
| 101 | +// #[derive(Copy)] // ❌ 无法编译!因为 Vec<Point> 不是 Copy |
| 102 | +``` |
| 103 | + |
| 104 | +### 2.3. 无处不在的标记 Trait - `Sized` |
| 105 | + |
| 106 | +`Sized` 是一个非常基础的标记 Trait,它表示一个类型在**编译时具有已知的大小**。 |
| 107 | + |
| 108 | +* **几乎所有类型都是 `Sized`**:`i32` (4字节),`bool` (1字节),你定义的 `struct` 等等。编译器在处理泛型时,默认就会假定类型参数是 `Sized` 的。 |
| 109 | + ```rust |
| 110 | + // T 实际上有一个隐藏的约束: T: Sized |
| 111 | + fn process<T>(value: T) { /* ... */ } |
| 112 | + ``` |
| 113 | +* **什么不是 `Sized`?**:最常见的例子是切片 `[T]` 和字符串切片 `str`,因为它们的长度是动态的。Trait 对象 `dyn Trait` 也是动态大小的。 |
| 114 | +* **如何处理非 `Sized` 类型?**:我们不能直接在栈上创建非 `Sized` 的值,但可以通过**引用**或**智能指针**(如 `&`、`Box`)来使用它们。通过 `?Sized` 语法,我们可以告诉编译器,一个泛型参数**可能不是** `Sized` 的。 |
| 115 | + ```rust |
| 116 | + // 通过 ?Sized 移除默认的 Sized 约束 |
| 117 | + fn process_dynamically<T: ?Sized>(value: &T) { /* ... */ } |
| 118 | + ``` |
| 119 | + |
| 120 | +## 3. 标记 Trait 的实际应用与意义 |
| 121 | + |
| 122 | +理解了这些概念后,我们来看看它们在实际编程中是如何发挥作用的。 |
| 123 | + |
| 124 | +### 3.1. 作为泛型约束,保证安全 |
| 125 | + |
| 126 | +这是标记 Trait 最核心的应用。例如,标准库的线程创建函数 `std::thread::spawn` 的签名: |
60 | 127 |
|
61 | 128 | ```rust |
62 | 129 | pub fn spawn<F, T>(f: F) -> JoinHandle<T> |
63 | 130 | where |
64 | | - F: FnOnce() -> T, |
65 | | - F: Send + 'static, |
| 131 | + F: FnOnce() -> T + Send + 'static, |
66 | 132 | T: Send + 'static, |
67 | 133 | ``` |
68 | 134 |
|
69 | | -F(发送到新线程的函数)和 T(从线程返回的值)的这些 `Send` 约束实际上阻止了您向新线程发送值。 |
70 | | -通常,任何线程创建或线程间通信工具都会对其携带的值进行 `Send` 约束。 |
| 135 | +这里的 `F: Send` 和 `T: Send` 约束至关重要。它在**编译时**就保证了你传递给新线程的闭包 `F` 和它返回的值 `T` 都是可以安全地跨线程传递的。如果没有这个约束,就可能在运行时发生数据竞争等内存安全问题。 |
| 136 | + |
| 137 | +### 3.2. 组合与抽象 |
| 138 | + |
| 139 | +当你有一系列复杂的泛型约束时,可以定义一个空的 Trait 来聚合它们,使代码更整洁。 |
71 | 140 |
|
| 141 | +```rust |
| 142 | +// 定义一个聚合了多个常用 Trait 的新 Trait |
| 143 | +pub trait DoesALot: Clone + Send + std::fmt::Debug {} |
| 144 | + |
| 145 | +// 自动为所有满足条件的类型实现这个 Trait |
| 146 | +impl<T: Clone + Send + std::fmt::Debug> DoesALot for T {} |
| 147 | + |
| 148 | +// 现在函数签名可以变得更简洁 |
| 149 | +fn complex_function<T: DoesALot>(item: T) { |
| 150 | + // ... |
| 151 | +} |
| 152 | +``` |
72 | 153 |
|
73 | | -许多其他线程安全规则也被表示为 trait 实现。例如,`Send` 和 `Sync` 之间的基本关系是[一个 impl 本身](https://doc.rust-lang.org/src/core/marker.rs.html#51): |
| 154 | +### 3.3. 在 `dyn Trait` 中组合属性 |
| 155 | + |
| 156 | +自动 Trait 在 trait 对象中也扮演了特殊角色。你可以将一个非自动 Trait 与多个自动 Trait 组合成一个 `dyn Trait`。 |
74 | 157 |
|
75 | 158 | ```rust |
76 | | -unsafe impl<T: Sync + ?Sized> Send for &T {} |
| 159 | +// 这是合法的,因为 Send 和 Sync 是自动 Trait |
| 160 | +let my_error: Box<dyn std::error::Error + Send + Sync> = /* ... */; |
| 161 | + |
| 162 | +// `dyn Error` 和 `dyn Error + Send + Sync` 是不同的类型, |
| 163 | +// 后者可以安全地在线程间共享。 |
77 | 164 | ``` |
78 | 165 |
|
79 | | -有时它可能是偷偷摸摸的;例如,在 `std::sync::mpsc` 中,你可以完美地为任何类型使用一个通道(channel),即使是非 `Send` 类型,但如果消息类型不是 `Send`,你就不能发送通道的末端,因此整个通道无法离开单个线程。 |
| 166 | +## 4. 相关概念:`PhantomData` - 标记“类型” |
| 167 | + |
| 168 | +与标记 Trait 类似,`std::marker::PhantomData<T>` 是一个**零大小的标记类型 (Marker Type)**。它本身不占用任何内存,但它在编译时“假装”自己拥有一个类型为 `T` 的数据。 |
| 169 | + |
| 170 | +它的主要用途是:**向编译器传达关于泛型参数的所有权、生命周期或 drop-check 等信息,即使你的结构体实际上并不直接存储这个类型的数据。** 这在编写不安全的底层代码时尤其重要,可以帮助我们利用 Rust 的安全检查机制。 |
| 171 | + |
| 172 | +## 总结 |
80 | 173 |
|
81 | | -在所有这些情况下,编译器的特殊功能根本不是确保线程安全所必需的;编译器所做的是通过在可能的情况下对由 `Send` 或 `Sync` 部分组成的数据结构实现 `Send` 和 `Sync` 来创造便利。 |
82 | | -如果 `Send` 和 `Sync` 不是 auto traits,那么语言基本上是一样的;但是你会花费更多的时间来为这些 traits 添加 `derives` 或 `impls`(并且偶尔会对忘记它们的库提交错误)。 |
| 174 | +- **标记 Trait** 是一个空的 Trait,用于给类型**添加属性标签**,而非行为。 |
| 175 | +- **自动 Trait** (`Send`, `Sync`) 是一种特殊的标记 Trait,如果一个复合类型的所有成员都具备该属性,它就会被**自动实现**。 |
| 176 | +- **`Copy`** 是一个有条件的标记 Trait,需要显式 `derive`,并且与 `Drop` 互斥,它改变了类型的赋值行为(从移动变为复制)。 |
| 177 | +- **`Sized`** 是一个几乎无处不在的标记 Trait,用于标识编译时大小已知的类型,它是理解 `dyn Trait` 和动态分发的关键。 |
| 178 | +- 这些 Trait 的核心价值在于它们是 Rust **泛型系统和安全保证**(尤其是线程安全)的基石。它们在编译时强制执行规则,将潜在的运行时错误转化为编译错误,这正是 Rust 强大可靠的原因之一。 |
0 commit comments