Зачем нужно владение
Вы уже несколько раз натыкались на это в курсе: 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++.
# Когда именно Mojo уничтожает значение.
@fieldwise_initstruct 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 больше не нужен --- функция получила владение: третий уничтожен: третий --- владение передано ---
Разберём вывод по шагам:
firstуничтожается сразу после третьегоfirst.use()— там его последнее использование. Не в концеmain, не при выходе из блока: ровно там, где он перестал быть нужен.secondживёт дольше, потому что дольше используется. Порядок уничтожения определяется не порядком создания, а логикой программы.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 приводит к обращению к освобождённой памяти — и который здесь ловится до запуска программы.
a = [1, 2, 3]b = ab.append(4)print(len(a), len(b)) # 4 4 — сюрприз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, и во что это обходится на практике.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0