Перейти к содержимому

Зачем нужно владение

Вы уже несколько раз натыкались на это в курсе: var b = a для списка не компилируется, у аргументов есть загадочные mut и var, а после a^ переменная a перестаёт существовать. Пора объяснить, откуда всё это растёт.

Ответ на удивление короткий: у каждого значения ровно один владелец, и компилятор знает, когда владелец им пользуется в последний раз.

Программа берёт память под данные. Рано или поздно её нужно вернуть. Вопрос «кто и когда это сделает» породил три разных ответа.

ПодходКто освобождаетЦена
Вручную и через RAII (C, C++)программист — сам или руками деструкторов, которые сам же и написалутечки, двойное освобождение, обращение к освобождённому
Сборщик мусора (Python, Java, Go, C#)среда выполнениянакладные расходы и потеря контроля над моментом освобождения
Владение (Rust, Mojo)компилятор на этапе сборкинужно объяснять компилятору свои намерения

Первый вариант быстрый, но опасный: по оценкам Microsoft и Google, около 70 % уязвимостей в их больших проектах на C и C++ — это ошибки работы с памятью. Второй безопасный, но за него платят скоростью и предсказуемостью.

Строк в таблице три, но ответов на самом деле больше: Swift, например, считает ссылки автоматически, и это не вполне ни то, ни другое.

Третий — это попытка получить безопасность первого подхода бесплатно: раз компилятор и так разбирает вашу программу целиком, пусть он сам и расставит освобождение памяти там, где нужно.

В Mojo оно формулируется в одну строку:

У значения в каждый момент времени ровно один владелец. Когда владелец заканчивается — заканчивается и значение.

Владелец — это переменная или аргумент функции. Передать владение можно (сигилом ^), поделить его между двумя переменными — нельзя.

Поле структуры значением тоже владеет, но собственного момента смерти у него нет: поле живёт ровно столько, сколько живёт сама структура.

Отсюда и та ошибка, которую вы видели в главе про структуры:

error: value of type 'List[Int]' cannot be implicitly copied, it does not conform to 'ImplicitlyCopyable'
Что это значит

var b = a требует, чтобы у значения стало два владельца. Так нельзя — значит, компилятору нужно сделать копию. А копия списка может стоить дорого, и молча он её делать не станет.

Как исправить

Скажите, чего вы хотите:

  • var b = a.copy() — да, копируем, я понимаю цену;
  • var b = a^ — не копируем, a передаёт владение и дальше недоступна.

Обратите внимание: это не запрет, а требование назвать намерение. Оба варианта разрешены, просто из кода теперь видно, какой из них выбран.

Уничтожение сразу после последнего использования

Заголовок раздела «Уничтожение сразу после последнего использования»

Вот здесь начинается самое интересное — и то, чего нет ни в Python, ни в C++, ни даже в Rust: там значение живёт до конца области видимости. Мы это проверили и показываем в главе Сравнение с Rust и C++.

lifetime.mojo
# Когда именно Mojo уничтожает значение.
@fieldwise_init
struct Resource:
"""Ресурс, который громко сообщает о своей жизни и смерти."""
var name: String
def use(self):
"""Отмечает очередное использование."""
print("используем:", self.name)
def __deinit__(deinit self):
"""Вызывается, когда значение больше не нужно."""
print("уничтожен:", self.name)
def consume(var item: Resource):
"""Забирает владение ресурсом себе."""
print("функция получила владение:", item.name)
def main():
var first = Resource("первый")
var second = Resource("второй")
first.use()
second.use()
first.use()
print("--- first больше не нужен ---")
second.use()
print("--- second больше не нужен ---")
var third = Resource("третий")
consume(third^)
print("--- владение передано ---")
Результат

используем: первый используем: второй используем: первый уничтожен: первый --- first больше не нужен --- используем: второй уничтожен: второй --- second больше не нужен --- функция получила владение: третий уничтожен: третий --- владение передано ---

Разберём вывод по шагам:

  1. first уничтожается сразу после третьего first.use() — там его последнее использование. Не в конце main, не при выходе из блока: ровно там, где он перестал быть нужен.
  2. second живёт дольше, потому что дольше используется. Порядок уничтожения определяется не порядком создания, а логикой программы.
  3. third передан в функцию через ^. Владение ушло вместе со значением, и уничтожается оно уже внутри consume — до того, как main напечатает последнюю строку.

Компилятор следит и за этим:

var a = Box(1)
var b = a^
print(a.value) # ошибка
error: use of uninitialized value 'a'
Что это значит

После a^ переменная a пуста: владение ушло к b. Компилятор относится к ней так же, как к переменной, которой вообще не присваивали значения.

Как исправить

Работайте с новым владельцем — b. Если значение нужно в обоих местах, вам нужна была не передача, а копия: var b = a.copy().

Это тот самый класс ошибок, который в C приводит к обращению к освобождённой памяти — и который здесь ловится до запуска программы.

🐍 Python
a = [1, 2, 3]
b = a
b.append(4)
print(len(a), len(b)) # 4 4 — сюрприз
🔥 Mojo
var a: List[Int] = [1, 2, 3]
var b = a.copy()
b.append(4)
print(len(a), len(b)) # 3 4

В Python b = a не копирует список, а заводит второе имя для того же объекта — и правка через одно имя видна через другое. В Mojo такой ситуации просто не возникает: либо копия, либо передача владения.

  • Нет сборщика мусора — значит, нет ни его пауз, ни его накладных расходов, ни счётчика ссылок в каждом объекте.
  • Детерминированное освобождение. Файл закроется, соединение разорвётся и замок освободится в предсказуемый момент, а не «когда-нибудь».
  • Безопасность без ручного управления. Обращение к освобождённому значению — ошибка компиляции, а не загадочное падение на проде.
  • Видимая стоимость. Копия большого списка не спрячется за невинным знаком равенства: чтобы её сделать, придётся написать .copy().

Цена одна: компилятору иногда нужно объяснять то, что в Python подразумевалось само. Первые дни это раздражает — а потом оказывается, что вы просто стали точнее понимать собственный код.

🎯 Проверь себя

Когда в Mojo уничтожается значение?

Сразу после последнего использования, а не в конце функции или блока. Поэтому порядок уничтожения определяется логикой программы, а не порядком создания переменных.

Почему `var b = a` для списка требует уточнения?

Потому что у значения может быть только один владелец. Двум переменным нужна либо копия, либо передача владения — и компилятор просит выбрать явно: a.copy() или a^. Копия списка стоит дорого, и молча её никто делать не будет.

Что происходит с переменной после `var b = a^`?

Она становится неинициализированной: владение ушло. Любое обращение к ней — ошибка компиляции use of uninitialized value.

Семантика значений: чем «переменная — это имя объекта» из Python отличается от «переменная — это само значение» в Mojo, и во что это обходится на практике.

Примеры проверены на Mojo 1.1.0

Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0