Skip to content

marker trait 与 auto trait

原文:https://users.rust-lang.org/t/understanding-the-marker-traits/75625

在某种意义上,标记特征(marker trait)只是一个内部没有任何项的 trait。即使没有任何特殊的编译器支持,这有时也是有用的(例如 sealed traits)。

从另一种意义上说,有一个不稳定的#[marker]属性,您可以将其放在 marker trait 上,以便选择加入 RFC 12684 的重叠实现(也是不稳定的)。

还有一种意思是 std::marker 中的东西。 其中大多数是标记特征,许多也是 auto traits(另一个不稳定的特性),而且几乎所有这些 trait 都有特殊的编译器行为。

SendSyncUnpin 都是自动特征(auto trait),这意味着如果某个结构包含了全部实现该 trait 的字段,则该结构也会自动实现该 trait。这就是检查的范围。 但是,您可以通过实现!Send!Sync!Unpin来选择退出该 trait(opt out of the trait)。 您还可以通过将PhantomPinned放入您的结构中来选择退出Unpin(因为它是!Unpin)。 auto traits 的概念可能有一天会变得不那么特别(即稳定)。另一方面,我看到一些人对实现这种稳定持怀疑态度;时间会证明一切(time will tell)。

Copy 不是自动特征,但是实现 Copy 的能力是类似的 —— 你的所有字段也必也是具有Copy特征的。 此外,您不能为实现 Drop 的类型实现 Copy 。它具有额外的特殊(语言级别)行为,因为 Copy 值的移动不是破坏性的(破坏性)(您仍然可以使用原始值)。

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

我相信所有其他实验标记特征都是实现细节机制,以实现语言功能,比如缩小大小(例如,从数组到切片,或从基本类型到 dyn Trait),模式匹配(例如,不能依赖于Eq trait的实现)等。 有时检查文档仍然可以帮助解释语言行为(例如,为什么你不能将深度嵌套的类型强制转换为dyn Trait)。

还有其他不在 std::marker 模块中的自动/标记特征,比如 UnwindSafe

还有其他非标记特征在语言中具有特殊作用,例如 Drop。

PhantomData<T> 是一种标记类型(marker type),具有特殊的编译器行为,“就像它拥有一个T”一样。 它是一个标记,因为它的大小为零,不影响对齐(alignment),通过重要的标准特征,所以你仍然可以derive它们,等等。

其实,还是有点取决于你的意思。没有Send能力的运行时检查;trait 的实现(或不实现)是编译时的决定。

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

事实上, auto traits 在这里也很特别,因为你不能有dyn NonAutoOne + NonAutoTwo,但你可以有dyn NonAuto + Send + Sync + Unpin + AnyNumberOfAutoTraitsdyn Errordyn Error + Send + Sync是不同的类型。

此外,由于标记特征还可以具有 supertrait 或其他约束,它仍然可以作为一个指标,来调用某些方法调用某些方法

rust
pub fn f<T: Copy>(t: T) -> T{
    t.clone()
}

(我猜这就是“编译之外的重要”的部分含义)

当你有一个复杂的约束,又不想进行太多的重复操作时,就会很有用:

rust
pub trait DoesALot: This + That + Clone + Send + Deref {}
impl<T: This + That + Clone + Send + Deref> DoesALot for T {}

还需要提一下的事情是,虽然 SendSync 作为自动特征具有特殊行为,但实际的线程安全部分主要由库代码处理。特别是 std::thread::spawn 具有约束:

rust
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static,

F(发送到新线程的函数)和 T(从线程返回的值)的这些 Send 约束实际上阻止了您向新线程发送值。 通常,任何线程创建或线程间通信工具都会对其携带的值进行 Send 约束。

许多其他线程安全规则也被表示为 trait 实现。例如,SendSync 之间的基本关系是一个 impl 本身

rust
unsafe impl<T: Sync + ?Sized> Send for &T {}

有时它可能是偷偷摸摸的;例如,在 std::sync::mpsc 中,你可以完美地为任何类型使用一个通道(channel),即使是非Send类型,但如果消息类型不是Send,你就不能发送通道的末端,因此整个通道无法离开单个线程。

在所有这些情况下,编译器的特殊功能根本不是确保线程安全所必需的;编译器所做的是通过在可能的情况下对由SendSync部分组成的数据结构实现SendSync来创造便利。 如果SendSync不是 auto traits,那么语言基本上是一样的;但是你会花费更多的时间来为这些 traits 添加 derivesimpls(并且偶尔会对忘记它们的库提交错误)。

Released under the MIT License