Union в Rust: объяснение с примерами
Что такое Union?
Union (объединение) в Rust - это специальный тип данных, который позволяет хранить разные типы данных в одной области памяти. В отличие от enum, union не имеет тега для отслеживания активного варианта, что делает его небезопасным для использования.
Основные характеристики:
- Размер памяти равен размеру самого большого поля
- Одновременно активно только одно поле
- Небезопасный доступ - компилятор не проверяет, какое поле активно
- Требует unsafe для чтения и записи
Синтаксис объявления
#![allow(unused)] fn main() { union MyUnion { field1: u32, field2: f32, field3: [u8; 4], } }
Пример 1: Базовое использование
union IntOrFloat { integer: u32, float: f32, } fn main() { // Создание union let mut value = IntOrFloat { integer: 42 }; // Небезопасное чтение - мы должны знать, какое поле активно! unsafe { println!("Integer value: {}", value.integer); } // Запись другого поля value.float = 3.14; // Чтение как float (теперь это активное поле) unsafe { println!("Float value: {}", value.float); // ОПАСНО: чтение неактивного поля! // println!("Integer value: {}", value.integer); // Неопределенное поведение! } }
Пример 2: Преобразование байтов
union BytesConverter { value: u32, bytes: [u8; 4], } fn main() { let converter = BytesConverter { value: 0x12345678 }; unsafe { println!("Value: 0x{:x}", converter.value); println!("Bytes: {:02x} {:02x} {:02x} {:02x}", converter.bytes[0], converter.bytes[1], converter.bytes[2], converter.bytes[3]); } // Порядок байтов зависит от архитектуры! // На little-endian: [0x78, 0x56, 0x34, 0x12] // На big-endian: [0x12, 0x34, 0x56, 0x78] }
Пример 3: Работа с разными типами данных
union MultiType { number: i32, character: char, boolean: bool, pointer: *const i32, } fn main() { let data = [10, 20, 30]; let mut multi = MultiType { number: 100 }; unsafe { println!("Number: {}", multi.number); } // Меняем активное поле multi.character = 'A'; unsafe { println!("Character: {}", multi.character); } multi.boolean = true; unsafe { println!("Boolean: {}", multi.boolean); } multi.pointer = data.as_ptr(); unsafe { println!("Pointer: {:?}", multi.pointer); println!("Dereferenced: {}", *multi.pointer); } }
Пример 4: Безопасная обертка вокруг union
use std::mem; #[derive(Debug)] enum ActiveField { Integer, Float, Bytes, } struct SafeUnion { data: IntOrFloatOrBytes, active: ActiveField, } union IntOrFloatOrBytes { integer: i32, float: f32, bytes: [u8; 4], } impl SafeUnion { fn new_integer(value: i32) -> Self { SafeUnion { data: IntOrFloatOrBytes { integer: value }, active: ActiveField::Integer, } } fn new_float(value: f32) -> Self { SafeUnion { data: IntOrFloatOrBytes { float: value }, active: ActiveField::Float, } } fn get_integer(&self) -> Option<i32> { if let ActiveField::Integer = self.active { unsafe { Some(self.data.integer) } } else { None } } fn get_float(&self) -> Option<f32> { if let ActiveField::Float = self.active { unsafe { Some(self.data.float) } } else { None } } fn set_integer(&mut self, value: i32) { self.data.integer = value; self.active = ActiveField::Integer; } fn set_float(&mut self, value: f32) { self.data.float = value; self.active = ActiveField::Float; } } fn main() { let mut safe_union = SafeUnion::new_integer(42); println!("As integer: {:?}", safe_union.get_integer()); println!("As float: {:?}", safe_union.get_float()); safe_union.set_float(3.14); println!("As integer: {:?}", safe_union.get_integer()); println!("As float: {:?}", safe_union.get_float()); }
Пример 5: Совместимость с C
// Для взаимодействия с C кодом #[repr(C)] union CCompatible { long_value: i64, double_value: f64, struct_value: [u32; 2], } // Использование в FFI extern "C" { fn some_c_function(param: *const CCompatible); } fn main() { let data = CCompatible { long_value: 12345 }; unsafe { some_c_function(&data); } }
Пример 6: ManuallyDrop для автоматического управления памятью
#![allow(unused)] fn main() { use std::mem::ManuallyDrop; union StringOrInt { string: ManuallyDrop<String>, number: i32, } impl StringOrInt { fn new_string(s: String) -> Self { StringOrInt { string: ManuallyDrop::new(s), } } fn new_number(n: i32) -> Self { StringOrInt { number: n } } // Опасный метод - нужно вручную управлять временем жизни unsafe fn take_string(&mut self) -> String { if let ActiveField::String = self.active { ManuallyDrop::take(&mut self.string) } else { panic!("Active field is not string!"); } } } impl Drop for StringOrInt { fn drop(&mut self) { // Нужно вручную вызывать деструктор для String unsafe { if let ActiveField::String = self.active { ManuallyDrop::drop(&mut self.string); } } } } }
Ключевые моменты для запоминания:
- ⚠️ Все операции с union требуют unsafe - компилятор не может гарантировать безопасность
- Вы сами отвечаете за отслеживание активного поля
- Чтение неактивного поля - неопределенное поведение
- Размер union = размер самого большого поля + выравнивание
- Инициализировать нужно только одно поле при создании
Когда использовать union:
- Взаимодействие с C кодом
- Экономия памяти в критических местах
- Низкоуровневые операции с памятью
- Альтернативные представления данных
Альтернативы:
- Enum - безопасный вариант, когда нужны разные типы
- Struct - когда все поля должны быть доступны одновременно
- Transmute - для простых преобразований типов
Warning
Union в Rust - это мощный, но опасный инструмент. Используйте его только когда действительно необходимо, и всегда обеспечивайте безопасную обертку вокруг него!