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);
            }
        }
    }
}
}

Ключевые моменты для запоминания:

  1. ⚠️ Все операции с union требуют unsafe - компилятор не может гарантировать безопасность
  2. Вы сами отвечаете за отслеживание активного поля
  3. Чтение неактивного поля - неопределенное поведение
  4. Размер union = размер самого большого поля + выравнивание
  5. Инициализировать нужно только одно поле при создании

Когда использовать union:

  • Взаимодействие с C кодом
  • Экономия памяти в критических местах
  • Низкоуровневые операции с памятью
  • Альтернативные представления данных

Альтернативы:

  • Enum - безопасный вариант, когда нужны разные типы
  • Struct - когда все поля должны быть доступны одновременно
  • Transmute - для простых преобразований типов

Warning

Union в Rust - это мощный, но опасный инструмент. Используйте его только когда действительно необходимо, и всегда обеспечивайте безопасную обертку вокруг него!