联合体
联合体的声明使用与结构体声明相同的语法,只是将 struct
替换为 union
。
联合体声明在它所在的模块或块的类型命名空间中定义给定的名称。
#![allow(unused)] fn main() { #[repr(C)] union MyUnion { f1: u32, f2: f32, } }
联合体的关键特性在于其所有字段共享同一块存储空间。因此,对联合体中一个字段的写入可能会覆盖其其他字段,且联合体的大小由其最大字段的大小决定。
联合体字段类型被限制为以下类型子集
Copy
类型
- 引用 (对于任意
T
的&T
和&mut T
)
ManuallyDrop<T>
(对于任意的T
)
- 仅包含允许的联合体字段类型的元组和数组
这项限制特别确保联合体字段永远不需要被丢弃(drop)。与结构体和枚举类似,可以为联合体 impl Drop
以手动定义当它被丢弃时会发生什么。
不包含任何字段的联合体不被编译器接受,但可以被宏接受。
联合体的初始化
可以使用与结构体类型相同的语法创建联合体类型的值,但必须精确指定一个字段
#![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 }; }
读取和写入联合体字段
联合体没有“活动字段”的概念。相反,每一次联合体访问都只是将存储空间解释为用于访问的字段类型。
读取联合体字段是按照该字段的类型来读取联合体的比特位。
字段可能具有非零偏移量(除非使用了C表示);在这种情况下,将读取从字段偏移量开始的比特位
确保数据对于字段类型是有效的,这是程序员的责任。否则将导致未定义行为。例如,从一个布尔类型的字段中读取值 3
是未定义行为。实际上,对使用了C表示的联合体进行写入后再读取,类似于从用于写入的类型到用于读取的类型的transmute
。
因此,所有读取联合体字段的操作都必须放在 unsafe
块中
#![allow(unused)] fn main() { union MyUnion { f1: u32, f2: f32 } let u = MyUnion { f1: 1 }; unsafe { let f = u.f1; } }
通常,使用联合体的代码会提供安全的封装来包围不安全的联合体字段访问。
相比之下,对联合体字段的写入是安全的,因为它们只是覆盖任意数据,不会导致未定义行为。(注意:联合体字段类型绝不能拥有 drop glue,因此写入联合体字段永远不会隐式地丢弃任何内容。)
联合体的模式匹配
另一种访问联合体字段的方式是使用模式匹配。
对联合体字段进行模式匹配使用与结构体模式相同的语法,但该模式必须精确指定一个字段。
由于模式匹配类似于按照特定字段读取联合体,它也必须放在 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); } } } } }
模式匹配可以将联合体作为更大结构体的一部分进行匹配。特别是,当通过 FFI 使用 Rust 联合体实现 C 标记联合体时,这允许同时对标记和相应的字段进行匹配
#![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 } // ERROR: cannot borrow `u` (via `u.f2`) as mutable more than once at a time fn test() { let mut u = MyUnion { f1: 1 }; unsafe { let b1 = &mut u.f1; // ---- first mutable borrow occurs here (via `u.f1`) let b2 = &mut u.f2; // ^^^^ second mutable borrow occurs here (via `u.f2`) *b1 = 5; } // - first borrow ends here assert_eq!(unsafe { u.f1 }, 5); } }
如您所见,在许多方面(布局、安全性和所有权除外),联合体行为与结构体完全一致,这主要是因为它们从结构体继承了语法形式。对于 Rust 语言中许多未提及的方面(例如隐私、名称解析、类型推断、泛型、特征实现、固有实现、一致性、模式检查等等等等),这也是成立的。