Так ли просто написать «Hello, World!» на языке Rust — про ссылки и разыменование

Сегодня столкнулся с интересной проблемой, которая показалась мне забавной. Простенькая задачка, что будет если добавить амперсанд к строке «Hello, World!». Собственно, давайте попробуем разобраться, что происходит когда мы делаем ссылку на «&str» тип.

Посмотрите на код ниже, далее мы подробно разберем каждую строку.

 

fn main() {
println!("{}", "Hello, World!");
println!("{}", &"Hello, World!");
println!("{}", &*"привет!");
println!("{}", &&*"Hello, World!");
println!("{}", &&&&"Hello, World!");
// error[E0614]: type `str` cannot be dereferenced
// Но почему ошибка? Ответ кроется в том как создаются ссылки.
println!("{}", &&&&**"Hello, World!");
println!("{}", &&&&****"Hello, World!");
}

Разберём пошагово, что происходит в каждом вызове println! и почему возникает ошибка E0614.

Основы: типы и ссылки в Rust

  • "привет!" — строковый литерал типа &'static str (неизменяемая ссылка на строку в статической памяти).
  • & — оператор взятия ссылки (borrowing).
  • * — оператор разыменования (dereferencing).

Разбор каждого вызова

  1. println!("{}", "привет!");
    Работает. "привет!" уже имеет тип &str, который можно вывести.
  2. println!("{}", &"привет!");
    Работает. Берём ссылку на &str → получаем &&strprintln! умеет работать с ссылками на строки.
  3. println!("{}", &*"привет!");
    Работает.

    • *"привет!" — разыменование &str → получаем значение типа str (но это не ссылка!).
    • &*... — берём ссылку на str → снова &str.
      Итог: эквивалентно первому варианту.
  4. println!("{}", &&*"Hello, World!");
    Работает.

    • *"Hello, World!" → str.
    • &*... → &str.
    • &&*... → &&str.
      println! справляется с &&str.
  5. println!("{}", &&&&"Hello, World!");
    Работает. Просто создаём многоуровневую ссылку: &&&&strprintln! может вывести и такое.
  6. println!("{}", &&&&**"Hello, World!");
    Ошибка E0614.
    Почему:

    • **"Hello, World!" — попытка дважды разыменовать &str.
    • Первое * → str (значение, не ссылка).
    • Второе * → попытка разыменовать str, но str не является указателем и не поддерживает разыменование.
  7. println!("{}", &&&&****"Hello, World!");
    Тоже ошибка E0614.
    Разбор:

    • ****"Hello, World!" — четыре операции разыменования.
    • После первого * → str.
    • Остальные три * пытаются разыменовать str, что невозможно.

Почему str нельзя разыменовать?

  • str — это не указатель, а динамически размещённый тип (DST, dynamically sized type).
  • Он представляет собой «сырые» байты строки без информации о длине.
  • В Rust DST всегда используются через ссылки (&strBox<str> и т. д.), потому что ссылка содержит длину.
  • Попытка разыменовать str как указатель приводит к ошибке, так как у него нет адреса для разыменования.

Если свести все к одному предложению.

Ошибка возникает, когда мы пытаемся применить оператор * к значению типа str (а не к ссылке на него). Строковые литералы ("...") уже являются ссылками (&str), и их можно разыменовать только один раз — чтобы получить str.

Дальнейшее разыменование невозможно.

Немного усложним задачу

Что будет, если сделать так:

fn main() { 
println!("{}", &*&*&*&*"Hello, World!");
}

Код все еще прекрасно работает и выводит в консоль приветствие всему миру.

Из этого можно сделать неверный вывод, что нельзя ставить несколько знаков разыменование подряд. Давайте сразу развенчаем эту теорию.

fn main() {    
    let hello = &&&&&&"Hello, World!";
    println!("{}", *****hello);
}

Код снова компилируется. Так в чем же дело?

Почему работает первый пример

