Avatar ·

Можно ли использовать глобальные переменные в Rust?

Всем привет! Я только начал изучать Rust, перешел с C++, и у меня возникла проблема. В C++ я привык использовать глобальные переменные, чтобы хранить конфигурацию приложения или счётчики, к которым нужен доступ из разных функций. Например, у меня есть простой счётчик вызовов функции: ```rust static COUNTER: i32 = 0; fn increment() { COUNTER += 1; // Ошибка! } ``` Компилятор выдаёт ошибку: "cannot mutate static variable". Я попытался обернуть её в `mut`: ```rust static mut COUNTER: i32 = 0; fn increment() { COUNTER += 1; // unsafe! Очень неудобно. } ``` Теперь компилятор требует обернуть доступ в `unsafe { ... }`. Я понимаю, что это небезопасно, но как тогда правильно организовать глобальное состояние? Мне нужно, чтобы переменная была видна во всей программе, но при этом код оставался безопасным. Я слышал про `lazy_static` и `OnceCell`, но не понимаю, какой подход выбрать. Подскажите, пожалуйста, как в Rust принято работать с глобальными переменными? Может быть, есть альтернативы, которые не нарушают идиомы языка? Заранее спасибо! <<>>

Проблема глобальных переменных в Rust

Rust жёстко контролирует безопасность памяти. Глобальные изменяемые переменные (как в C++) — прямой путь к гонкам данных (data races). Поэтому компилятор запрещает прямую мутацию. Вместо этого Rust предлагает безопасные обёртки.

Решение 1: Mutex для потокобезопасности

Самый простой способ — использовать Mutex (или RwLock). Он гарантирует, что только один поток может изменять значение в любой момент времени.

use std::sync::Mutex;

static COUNTER: Mutex<i32> = Mutex::new(0);

fn increment() {
    let mut num = COUNTER.lock().unwrap();
    *num += 1;
}

fn get_value() -> i32 {
    *COUNTER.lock().unwrap()
}

fn main() {
    increment();
    increment();
    println!("Счётчик: {}", get_value()); // 2
}

Как это работает:

  • Mutex::new(0) создаёт глобальный мьютекс с начальным значением.
  • lock() захватывает блокировку. Если другой поток уже держит блокировку, текущий поток ждёт.
  • unwrap() — если мьютекс отравлен (poisoned) из-за паники в другом потоке, программа упадёт. Для продакшена стоит обрабатывать ошибку.
  • После выхода из области видимости блокировка автоматически освобождается.

Решение 2: lazy_static или once_cell для ленивой инициализации

Если глобальная переменная требует сложной инициализации (например, чтение файла конфигурации), используйте крейт lazy_static (или once_cell, который входит в стандартную библиотеку с Rust 1.70).

// Cargo.toml: lazy_static = "1.4"
use lazy_static::lazy_static;
use std::sync::Mutex;

lazy_static! {
    static ref CONFIG: Mutex<Vec<String>> = Mutex::new(vec![
        "option1".to_string(),
        "option2".to_string(),
    ]);
}

fn main() {
    let mut config = CONFIG.lock().unwrap();
    config.push("option3".to_string());
    println!("{:?}", *config);
}

Альтернатива без внешних крейтов (Rust 1.70+):

use std::sync::LazyLock;
use std::sync::Mutex;

static CONFIG: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| {
    Mutex::new(vec!["default".to_string()])
});

Решение 3: Atomic для простых чисел

Если вам нужно хранить целочисленный счётчик или флаг, используйте атомарные типы (AtomicI32, AtomicBool и т.д.). Они работают без блокировок и очень эффективны.

use std::sync::atomic::{AtomicI32, Ordering};

static COUNTER: AtomicI32 = AtomicI32::new(0);

fn increment() {
    COUNTER.fetch_add(1, Ordering::SeqCst);
}

fn get_value() -> i32 {
    COUNTER.load(Ordering::SeqCst)
}

fn main() {
    increment();
    increment();
    println!("Счётчик: {}", get_value()); // 2
}

Важно: Ordering определяет гарантии видимости. SeqCst — самый строгий (и безопасный для новичков). Для продакшена можно использовать Relaxed или Acquire/Release, но это требует понимания модели памяти.

Когда не стоит использовать глобальные переменные

В Rust принято передавать состояние через параметры функций или через структуры. Это делает код тестируемым и предсказуемым. Глобальные переменные — это исключение для редких случаев:

  • Глобальный логгер или конфигурация, которая не меняется после старта.
  • Счётчики метрик (например, число обработанных запросов).
  • Кэш с однократной инициализацией.

Если вы пишете библиотеку, избегайте глобального состояния — оно мешает пользователям контролировать поведение.

<<>>

Альтернатива: передача состояния через параметры

В Rust принято избегать глобальных переменных. Вместо этого используйте структуру, которая хранит состояние, и передавайте её как параметр. Это делает код гибким и тестируемым.

struct AppState {
    counter: i32,
}

impl AppState {
    fn new() -> Self {
        AppState { counter: 0 }
    }

    fn increment(&mut self) {
        self.counter += 1;
    }

    fn get_value(&self) -> i32 {
        self.counter
    }
}

fn main() {
    let mut state = AppState::new();
    state.increment();
    state.increment();
    println!("Счётчик: {}", state.get_value()); // 2
}

Плюсы:

  • Нет проблем с потоками — каждый поток может иметь свою копию.
  • Легко тестировать — можно создать несколько экземпляров.
  • Явная передача зависимостей (dependency injection).
  • Не нужно использовать unsafe или блокировки.

Когда это не подходит: если состояние действительно должно быть глобальным (например, реестр обработчиков в фреймворке). В таких случаях используйте Mutex или Atomic из первого ответа.

Совет: начните с локального состояния. Если почувствуете, что передача через параметры становится слишком громоздкой (например, в 10+ вложенных функциях), задумайтесь о внедрении зависимостей через структуры или трейты, а не о глобальных переменных.