Копировать одну переменную в другую и ожидать, что значение будет доступно в первой переменной — это обычное дело для многих языков программирования. Но это не совсем верно для строк на 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, поэтому необходимо некоторое погружение в тему. Продолжаю изучение, но, похоже, это сделано для обеспечения большей безопасности при работе с памятью и повышения производительности.

Читайте также:


Перевод статьи Gaurav Singh: How ‘String’ works in Rust?

Предыдущая статьяКак сделать приложение-чат с Redis, WebSocket и Go
Следующая статьяПочему Python не станет языком программирования будущего