Знакомо это чувство, когда берешь новый язык и ожидаешь, что будешь нестись сломя голову, а вместо этого спотыкаешься на первом же шаге? Это было со мной, когда я взялся за Rust. У меня была вполне рабочая утилита на JavaScript — что-то вроде парсера, который изменял несколько массивов — и я решил, что перепишу её за время перерыва на кофе. Я открыл редактор, набрал код, который казался мне естественным, и запустил компилятор. Он «взорвался». И не ошибками синтаксиса. И не пропущенными точками с запятой. Он сообщил мне, что я попытался использовать значение, которое уже было перемещено. Я уставился в экран. Перемещено? Я ничего не перемещал. Я просто присвоил его другой переменной.
Зона комфорта JavaScript
JavaScript приучает вас относиться к данным как к общей маркерной доске. Вы объявляете объект или массив, передаете его функции, позволяете этой функции добавить свойство, а затем передаете его куда-то еще. Сборщик мусора сидит на заднем плане, как невидимый уборщик, ожидая возможности подмести всё, что вы оставили. Вы никогда не спрашиваете: «Кто владеет этой строкой?» Вы спрашиваете: «Могу ли я её прочитать?» Если ответ «да», вы с ней работаете. Если вам нужна еще одна ссылка, вы просто пишете let b = a и идете дальше. Обе переменные указывают на один и тот же участок памяти, и если a изменяет его, b мгновенно видит изменения. Это удобно. Это также хаотично, но этот хаос редко бьет вас по рукам, потому что среда выполнения берет на себя очистку.
Первый шок от компилятора
Rust вам не доверяет. Звучит сурово, но это первое, что вы узнаете. Язык построен вокруг системы владения с тремя строгими правилами. Во-первых, у каждого значения есть ровно один владелец. Во-вторых, в каждый момент времени владелец может быть только один. В-третьих, когда владелец выходит из области видимости, Rust автоматически удаляет значение. Никакого сборщика мусора. Никакого подсчета ссылок в фоновом режиме, если только вы не включите его явно. Только эти три правила, которые компилятор проверяет еще до того, как ваш код будет запущен.
Когда я писал Rust-версию той JavaScript-утилиты, я делал то, что казалось естественным. Я создал вектор, который является аналогом массива в Rust, и присвоил его второй переменной. Затем я попытался использовать первую. Компилятор отказался. В JavaScript let b = a копирует ссылку. В Rust это перемещает владение. Исходная переменная становится недействительной. Компилятор делает это, чтобы гарантировать, что два пути выполнения случайно не начнут обращаться к одной и той же области памяти — именно так в других системах возникают состояния гонки и ошибки использования памяти после освобождения.
Почему Rust отбирает ваши игрушки
Посмотрите на этот паттерн в JavaScript:
let a = [1, 2, 3];
let b = a;
a.push(4);
// Both a and b see 4.
Это происходит на автомате. Вы бы даже не задумались. В Rust та же интуиция приведет к отказу компилятора еще до того, как вы закончите печатать. Как только b становится владельцем вектора, a превращается в пустое имя. Вы не можете добавить в него элементы. Вы не можете даже взглянуть на него. Теперь память принадлежит b.
Это кажется наказанием, пока вы не поймете, чего это позволяет избежать. Если бы две переменные могли одновременно изменять одни и те же данные в куче без координации, вы бы рисковали повреждением памяти. Rust устраняет всю эту категорию багов, делая передачу владения явной. Компилятор не придирается. Он выступает в роли привратника. Он заставляет вас на каждом этапе решать, какая часть вашего кода отвечает за какие данные.
Заимствование: обходной путь, который на самом деле является фичей
Конечно, если бы каждое присваивание навсегда передавало владение, написание программ было бы изнурительным. Вам пришлось бы клонировать всё подряд, тратя память и время. Rust решает эту проблему с помощью заимствования.
Заимствование позволяет использовать значение, не забирая его себе. Существует два типа, и различие между ними имеет значение.
- Неизменяемые заимствования, записываемые как
&T: Они позволяют вам читать данные. Вы можете иметь столько, сколько захотите, в то же
