Сегодня столкнулся с интересной проблемой, которая показалась мне забавной. Простенькая задачка, что будет если добавить амперсанд к строке «Hello, World!». Собственно, давайте попробуем разобраться, что происходит когда мы делаем ссылку на «&str» тип.
Посмотрите на код ниже, далее мы подробно разберем каждую строку.
|
1 2 3 4 5 6 7 8 9 10 11 |
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).
Разбор каждого вызова
println!("{}", "привет!");
Работает."привет!"уже имеет тип&str, который можно вывести.println!("{}", &"привет!");
Работает. Берём ссылку на&str→ получаем&&str.println!умеет работать с ссылками на строки.println!("{}", &*"привет!");
Работает.*"привет!"— разыменование&str→ получаем значение типаstr(но это не ссылка!).&*...— берём ссылку наstr→ снова&str.
Итог: эквивалентно первому варианту.
println!("{}", &&*"Hello, World!");
Работает.*"Hello, World!"→str.&*...→&str.&&*...→&&str.
println!справляется с&&str.
println!("{}", &&&&"Hello, World!");
Работает. Просто создаём многоуровневую ссылку:&&&&str.println!может вывести и такое.println!("{}", &&&&**"Hello, World!");
ОшибкаE0614.
Почему:**"Hello, World!"— попытка дважды разыменовать&str.- Первое
*→str(значение, не ссылка). - Второе
*→ попытка разыменоватьstr, ноstrне является указателем и не поддерживает разыменование.
println!("{}", &&&&****"Hello, World!");
Тоже ошибкаE0614.
Разбор:****"Hello, World!"— четыре операции разыменования.- После первого
*→str. - Остальные три
*пытаются разыменоватьstr, что невозможно.
Почему str нельзя разыменовать?
str— это не указатель, а динамически размещённый тип (DST, dynamically sized type).- Он представляет собой «сырые» байты строки без информации о длине.
- В Rust DST всегда используются через ссылки (
&str,Box<str>и т. д.), потому что ссылка содержит длину. - Попытка разыменовать
strкак указатель приводит к ошибке, так как у него нет адреса для разыменования.
Если свести все к одному предложению.
Ошибка возникает, когда мы пытаемся применить оператор * к значению типа str (а не к ссылке на него). Строковые литералы ("...") уже являются ссылками (&str), и их можно разыменовать только один раз — чтобы получить str.
Дальнейшее разыменование невозможно.
Немного усложним задачу
Что будет, если сделать так:
|
1 2 3 |
fn main() { println!("{}", &*&*&*&*"Hello, World!"); } |
Код все еще прекрасно работает и выводит в консоль приветствие всему миру.
Из этого можно сделать неверный вывод, что нельзя ставить несколько знаков разыменование подряд. Давайте сразу развенчаем эту теорию.
|
1 2 3 4 |
fn main() { let hello = &&&&&&"Hello, World!"; println!("{}", *****hello); } |
Код снова компилируется. Так в чем же дело?
Почему работает первый пример
Здесь происходит чередование операций:
"Hello, World!"— тип&str.*— разыменование&str→ получаемstr(значение, не ссылку).&— берём ссылку наstr→ снова&str.- Повторяем цикл:
* → & → * → & → ....
Ключевой момент:
Каждый оператор * применяется только к ссылке (&str), а не к самому str. После разыменования сразу берётся новая ссылка, поэтому следующий * снова работает с &str.
Почему работает второй пример
Разберём типы пошагово:
"Hello, World!"→&str.&&&&&&"Hello, World!"→&&&&&&str(шестиуровневая ссылка).helloимеет тип&&&&&&str.
Теперь применяем разыменования:
*hello→&&&&&str(снимаем один уровень ссылки).- **
*hello→&&&&str(снимаем ещё один). ***hello→&&&str(ещё один).****hello→&&str(ещё).*****hello→&str(последний уровень).
Итог: *****hello даёт &str, который можно вывести через println!.
В чём разница с ошибочным случаем?
Ошибка E0614 возникает только когда пытаются разыменовать str напрямую (не ссылку на него). Уже догадались почему так происходит?
Приоритет и порядок операций
*) имеет более высокий приоритет, чем взятие ссылки (&) в языке программирования Rust (как и во многих других языках, включая Си/C++).|
1 2 3 |
fn main() { println!("{}", &&&&***&&"Hello, World!"); } |
Если добавить четвертый символ разыменование программа не скомпилируется, но сейчас она запускается и выводит приветствие.
Хотя мы читаем код слева направо, выполнение операций и их семантическая интерпретация зависят от приоритета операторов, который в данном случае заставляет компилятор связывать операторы * и & справа налево.
Приоритет vs Порядок чтения
* и &) имеют ассоциативность справа налево и одинаковый, высокий приоритет. Это означает, что компилятор группирует их, начиная справа.- Исходное выражение:
&&&&***&&"Hello, World!" - Шаг 1: Самое правое подвыражение
"Hello, World!"имеет тип&'static str. - Шаг 2: Группировка справа налево
Компилятор «читает» и связывает операторы справа налево из-за их ассоциативности:&&"Hello, World!"— берем ссылку на ссылку. Тип:&&'static str.***( ... )— затем применяются три разыменования.
Вот как компилятор это связывает:
&&&&( ***( &&"Hello, World!" ) )
Компилятор определяет, что операторы разыменования*должны быть применены к правой части выражения раньше, чем операторы взятия ссылки&из левой части результата.
* — который так же применяется как знак умножения. Но читаются они совершенно по разному.|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
&&&&( ***( &&"Hello, World!" ) ) ││││ └─┬─┘└────────┬────────┘ 3456 2 1 ││││ │ └───► Проверка типа &str и добавление двух ссылок. ││││ └───► Тройное разыменование ***: ││││ 1. *(&&&str) → &&str ││││ 2. *(&&str) → &str ││││ 3. *(&str) → str (DST!) ││││ 4. Далее разыменование невозможно ERROR E0614 │││└─► Внешняя ссылка & (третий уровень) ││└──► Внешняя ссылка & (второй уровень) │└───► Внешняя ссылка & (первый уровень) └────► Внешняя ссылка & (нулевой уровень) "Hello, World!" └──► Исходный литерал: &'static str _____________________________ Результат выражения: &&&&str. |



