Avatar ·

Что означают символы амперсанда (&) и звездочки (*) в Rust?

Всем привет! Я только начал изучать Rust и никак не могу разобраться с двумя операторами: & (амперсанд) и * (звёздочка). В других языках, например в C++, я привык, что & — это взятие адреса, а * — разыменование указателя. Но в Rust всё как-то иначе.

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

fn main() {
    let x = 42;
    let y = &x;
    println!("{}", y); // Работает, но что тут происходит?
    
    let mut z = 10;
    change_value(&z);
    println!("{}", z);
}

fn change_value(val: &i32) {
    *val = 100; // Ошибка! Почему?
}

Компилятор ругается на строку *val = 100, говорит что-то про то, что val — это неизменяемая ссылка. Но ведь я использую звёздочку, чтобы разыменовать! Почему это не работает?

Также я не понимаю, зачем иногда нужно писать & перед переменной при передаче в функцию, а иногда — нет. И когда нужно использовать * для доступа к значению?

Пожалуйста, объясните простыми словами, что такое ссылки и разыменование в Rust, и в чём отличие от указателей в C/C++.

<<>>

Что такое ссылки (&) и разыменование (*) в Rust

В Rust & и * — это не про указатели в стиле C, а про ссылки и разыменование в контексте системы владения (ownership). Давайте разберёмся по порядку.

1. Амперсанд (&) — оператор взятия ссылки

Когда вы пишете &x, вы не копируете значение x, а создаёте ссылку на него. Ссылка — это как «закладка» в книге: она говорит компилятору «посмотри на эту ячейку памяти, но не забирай её себе». В Rust это называется заимствование (borrowing).

let x = 42;
let y = &x;  // y — это ссылка на x

Теперь y имеет тип &i32 (ссылка на 32-битное целое). Она не владеет значением 42, а только указывает на него.

2. Звёздочка (*) — оператор разыменования

Чтобы получить доступ к значению, на которое указывает ссылка, используется *. Это называется разыменованием:

let x = 42;
let y = &x;
println!("{}", *y);  // Выведет 42

Без звёздочки println!("{}", y) тоже сработает, но только потому, что макрос println! автоматически разыменовывает ссылки для форматирования. В обычных операциях (например, арифметика) нужно писать *y.

3. Почему ваш код не работает?

Вы написали:

fn change_value(val: &i32) {
    *val = 100; // Ошибка!
}

Проблема в том, что val — это неизменяемая (immutable) ссылка (&i32). Через неё нельзя изменить данные. Чтобы изменять значение, нужно использовать изменяемую ссылку (&mut i32):

fn change_value(val: &mut i32) {
    *val = 100; // Теперь работает!
}

fn main() {
    let mut z = 10;
    change_value(&mut z);  // Передаём изменяемую ссылку
    println!("{}", z);     // Выведет 100
}

Обратите внимание: &mut z — это взятие изменяемой ссылки. Это одно из ключевых правил Rust: у вас может быть либо одна изменяемая ссылка, либо множество неизменяемых, но не одновременно.

4. Отличие от C/C++

  • Безопасность: В Rust ссылки всегда валидны. Нет нулевых ссылок (null), нет висячих указателей (dangling pointers). Компилятор проверят это на этапе компиляции.
  • Автоматическое разыменование: В некоторых контекстах (методы, сравнения) Rust автоматически применяет *. В C/C++ этого нет.
  • Владение: В C & даёт адрес, который можно хранить и использовать, пока жив оригинал. В Rust ссылка «живёт» только пока жив владелец, и компилятор это проверяет.

5. Примеры для закрепления

// Неизменяемая ссылка — только чтение
let s1 = String::from("hello");
let len = calculate_length(&s1);  // Заимствуем s1

fn calculate_length(s: &String) -> usize {
    s.len()  // s — ссылка, но .len() работает благодаря автоматическому разыменованию
}  // s выходит из области видимости, но ничего не происходит — мы не владели значением

// Изменяемая ссылка — запись
let mut s2 = String::from("hello");
change(&mut s2);

fn change(some_string: &mut String) {
    some_string.push_str(", world");  // Изменяем через ссылку
}

Главное запомните: & — это «одолжить значение на время», а * — «дай мне то, на что ты ссылаешься». И никогда не пытайтесь изменить данные через &T — компилятор этого не позволит.

<<>>

Дополнение: Автоматическое разыменование и оператор &mut

Первый ответ отлично объясняет базу. Я хочу добавить нюанс, который часто путает новичков — автоматическое разыменование (deref coercion).

В Rust компилятор часто сам подставляет * за вас, когда это очевидно. Например, при вызове методов:

let x = Box::new(42);  // Box — это умный указатель, похожий на ссылку
println!("{}", x);      // Работает! println сам разыменовал Box
println!("{}", *x);     // То же самое, но явно

// Вызов методов:
let s = String::from("hello");
let r = &s;            // r: &String
println!("{}", r.len()); // Работает! Rust автоматически разыменовал &String до String

Это поведение реализовано через трейт Deref. Компилятор видит, что у вас &String, а метод .len() есть у String, и автоматически применяет * столько раз, сколько нужно.

Когда нужно писать * явно?

  • В арифметике: let sum = *a + *b; — если a и b — ссылки.
  • При присваивании: *ptr = new_value; — чтобы изменить значение через изменяемую ссылку.
  • При сравнении: if *a == *b { ... } — если хотите сравнить значения, а не адреса.

Также помните: &mut даёт вам эксклюзивный доступ на запись. Если вы передали &mut x в функцию, то пока она не завершится, вы не сможете использовать x напрямую — это защита от гонок данных (data races).

Совет: всегда начинайте с неизменяемых ссылок (&T), и только если компилятор скажет, что нужно &mut, меняйте. Так вы привыкнете к идиомам Rust.