Переменные и данные, взаимодействующие с Move
В Rust несколько переменных могут взаимодействовать с теми же данными по-разному. Рассмотрим пример с целым числом в листинге 4-2.
let x = 5;
let y = x;
Листинг 4-2: Присваивание целочисленного значения переменной x
переменной y
Вероятно, мы можем догадаться, что это делает: "привязываем значение 5
к x
; затем копируем значение из x
и привязываем его к y
". Теперь у нас есть две переменные, x
и y
, и обе равны 5
. Действительно, так происходит, потому что целые числа — это простые значения с известным, фиксированным размером, и эти два значения 5
помещаются на стек.
Теперь посмотрим на версию с String
:
let s1 = String::from("hello");
let s2 = s1;
Это выглядит очень похоже, поэтому мы можем предположить, что принцип работы будет одинаковым: то есть, на второй строке будет создана копия значения из s1
и привязана к s2
. Но это не совсем так происходит.
Посмотрите на рис. 4-1, чтобы понять, что происходит с String
"под капотом". String
состоит из трех частей, показанных слева: указатель на память, которая хранит содержимое строки, длина и емкость. Эта группа данных хранится на стеке. Справа находится память в куче, которая хранит содержимое.
Рисунок 4-1: Представление в памяти String
, содержащего значение "hello"
, привязанного к s1
Длина — это количество байт памяти, которое в настоящее время использует содержимое String
. Емкость — это общий объем памяти в байтах, который String
получил от аллокатора. Разница между длиной и емкостью имеет значение, но не в этом контексте, поэтому на данный момент можно игнорировать емкость.
Когда мы присваиваем s1
s2
, данные String
копируются, что означает, что мы копируем указатель, длину и емкость, которые находятся на стеке. Мы не копируем данные в куче, на которые указывает указатель. Другими словами, представление данных в памяти выглядит как на рис. 4-2.
Рисунок 4-2: Представление в памяти переменной s2
, которая имеет копию указателя, длины и емкости s1
Представление не выглядит как на рис. 4-3, который показывает, как бы память выглядела, если бы Rust также скопировал данные в куче. Если бы Rust сделал это, операция s2 = s1
могла бы быть очень дорогой по времени выполнения, если бы данные в куче были большими.
Рисунок 4-3: Еще один вариант того, что может произойти при s2 = s1
, если Rust также скопирует данные в куче
Ранее мы говорили, что когда переменная выходит за пределы области видимости, Rust автоматически вызывает функцию drop
и очищает память в куче для этой переменной. Но на рис. 4-2 показано, что оба указателя на данные указывают на одну и ту же область. Это проблема: когда s2
и s1
выйдут за пределы области видимости, они оба попытаются освободить одну и ту же память. Это называется ошибкой double free и является одной из ошибок безопасности памяти, о которых мы говорили ранее. Освобождение памяти дважды может привести к повреждению памяти, что, в свою очередь, может привести к уязвимостям безопасности.
Для обеспечения безопасности памяти после строки let s2 = s1;
Rust считает, что s1
больше не действителен. Поэтому Rust не нужно освобождать ничего, когда s1
выходит за пределы области видимости. Посмотрите, что происходит, если вы пытаетесь использовать s1
после создания s2
; это не сработает:
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
Вы получите ошибку такого вида, потому что Rust предотвращает использование недействительного ссылки:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which
does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1}, world!");
| ^^ value borrowed here after move
Если вы слышали термины shallow copy и deep copy при работе с другими языками, концепция копирования указателя, длины и емкости без копирования данных, вероятно, звучит как создание shallow copy. Но поскольку Rust также делает первую переменную недействительной, вместо того чтобы называть это shallow copy, это известно как move. В этом примере мы можем сказать, что s1
был передан в s2
. Таким образом, на самом деле происходит то, что показано на рис. 4-4.
Рисунок 4-4: Представление в памяти после того, как s1
стала недействительной
Это решает нашу проблему! Теперь только s2
действителен, и когда она выйдет за пределы области видимости, она сама освободит память, и мы закончим.
Кроме того, здесь скрывается важный выбор в дизайне: Rust никогда не будет автоматически создавать "глубокие" копии ваших данных. Поэтому можно предположить, что любая автоматическая копия будет дешева по времени выполнения.