1. Основы владения и заимствования: Введение в синтаксис лайфтаймов и роль Borrow Checker
Основы владения и заимствования: Введение в синтаксис лайфтаймов и роль Borrow Checker
Добро пожаловать в курс «Мастерство Lifetimes в Rust». Если вы выбрали этот путь, значит, вы уже знакомы с базовым синтаксисом Rust и, вероятно, уже сталкивались с сообщениями компилятора, которые кажутся загадочными или даже враждебными. Особенно теми, что касаются времени жизни (lifetimes).
В этой первой статье мы заложим фундамент. Мы не будем сразу нырять в сложные ковариантности или высшие ранги (HRTB) — всему свое время. Сегодня наша цель — понять физику процесса: почему Rust вообще требует от нас управления временем жизни ссылок и как работает тот самый строгий судья — Borrow Checker.
Почему Rust такой строгий?
Многие современные языки программирования (Java, Python, Go) используют сборщик мусора (Garbage Collector — GC). GC — это удобный механизм, который бегает за вашей программой и подчищает память, на которую больше никто не ссылается. Это удобно, но имеет свою цену: непредсказуемые паузы и накладные расходы на производительность.
Другие языки (C, C++) отдают управление памятью полностью в руки программиста. Это дает максимальную скорость, но открывает ящик Пандоры с ошибками: утечки памяти, двойные освобождения и, самое страшное, — висячие ссылки (dangling pointers).
Rust выбирает третий путь. Он гарантирует безопасность памяти без сборщика мусора. Как? С помощью системы владения (Ownership) и заимствования (Borrowing). И именно здесь на сцену выходят лайфтаймы.
Три закона владения
Давайте быстро освежим в памяти три столпа, на которых держится Rust:
Эти правила просты, пока мы просто передаем значения. Сложности начинаются, когда мы хотим сослаться на данные, не забирая владения.
Проблема висячих ссылок
Представьте ситуацию: у вас есть ссылка на данные, но сами данные уже были удалены из памяти. Если вы попытаетесь прочитать что-то по этой ссылке, программа может упасть, прочитать мусор или, что хуже, создать уязвимость в безопасности. Это и есть висячая ссылка.
Рассмотрим классический пример, который Rust не позволит скомпилировать:
В этом коде:
* Переменная r объявлена во внешней области видимости.
* Переменная x объявлена во внутренней области видимости (внутри фигурных скобок).
* Мы пытаемся присвоить r ссылку на x.
* Блок заканчивается, x уничтожается.
* Мы пытаемся использовать r.
Компилятор Rust выдаст ошибку: x does not live long enough (x живет недостаточно долго). Чтобы предотвратить такие ошибки, Rust использует Borrow Checker.
Роль Borrow Checker
Borrow Checker (проверка заимствований) — это часть компилятора, которая анализирует, как долго живут данные и как долго живут ссылки на них. Его задача — убедиться, что ни одна ссылка не переживет данные, на которые она указывает.
В примере выше Borrow Checker видит два периода жизни:
r (назовем его 'a).x (назовем его 'b).Правило Borrow Checker гласит: если r ссылается на x, то время жизни 'b (данных) должно быть больше или равно времени жизни 'a (ссылки). В нашем примере 'b короче 'a, поэтому компиляция запрещена.
Что такое лайфтаймы (Lifetimes)?
Лайфтайм — это именованная область кода, в которой ссылка является валидной.
Важно понимать фундаментальную вещь: лайфтаймы не изменяют время жизни данных. Вы не можете с помощью синтаксиса лайфтаймов заставить переменную жить дольше, чем она должна. Лайфтаймы — это лишь метки для компилятора, помогающие ему понять взаимосвязь между ссылками.
Неявные лайфтаймы
Вы уже использовали лайфтаймы, даже если не писали их явно. В большинстве простых случаев Rust способен вывести их автоматически. Это называется Lifetime Elision (сокрытие лайфтаймов).
Например, функция:
Для компилятора она выглядит так:
Компилятор говорит: «Ага, функция принимает одну ссылку и возвращает одну ссылку. Скорее всего, возвращаемая ссылка — это часть входной ссылки. Значит, их времена жизни связаны».
Когда автоматика не справляется
Проблемы возникают, когда у функции несколько входных параметров, являющихся ссылками, и мы возвращаем ссылку. Рассмотрим функцию, которая возвращает самую длинную из двух строк:
Этот код не скомпилируется. Ошибка будет гласить: missing lifetime specifier.
Почему? Потому что Rust не знает, какую именно ссылку мы вернем — x или y.
* Если мы вернем x, то возвращаемая ссылка будет валидна столько же, сколько x.
* Если y — то столько же, сколько y.
Поскольку условие if вычисляется в рантайме (во время выполнения), компилятор на этапе компиляции не может знать, чье время жизни использовать. Ему нужна помощь.
Синтаксис лайфтаймов
Аннотации времени жизни начинаются с апострофа ', за которым следует имя. Обычно используются короткие имена в нижнем регистре, например 'a, 'b, 'c.
* &i32 — обычная ссылка.
* &'a i32 — ссылка с явным временем жизни 'a.
* &'a mut i32 — изменяемая ссылка с явным временем жизни 'a.
Аннотирование функций
Чтобы исправить функцию longest, мы должны явно сказать компилятору: «Возвращаемая ссылка будет жить столько же, сколько живет минимальная из двух переданных ссылок».
Мы делаем это, объявляя параметры лайфтайма, подобно тому, как мы объявляем обобщенные типы (generics):
Давайте разберем этот синтаксис по частям:
fn longest<'a>: Мы объявляем, что функция использует параметр времени жизни 'a.(x: &'a str, y: &'a str): Оба параметра x и y являются ссылками, которые живут как минимум столько же, сколько 'a.-> &'a str: Возвращаемое значение также будет ссылкой, живущей как минимум столько же, сколько 'a.Что это означает на практике?
Когда мы вызываем эту функцию, 'a станет равным пересечению времен жизни x и y. То есть 'a будет равно меньшему из двух времен жизни. Таким образом, Borrow Checker гарантирует, что возвращаемая ссылка не будет использована после того, как один из аргументов станет невалидным.
Пример использования аннотированной функции
Посмотрим, как это работает в main:
Если бы мы попытались использовать result после закрывающей скобки внутреннего блока, Rust выдал бы ошибку, так как время жизни 'a (привязанное к string2) истекло.
Лайфтаймы в структурах
До сих пор мы говорили о функциях. Но что, если мы хотим сохранить ссылку внутри структуры?
Здесь мы обязаны указать лайфтайм 'a для структуры ImportantExcerpt.
Запись struct ImportantExcerpt<'a> означает: экземпляр этой структуры не может пережить ссылку, которую он хранит в поле part. Если строка novel будет удалена, то и экземпляр i станет невалидным.
Резюме
В следующей статье мы углубимся в Lifetime Elision (правила, по которым Rust не требует явных аннотаций) и разберем более сложные сценарии взаимодействия лайфтаймов.