Копировать одну переменную в другую и ожидать, что значение будет доступно в первой переменной — это обычное дело для многих языков программирования. Но это не совсем верно для строк на Rust. Почему?
Начнём с того, что Rust имеет уникальную модель управления памятью, предотвращающую копирование строк. В Rust как языке системного программирования реализовано управление памятью в явном виде, которое позволяет лучше оптимизировать имеющиеся ресурсы памяти. Такая модель называется в Rust владением. Существуют три основных правила владения: 1) каждое значение имеет переменную, которая называется его владельцем; 2) в каждый конкретный момент времени у значения может быть только один владелец; 3) когда владелец выходит из области видимости, значение удаляется. По этим правилам в Rust осуществляется управление памятью.
Прежде чем двигаться дальше, напомним, что данные сохраняются либо в стеке, либо в куче. Где именно будут сохранены данные, зависит от того, известен ли их размер во время компиляции или нет. В стеке хранятся данные с известным размером, а в куче — другие. Так примитивные типы данных, доступные на Rust, имеют определённую ёмкость, поэтому они сохраняются в стеке. Это хорошо, так как доступ к данным быстрее в стеке, чем в куче. Значит, можно безопасно присваивать и копировать переменные любых примитивных типов данных, соблюдая при этом правила владения Rust.
Однако всё меняется, когда вы используете строки.
Сначала рассмотрим эти два способа создания строк:
// Ссылка на строковый литерал
let s1 = “Hello, World!”;
// Создаёт новый строковый объект, выделяет память в куче
let s2 = String::from(“Hello, World!”);
Переменная s1
является неизменяемой, но становится изменяемой, если вы хотите изменить её значение в программе. s2
допускает изменяемость. Она аналогична строкам в других языках программирования. Далее идёт копирование, и в Rust оно происходит по-другому.
let s1 = "Hello, World!";
let s2 = s1;
println!("{:p}", s1); // Hello, World!
println!("{:p}", s2); // Hello, World!
Результат был ожидаем: обе переменные s1
и s2
выдают Hello, World!
. Давайте посмотрим, где сохраняются обе строки:
let s1 = "Hello, World!";
let s2 = s1;
println!("{:p}", s1); // 0x7ffcc5b36e78
println!("{:p}", s2); // 0x7ffcc5b36e88
При выводе видим, что s1
и s2
указывают на два разных адреса.
let s1 = String::from("Hello, World");
let s2 = s1;
println!("{}", s1); // ошибка[E0382]: заимствование перемещённого значения: `s1`
println!("{}", s2); // Hello, World!
println!("{:p}", &s1); // ошибка[E0382]: заимствование перемещённого значения: `s1`
println!("{:p}", &s2); // 0x7ffe87ebea90
А такой результат ожиданиям не соответствует. Выглядит так, что s2 = s1
выполняет копирование s1
в s2
, но вместо этого лишь обновляет s2
ссылку на указатель для указания на выделение памяти в куче, а не копирования данных в куче. Rust делает это для оптимизации производительности во время выполнения. Это напоминает поверхностное копирование в других языках программирования. Если происходит именно это, то значение всё равно должно быть доступно в s1
, но это приводит к ошибке, как показано выше. Помните второе правило владения Rust? Оно гласит, что в каждый конкретный момент времени у значения может быть только один владелец. И копирование s1
в s2
привело к переходу владения к s2
.
Так как данные s1
были в куче (вначале), мы можем сказать, что если бы обе переменные s1
и s2
указывали на один и тот же адрес в куче, то обе пытались бы очистить одну и ту же память в куче после выхода из области видимости. И это бы привело к тому, что называется в Rust ошибкой двойного освобождения, а это может быть чревато проблемами безопасности памяти.
«Есть решение, при выборе которого исходили из следующего: Rust никогда не будет автоматически создавать «глубокие» копии ваших данных, поэтому любое автоматическое копирование может считаться недорогим с точки зрения производительности во время выполнения». — Из «Языка программирования Rust» Стива Клабника и Кэрола Николса
На самом же деле Rust обновляет ссылку s2
и делает недействительной переменную s1
, поэтому только s2
может очистить память, выделенную в куче, а s1
уже не участвует в обеспечении безопасности использования памяти. Так что это на самом деле не «копирование», раз первая переменная становится недействительной, и то, что в другом случае называется «поверхностным копированием», известно в Rust как «перемещение».
Поэтому типаж Copy не реализован в строке во избежание такой ситуации, когда у значений переменных памяти в куче несколько владельцев.
А что, если хочется скопировать как обычно и оставить две разные копии? В Rust предусмотрено клонирование для нового выделения и создания копии первого в новом. В остальном оно аналогично «глубокому копированию».
Источником путаницы может быть модель владения на Rust, поэтому необходимо некоторое погружение в тему. Продолжаю изучение, но, похоже, это сделано для обеспечения большей безопасности при работе с памятью и повышения производительности.
Читайте также:
- Кто на свете всех сильнее - Java, Go и Rust в сравнении
- Реализация base64 на Rust
- Как быстро выучить новый язык программирования
Перевод статьи Gaurav Singh: How ‘String’ works in Rust?