联合体
unions.md
commit: 7c6e0c00aaa043c89e0d9f07e78999268e8ac054
本章译文最后维护日期:2021-2-10
句法
Union :
union
IDENTIFIER GenericParams? WhereClause?{
StructFields}
除了用 union
代替 struct
外,联合体声明使用和结构体声明相同的句法。
#![allow(unused)] fn main() { #[repr(C)] union MyUnion { f1: u32, f2: f32, } }
联合体的关键特性是联合体的所有字段共享同一段存储。因此,对联合体的一个字段的写操作会覆盖其他字段,而联合体的尺寸由其尺寸最大的字段的尺寸所决定。
联合体的初始化
可以使用与结构体类型相同的句法创建联合体类型的值,但必须只能指定一个字段:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; }
上面的表达式创建了一个类型为 MyUnion
的值,并使用字段 f1
初始化了其存储。可以使用与结构体字段相同的句法访问联合体:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; let f = unsafe { u.f1 }; }
读写联合体字段
联合体没有“活跃字段(active field)”的概念。相反,每次访问联合体只是用访问所指定的字段的类型解释此联合体的存储。读取联合体的字段就是以当前读取字段的类型来解读此联合体的存储位。字段(间)可以有非零的偏移量存在(使用 C表型的除外);在这种情况下,读取将从字段的相对偏移量的 bit 开始。程序员有责任确保此数据在当前字段类型下有效。否则会导致未定义行为(undefined behavior)。例如,在 bool
类型的字段下读取到数值 3
是未定义行为。实际上,对一个 C表型的联合体进行写操作,然后再从中读取,就好比从用于写入的类型到用于读取的类型的 transmute
操作。
因此,所有的联合体字段的读取必须放在非安全(unsafe
)块里:
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; unsafe { let f = u.f1; } }
对实现了 Copy
trait 或 [ManuallyDrop
][ManuallyDrop] trait 的联合体字段的写操作不需要事先的析构读操作,也因此这些写操作不必放在非安全(unsafe
)块中。1
#![allow(unused)] fn main() { use std::mem::ManuallyDrop; union MyUnion { f1: u32, f2: ManuallyDrop<String> } let mut u = MyUnion { f1: 1 }; // 这些都不是必须要放在 `unsafe` 里的 u.f1 = 2; u.f2 = ManuallyDrop::new(String::from("example")); }
通常,那些用到联合体的程序代码会先在非安全的联合体字段访问操作上提供一层安全包装,然后再使用。
联合体和 Drop
当一个联合体被销毁时,它无法知道需要销毁它的哪些字段。因此,所有联合体的字段都必须实现 Copy
trait 或被包装进 ManuallyDrop<_>
。这确保了联合体在超出作用域时不需要销毁任何内容。
与结构体和枚举一样,联合体也可以通过 impl Drop
手动定义被销毁时的具体动作。
联合体上的模式匹配
访问联合体字段的另一种方法是使用模式匹配。联合体字段上的模式匹配与结构体上的模式匹配使用相同的句法,只是这种模式只能一次指定一个字段。因为模式匹配就像使用特定字段来读取联合体,所以它也必须被放在非安全(unsafe
)块中。
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } fn f(u: MyUnion) { unsafe { match u { MyUnion { f1: 10 } => { println!("ten"); } MyUnion { f2 } => { println!("{}", f2); } } } } }
模式匹配可以将联合体作为更大的数据结构的一个字段进行匹配。特别是,当使用 Rust 联合体通过 FFI 实现 C标签联合体(C tagged union)时,这允许同时在标签和相应字段上进行匹配:
#![allow(unused)] fn main() { #[repr(u32)] enum Tag { I, F } #[repr(C)] union U { i: i32, f: f32, } #[repr(C)] struct Value { tag: Tag, u: U, } fn is_zero(v: Value) -> bool { unsafe { match v { Value { tag: Tag::I, u: U { i: 0 } } => true, Value { tag: Tag::F, u: U { f: num } } if num == 0.0 => true, _ => false, } } } }
引用联合体字段
由于联合体字段共享存储,因此拥有对联合体一个字段的写访问权就同时拥有了对其所有其他字段的写访问权。因为这一事实,引用的借用检查规则必须调整。因此,如果联合体的一个字段是被出借,那么在相同的生存期内它的所有其他字段也都处于出借状态。
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } // 错误: 不能同时对 `u` (通过 `u.f2`)拥有多余一次的可变借用 fn test() { let mut u = MyUnion { f1: 1 }; unsafe { let b1 = &mut u.f1; // ---- 首次可变借用发生在这里 (通过 `u.f1`) let b2 = &mut u.f2; // ^^^^ 二次可变借用发生在这里 (通过 `u.f2`) *b1 = 5; } // - 首次借用在这里结束 assert_eq!(unsafe { u.f1 }, 5); } }
如您所见,在许多方面(除了布局、安全性和所有权),联合体的行为与结构体完全相同,这很大程度上是因为联合体继承使用了结构体的句法的结果。对于 Rust 语言未明确提及的许多方面(比如隐私性(privacy)、名称解析、类型推断、泛型、trait实现、固有实现、一致性、模式检查等等)也是如此。