Skip to content

Commit cff862d

Browse files
committed
feat: 🎸 更新 marker trait
1 parent 4e99c72 commit cff862d

4 files changed

Lines changed: 149 additions & 51 deletions

File tree

docs/notes/marker trait.md

Lines changed: 138 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,178 @@
1-
# marker traitauto trait
1+
# Marker TraitAuto Trait
22

3-
> 原文:https://users.rust-lang.org/t/understanding-the-marker-traits/75625
3+
在 Rust 的世界里,Trait 通常用来定义类型可以执行的**行为**(比如 `Display` Trait 定义了如何打印)。但有一类特殊的 Trait,它们内部没有任何方法,它们的存在不是为了定义行为,而是为了给类型贴上一个“标签”,表明这个类型具有某种**属性**。这就是 **标记 Trait (Marker Trait)**
44

5-
在某种意义上,标记特征(marker trait)只是一个内部没有任何项的 trait。即使没有任何特殊的编译器支持,这有时也是有用的(例如 [sealed traits](https://rust-lang.github.io/api-guidelines/future-proofing.html)
5+
这篇文章将带你深入理解标记 Trait 和与之密切相关的自动 Trait (Auto Trait),它们是 Rust 安全性和并发能力的重要基石
66

7-
从另一种意义上说,有一个不稳定的 `#[marker]` 属性,您可以将其放在 marker trait 上,以便选择加入 RFC 12684 的重叠实现(也是不稳定的)。
7+
## 1. 什么是标记 Trait (Marker Trait)?
88

9-
还有一种意思是 `std::marker` 中的东西。
10-
其中大多数是标记特征,许多也是 [auto traits](https://github.com/rust-lang/rust/issues/13231)(另一个不稳定的特性),而且几乎所有这些 trait 都有特殊的编译器行为。
9+
从最基础的定义来看,标记 Trait 就是一个**空的 Trait**
1110

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,并不意味着它能“做”什么新事情,而是意味着它“是”什么,或者说它满足了某种特定的条件或属性。
1617

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 实现自己的类型。
1919

20-
`Sized` 是编译器通过 trait 公开的类型的一个固有属性(intrinsic property)。您声明泛型参数的位置基本上都有一个隐式的 `Sized` 约束。
21-
但是可以通过 `?Sized` 约束来移除该约束。 `Sized` 也用于表示“非动态特征”(non-dyn Trait),
22-
尽管在我看来这是一种 hack,独特的编译器支持的标记特性将是更好的解决方案。
20+
## 2. 编译器“魔法”加持:特殊的标记 Trait
2321

24-
我相信所有其他实验标记特征都是实现细节机制,以实现语言功能,比如缩小大小(例如,从数组到切片,或从基本类型到 `dyn Trait`),模式匹配(例如,不能依赖于 `Eq trait` 的实现)等。
25-
有时检查文档仍然可以帮助解释语言行为(例如,为什么你不能将深度嵌套的类型强制转换为 `dyn Trait`)。
22+
Rust 中有一些在 `std::marker` 模块内的标记 Trait,它们被编译器赋予了特殊的意义和行为。
2623

2724
还有其他不在 `std::marker` 模块中的自动/标记特征,比如 [UnwindSafe](https://doc.rust-lang.org/stable/std/panic/trait.UnwindSafe.html)
2825

29-
还有其他非标记特征在语言中具有特殊作用,例如 Drop
26+
它们是 Rust 语言核心特性的一部分。让我们来逐一认识其中最重要的几个
3027

31-
[`PhantomData<T>`](https://doc.rust-lang.org/std/marker/struct.PhantomData.html) 是一种标记类型(marker type),具有特殊的编译器行为,“就像它拥有一个 `T` ”一样。
32-
它是一个标记,因为它的大小为零,不影响对齐(alignment),通过重要的标准特征,所以你仍然可以 `derive` 它们,等等。
3328

34-
其实,还是有点取决于你的意思。没有 `Send` 能力的运行时检查;trait 的实现(或不实现)是编译时的决定。
29+
### 2.1. [自动 Trait (Auto Traits)]((https://github.com/rust-lang/rust/issues/13231)) - `Send` `Sync`
3530

36-
但是,如果标记特征是 `dyn` 安全的(如果一个标记特征有一个 supertrait 是`dyn`不安全的,那么它可能是 `dyn` 不安全的 —— 例如 `Copy`),则没有规则可以将其转换为 `dyn Trait`
37-
这可能是你在运行时与之交互的东西,例如向下转换时。
31+
这是最常见的一类特殊标记 Trait。
3832

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`)。可以把它想象成一份存储在云端的文档,多人可以同时安全地“只读”它
4135

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`,编译器会为你自动推导。
4341

4442
```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,
4747
}
48+
// 因此,MyData 会自动实现 Send 和 Sync,无需我们手动编写!
4849
```
4950

50-
*(我猜这就是“编译之外的重要”的部分含义)*
51+
**选择退出 (Opt-out)**
5152

52-
当你有一个复杂的约束,又不想进行太多的重复操作时,就会很有用:
53+
然而,某些类型天生就不是线程安全的,比如原始指针 `*mut T`。如果你的结构体包含了这样的字段,编译器就会正确地推断出你的结构体**不是** `Send``Sync` 的。
5354

5455
```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
5763
```
5864

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` 的签名:
60127

61128
```rust
62129
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
63130
where
64-
F: FnOnce() -> T,
65-
F: Send + 'static,
131+
F: FnOnce() -> T + Send + 'static,
66132
T: Send + 'static,
67133
```
68134

69-
F(发送到新线程的函数)和 T(从线程返回的值)的这些 `Send` 约束实际上阻止了您向新线程发送值。
70-
通常,任何线程创建或线程间通信工具都会对其携带的值进行 `Send` 约束。
135+
这里的 `F: Send` 和 `T: Send` 约束至关重要。它在**编译时**就保证了你传递给新线程的闭包 `F` 和它返回的值 `T` 都是可以安全地跨线程传递的。如果没有这个约束,就可能在运行时发生数据竞争等内存安全问题。
136+
137+
### 3.2. 组合与抽象
138+
139+
当你有一系列复杂的泛型约束时,可以定义一个空的 Trait 来聚合它们,使代码更整洁。
71140

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+
```
72153

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`
74157

75158
```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+
// 后者可以安全地在线程间共享。
77164
```
78165

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+
## 总结
80173

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 强大可靠的原因之一。

docs/第 1 章 步入Rust的世界(第一部分 基础知识)/1.0 设计哲学.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,15 @@ fn main() {
118118

119119
在前面的代码中,我们能够在对`value`调用`lock()`之后修改数据。 Rust 使用保护共享数据本身而不是代码的概念。与`Mutex`和受保护数据的交互不是独立的,就像 C++ 那样。 您必须在`Mutex`类型上`lock`才能访问内部数据。 那释放`lock`呢? 好吧,调用`lock()`返回一个称为`MutexGuard`的东西,当变量超出作用域(scope)时,它将自动释放锁。
120120

121-
它是 Rust 提供的许多安全的并发抽象之一,我们将在后面章节中对其进行详细介绍。标记特征(marker traits)的概念是另一个新颖的概念,它可以在编译时验证并确保对并发代码中的数据进行同步以及安全的访问。
122-
类型分别用称为`Send``Sync`的标记特征(marker trait)进行注释,以指示它们是安全发送到线程还是安全在线程之间共享。
121+
它是 Rust 提供的许多安全的并发抽象之一,我们将在后面章节中对其进行详细介绍。标记特征(Marker Trait)的概念是另一个新颖的概念,它可以在编译时验证并确保对并发代码中的数据进行同步以及安全的访问。
122+
类型分别用称为`Send``Sync`的标记特征(Marker Trait)进行注释,以指示它们是安全发送到线程还是安全在线程之间共享。
123123
当程序将值发送给线程时,编译器会检查该值是否实现了所需的标记特征,如果不是,则禁止使用该值。
124124
通过这种方式,Rust允许您毫无顾虑地编写并发代码,其中编译器在编译时会在多线程代码中捕获错误。
125125
编写并发代码已经很困难。 使用C / C ++,它变得更加困难和神秘。 CPU 的时钟频率没有再提高; 相反,CPU 升级带来了更多核心。 因此,并发编程是前进的方向。
126126
Rust 使编写并发代码变得轻而易举,并降低了许多人编写安全的并发代码的门槛。
127127

128-
Rust 还使用 C++ 的 RAII 习惯用法进行资源初始化。该技术基本上将资源的生存期与对象的生存期联系在一起,而堆分配类型的释放是通过`drop`特性提供的`drop`方法执行的。 当变量超出作用域(scope)时,将自动调用此方法。 它还用`Result``Option`类型替换了**空指针**的概念。 这意味着 Rust 不允许代码中包含`null/undefined`值,除非通过外部函数接口与其他语言进行交互以及使用不安全的代码(unsafe code)时除外。 该语言还强调组合而不是继承,并具有一个特征系统(trait system),该特征系统由数据类型实现,类似于 Haskell typeclasses(类型类),也称为更带劲的 Java 接口(Java interfaces on steroids)。Rust 的Traits(特征)是其许多功能的支柱,我们将在接下来的章节中看到。
128+
Rust 还使用 C++ 的 RAII 习惯用法进行资源初始化。该技术基本上将资源的生存期与对象的生存期联系在一起,而堆分配类型的释放是通过`drop`特性提供的`drop`方法执行的。 当变量超出作用域(scope)时,将自动调用此方法。 它还用`Result``Option`类型替换了**空指针**的概念。 这意味着 Rust 不允许代码中包含`null/undefined`值,除非通过外部函数接口与其他语言进行交互以及使用不安全的代码(unsafe code)时除外。 该语言还强调组合而不是继承,并具有一个特征系统(trait system),该特征系统由数据类型实现,类似于 Haskell typeclasses(类型类),也称为更带劲的 Java 接口(Java interfaces on steroids)。Rust 的 Traits(特征)是其许多功能的支柱,我们将在接下来的章节中看到。
129129

130-
最后但同样重要的是,Rust 的社区非常活跃且友好,并且该语言具有全面的[文档](https://doc.rust-lang.org)。 连续三年(2016年,2017年和2018年),Stack Overflow 的开发人员调查显示了 Rust 是最受欢迎的编程语言,因此可以说,整个编程社区对此非常感兴趣。 综上所述,如果你的目标是编写出性能高、bug 少的软件,同时享受许多现代语言特性和一个很棒的社区,那么你应该关注 Rust。
130+
最后但同样重要的是,Rust 的社区非常活跃且友好,并且该语言具有全面的[文档](https://doc.rust-lang.org)。 连续三年(2016年,2017年和2018年),Stack Overflow 的开发人员调查显示了 Rust 是最受欢迎的编程语言,因此可以说,整个编程社区对此非常感兴趣。
131+
132+
综上所述,如果你的目标是编写出性能高、bug 少的软件,同时享受许多现代语言特性和一个很棒的社区,那么你应该关注 Rust。

docs/第 28 章 详解 Send 和 Sync/28.3 自动推理.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 28.3 自动推理
22

3-
Send 和 Sync 就是一种常见的标记 traits(marker traits)。
3+
Send 和 Sync 就是一种常见的标记 traits(Marker Trait)。
44
它们并没有任何方法或关联类型,仅仅标记了该类型可以被安全地在多个线程之间共享(Sync)或传递(Send)。
55
使用这些标记 traits,编译器可以在编译时对类型进行检查,确保它们可以安全地在多个线程中使用。
66

0 commit comments

Comments
 (0)