Здесь происходит чередование операций:

  1. "Hello, World!" — тип &str.
  2. * — разыменование &str → получаем str (значение, не ссылку).
  3. & — берём ссылку на str → снова &str.
  4. Повторяем цикл: * → & → * → & → ....

Ключевой момент:
Каждый оператор * применяется только к ссылке (&str), а не к самому str. После разыменования сразу берётся новая ссылка, поэтому следующий * снова работает с &str.

Почему работает второй пример

Разберём типы пошагово:

  1. "Hello, World!" → &str.
  2. &&&&&&"Hello, World!" → &&&&&&str (шестиуровневая ссылка).
  3. hello имеет тип &&&&&&str.

Теперь применяем разыменования:

  • *hello → &&&&&str (снимаем один уровень ссылки).
  • ***hello → &&&&str (снимаем ещё один).
  • ***hello → &&&str (ещё один).
  • ****hello → &&str (ещё).
  • *****hello → &str (последний уровень).

Итог: *****hello даёт &str, который можно вывести через println!.

В чём разница с ошибочным случаем?

Ошибка E0614 возникает только когда пытаются разыменовать str напрямую (не ссылку на него). Уже догадались почему так происходит?

Приоритет и порядок операций

Разыменование (*) имеет более высокий приоритет, чем взятие ссылки (&) в языке программирования Rust (как и во многих других языках, включая Си/C++).
Разберем следующий пример, чтобы поставить точку в наших изысканиях.
fn main() {    
    println!("{}", &&&&***&&"Hello, World!");
}

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

Хотя мы читаем код слева направо, выполнение операций и их семантическая интерпретация зависят от приоритета операторов, который в данном случае заставляет компилятор связывать операторы * и & справа налево.

 

Приоритет vs Порядок чтения

Унарные операторы (такие как * и &) имеют ассоциативность справа налево и одинаковый, высокий приоритет. Это означает, что компилятор группирует их, начиная справа.
Вот как выражение разбирается по шагам:
  1. Исходное выражение:
    &&&&***&&"Hello, World!"
  2. Шаг 1: Самое правое подвыражение
    "Hello, World!" имеет тип &'static str.
  3. Шаг 2: Группировка справа налево
    Компилятор «читает» и связывает операторы справа налево из-за их ассоциативности:
    • &&"Hello, World!" — берем ссылку на ссылку. Тип: &&'static str.
    • ***( ... ) — затем применяются три разыменования.

    Вот как компилятор это связывает:
    &&&&( ***( &&"Hello, World!" ) )

 

Компилятор определяет, что операторы разыменования * должны быть применены к правой части выражения раньше, чем операторы взятия ссылки & из левой части результата.
Говоря проще, приоритет операций здесь делит выражение на две части, ту что была до разыменование и после. Затем к правой (последующей) части применяется разыменование и если справа мы получаем данные по ссылке, то код компилируется. Путаницу может вызвать оператор * — который так же применяется как знак умножения. Но читаются они совершенно по разному.
&&&&( ***( &&"Hello, World!" ) )
││││ └─┬─┘└────────┬────────┘
3456   2           1
││││   │           └───► Проверка типа &str и добавление двух ссылок.
││││   └───► Тройное разыменование ***:
││││        1. *(&&&str) → &&str
││││        2. *(&&str)  → &str 
││││        3. *(&str)   → str (DST!)
││││        4. Далее разыменование невозможно ERROR E0614
│││└─► Внешняя ссылка & (третий уровень)
││└──► Внешняя ссылка & (второй уровень)
│└───► Внешняя ссылка & (первый уровень)
└────► Внешняя ссылка & (нулевой уровень)

"Hello, World!"
└──► Исходный литерал: &'static str
_____________________________
Результат выражения: &&&&str.

Резюмируя

На простом примере мы узнали немного о том, как работают ссылки в языке Rust, а так же выяснили, что всегда нужно учитывать приоритет операторов.
StackUP