Avatar ·

В чем разница между неизменяемыми переменными и константами в Rust?

Привет! Я начал изучать Rust и запутался в фундаментальной концепции. В документации говорится, что переменные по умолчанию неизменяемы (immutable), и для их изменения нужно использовать `mut`. Но также есть ключевое слово `const`. В чем между ними разница? Я пробовал писать такой код: ```rust const MAX_POINTS: u32 = 100_000; let x = 5; x = 6; // Ошибка! ``` И вроде бы и `MAX_POINTS`, и `x` нельзя изменить после инициализации. Но компилятор ругается на `x = 6`, а на константу — нет. Почему? И если я могу объявить неизменяемую переменную через `let`, то зачем вообще нужны константы? В каких случаях использовать `const`, а в каких — `let`? Еще я заметил, что константы нужно всегда аннотировать типом, а переменные — не всегда. Это как-то связано с их природой? Буду благодарен за подробное объяснение с примерами, чтобы раз и навсегда разобраться в этой теме. <<>>

Ключевые различия между const и неизменяемыми переменными let

На первый взгляд кажется, что const и let (без mut) делают одно и то же — запрещают изменение значения. Но на самом деле это принципиально разные концепции в Rust. Давайте разберем их по порядку.

1. Время жизни и область видимости

Константы (const) существуют на протяжении всей программы. Они встраиваются компилятором непосредственно в код в месте использования. У констант нет фиксированного адреса в памяти — компилятор может копировать их значение каждый раз, когда они используются.

Неизменяемые переменные (let) живут только в своей области видимости (блоке кода). Они имеют фиксированное место в стеке (или куче, если это Box), как и любые другие переменные.

2. Вычисление значения

Значение константы должно быть известно на этапе компиляции. Вы не можете вызвать функцию или выполнить сложные вычисления для инициализации константы — только литералы, другие константы и простые арифметические операции:

const X: u32 = 5;
const Y: u32 = X * 2; // OK
const Z: u32 = std::time::SystemTime::now() // ОШИБКА! Не константное выражение

Неизменяемая переменная может быть инициализирована результатом любого выражения, включая вызовы функций:

let x = some_function(); // OK
let y = 5 + 3; // тоже OK

3. Аннотация типа

Для констант аннотация типа обязательна. Компилятор не может вывести тип константы автоматически:

const MAX = 100; // ОШИБКА! Нужно указать тип
const MAX: u32 = 100; // OK

Для переменных let аннотация типа необязательна, если тип можно вывести из контекста:

let x = 5; // тип i32 выводится автоматически
let y: u32 = 5; // тоже OK, но избыточно

4. Shadowing (затенение)

Неизменяемые переменные можно "затенять" — объявлять новую переменную с тем же именем в той же области видимости:

let x = 5;
let x = x + 1; // OK — это новая переменная x, старая перестала существовать

Константы так делать нельзя. Если вы объявили const X: u32 = 5, то в той же области видимости нельзя объявить другую константу с именем X.

5. Использование в паттернах и типах

Константы можно использовать в:

  • Длинах массивов: [u8; MAX_SIZE]
  • Шаблонах match: match x { MY_CONST => ... }
  • Глобальных настройках

Переменные для этого не подходят.

Когда что использовать?

  • Используйте const для глобальных конфигурационных значений, математических констант (π, e), магических чисел — всего, что не меняется и известно на этапе компиляции.
  • Используйте let для любых значений, которые вычисляются во время выполнения программы, даже если они потом не изменяются.

Полный пример для наглядности:

const MAX_PLAYERS: u32 = 10; // Глобальная константа
const PI: f64 = 3.14159; // Математическая константа

fn main() {
    // Неизменяемая переменная — значение вычисляется во время выполнения
    let player_count = get_player_count();
    
    // Константа — значение известно на этапе компиляции
    println!("Максимум игроков: {}", MAX_PLAYERS);
    
    // Сравните:
    let x = PI * 2.0; // OK — PI известна
    // const Y: f64 = PI * 2.0; // Тоже OK, но Y — константа
    
    let result = if player_count > MAX_PLAYERS {
        "Слишком много игроков"
    } else {
        "Игроков достаточно"
    };
    println!("{}", result);
}

Главное правило: если значение может быть вычислено только во время выполнения — используйте let. Если оно известно до запуска программы — используйте const.

<<>>

Дополнение: производительность и семантика

Хочу добавить важный аспект, который часто упускают из виду — разницу в семантике с точки зрения компилятора и оптимизаций.

Константы — это не просто "переменные, которые нельзя изменить". Это встроенные значения. Когда вы пишете const X: u32 = 10, компилятор заменяет каждое использование X на число 10 на этапе компиляции. Это называется "инлайнинг". Поэтому:

  • Константы не занимают память в стеке или куче
  • Каждое использование константы — это отдельное значение (если вы используете X 5 раз, у вас будет 5 разных чисел 10 в коде)
  • Нельзя взять ссылку на константу: &X даст ссылку на временное значение

Неизменяемые переменные — это полноценные переменные с адресом в памяти. Если вы напишете:

let x = 10;
let y = &x;
let z = x;

То y будет ссылкой на ту же память, где хранится x. Компилятор может оптимизировать это, но семантически это разные вещи.

Практический совет: Если вы сомневаетесь, используйте let. Константы нужны только для значений, которые действительно являются "константами" в математическом или конфигурационном смысле. Для всего остального (включая значения, которые вы не собираетесь менять) используйте let без mut.