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

Времена жизни и origins

Самая коварная ошибка при работе с памятью — ссылка, которая пережила то, на что указывала. В C она называется висячим указателем и приводит к чтению чужих данных. В Python её просто не бывает: пока на объект есть обычная ссылка, он жив.

Mojo идёт третьим путём: сборщика мусора нет, но компилятор доказывает, что ссылка не переживёт своё значение. Инструмент для этого доказательства называется origin — происхождение.

Origin — это ярлык «откуда взялась ссылка». Не тип, не значение, а пометка на этапе компиляции: вот эта ссылка происходит из вон той переменной.

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

Вы уже видели origin в главе про конвенции:

def longer(ref a: String, ref b: String) -> ref [a, b] String:
return a if a.byte_length() > b.byte_length() else b

В квадратных скобках после ref — список источников. Здесь: «результат происходит либо из a, либо из b».

Вот главная тонкость, на которой спотыкаются все. Попробуем вернуть ссылку на поле структуры:

def balance_ref(ref self) -> ref [self] Int:
return self.balance # ошибка
error: cannot return reference with incompatible origin: 'origin_of(self.balance)' vs 'origin_of(self)'
Что это значит

Вы обещали ссылку с происхождением «структура целиком», а вернули ссылку на её поле. Для компилятора это разные происхождения: у поля своё, у структуры своё.

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

Назовите в скобках то самое место, откуда берёте ссылку:

def balance_ref(ref self) -> ref [self.balance] Int:
return self.balance

Правило простое: что возвращаете — то и указывайте в скобках.

Тот же приём решает задачу, которую мы дважды откладывали. Наивная попытка:

def biggest(ref numbers: List[Int]) -> ref [numbers] Int:
return numbers[0] # ошибка
error: cannot return reference with incompatible origin: 'origin_of(numbers["element"])' vs 'origin_of(numbers)'

Компилятор говорит прямым текстом: у элемента происхождение numbers["element"], а вы назвали numbers. Записывается это так же — местом, откуда берётся ссылка:

def biggest(ref numbers: List[Int]) -> ref [numbers[0]] Int:
var best = 0
for index in range(len(numbers)):
if numbers[index] > numbers[best]:
best = index
return numbers[best]
def main():
var scores: List[Int] = [3, 9, 4]
print(biggest(scores))
biggest(scores) = 0
print(scores[0], scores[1], scores[2])
Результат
9
3 0 4

Индекс в скобках — не «нулевой элемент», а способ сказать «элемент этого списка». Возвращать можно любой: в примере вернулся элемент с индексом 1, и запись через результат обнулила именно его.

Насколько это чистый маркер, видно из простого опыта: ref [numbers[999]] для списка из трёх элементов компилируется и работает. Индекс в скобках вообще не вычисляется — компилятор читает из него только «элемент вот этого списка».

Теперь самое интересное. Возьмём ссылку на элемент, изменим список и попробуем ей воспользоваться:

var xs: List[Int] = [1, 2, 3]
var p = Pointer(to=xs[0])
xs.append(4)
print(p[]) # ошибка
error: use of invalidated interior reference 'xs["element"]'
Что это значит

append может перевыделить память под список, и все ссылки внутрь него перестают быть действительными. Компилятор это знает и указывает на строку, где всё сломалось:

note: origin was invalidated here
xs.append(4)
Как исправить

Либо возьмите ссылку заново после изменения списка, либо запомните не ссылку, а индекс — он переживёт перевыделение.

Проверено, что происхождение рушат append, insert, pop, clear и resize — всё, что может подвинуть данные. А вот чтение и запись по другому индексу ссылку не ломают: xs[1] = 42 при живой ссылке на xs[0] компилируется.

Работает это и через границу функции. Если список меняет вызванная функция, компилятор всё равно поймает — и покажет строку вызова:

note: origin was invalidated here
grow(xs)

Это тот самый класс ошибок, который в C++ называется iterator invalidation: изменили вектор во время обхода — и получили неопределённое поведение, которое проявится когда-нибудь потом и не там. Если ссылку вы держите явно, здесь программа просто не соберётся.

🐍 Python
xs = [1, 2, 3]
it = iter(xs)
xs.append(4)
# RuntimeError только у dict и set,
# у списка — молча странное поведение
🔥 Mojo
var xs: List[Int] = [1, 2, 3]
var p = Pointer(to=xs[0])
xs.append(4)
print(p[]) # не компилируется

Python ловит изменение во время обхода только у словарей и множеств, и делает это во время выполнения. У списка он не ругается, но и памяти не портит: итератор просто хранит индекс, и вы получаете сюрприз в логике. Mojo ловит обращение к устаревшей ссылке до запуска — а вот если поймать не удалось, сюрприз будет уже в памяти. Об этом ниже.

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

def main():
var queue: List[Int] = [1, 2]
var head = Pointer(to=queue[0])
print(head[])
queue.append(3)
print(len(queue))
Результат
1
3

Здесь последнее использование head — до append, и никакого конфликта нет. Это то же правило «до последнего использования», что и у владения: ссылка живёт ровно столько, сколько нужна.

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

var s = String("привет")
var p = Pointer(to=s)
var moved = s^
print(p[]) # ошибка: use of uninitialized value 's'

Компилятор видит, что p ещё понадобится, и не даёт увезти s.

Указатель — это ссылка, которую можно положить в переменную

Заголовок раздела «Указатель — это ссылка, которую можно положить в переменную»

ref живёт в сигнатурах функций. Если ссылка нужна как обычное значение, берут Pointer:

def main():
var total = 10
var pointer = Pointer(to=total)
pointer[] += 5
print(total)
Результат
15

Разыменование — квадратные скобки без индекса: pointer[]. Origin у Pointer тоже есть, он просто спрятан в типе, поэтому все проверки из этой главы работают и для него.

Ссылку на локальную переменную не вернуть

Заголовок раздела «Ссылку на локальную переменную не вернуть»
def broken() -> ref [ ... ] String: # писать в скобках нечего
var local = String("исчезнет")
return local

Локальная переменная умрёт при выходе из функции, и любой ярлык происхождения оказался бы враньём. В любом безопасном коде компилятор это заметит:

error: cannot return reference with incompatible origin: 'origin_of(local)' vs ...

Возвращайте значение, а не ссылку на него, — как раз для этого владение и передаётся.

references.mojo
# Ссылки, которые переживают возврат из функции, — и их происхождение.
@fieldwise_init
struct Account:
"""Счёт: имя владельца и баланс."""
var owner: String
var balance: Int
def balance_ref(ref self) -> ref [self.balance] Int:
"""Возвращает ссылку на поле — через неё можно и читать, и писать."""
return self.balance
def biggest(ref numbers: List[Int]) -> ref [numbers[0]] Int:
"""Возвращает ссылку на наибольший элемент списка."""
var best = 0
for index in range(len(numbers)):
if numbers[index] > numbers[best]:
best = index
return numbers[best]
def main():
print("1. ссылка на элемент списка")
var scores: List[Int] = [3, 9, 4]
print(" максимум:", biggest(scores))
biggest(scores) = 0
print(" после обнуления максимума:", scores[0], scores[1], scores[2])
print("2. ссылка на поле структуры")
var account = Account("Анна", 100)
account.balance_ref() = 250
print(" ", account.owner, "->", account.balance)
print("3. указатель — та же ссылка, но как значение")
var total = 10
var pointer = Pointer(to=total)
pointer[] += 5
print(" total:", total)
print("4. ссылка живёт ровно до последнего использования")
var queue: List[Int] = [1, 2]
var head = Pointer(to=queue[0])
print(" первый элемент:", head[])
queue.append(3)
print(" после append длина:", len(queue))
Результат
1. ссылка на элемент списка
   максимум: 9
   после обнуления максимума: 3 0 4
2. ссылка на поле структуры
   Анна -> 250
3. указатель — та же ссылка, но как значение
   total: 15
4. ссылка живёт ровно до последнего использования
   первый элемент: 1
   после append длина: 3

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

Что пишут в квадратных скобках после `ref` в типе возврата?

Место, откуда берётся ссылка: ref [self.balance] для поля структуры, ref [numbers[0]] для элемента списка, ref [a, b] если источников несколько. Указать контейнер вместо самого места — частая ошибка: у поля и у элемента происхождение своё.

Почему `ref [numbers] Int` не годится для возврата элемента списка?

Потому что у элемента другое происхождение, чем у самого списка. Компилятор так и говорит: origin_of(numbers["element"]) вместо origin_of(numbers). Правильная запись — ref [numbers[0]], где индекс просто указывает на элемент этого списка, любой.

Что произойдёт, если сохранить ссылку на элемент и вызвать `append`?

Программа не соберётся: use of invalidated interior reference, и компилятор покажет строку, где происхождение стало недействительным. Но только если ссылкой пользуются ПОСЛЕ изменения — если её последнее использование раньше, всё в порядке.

Можно ли вернуть ссылку на локальную переменную функции?

Нет. Локальная переменная умрёт при выходе, и назвать её как источник ссылки нечем. Возвращайте само значение — передача владения для этого и существует.

Сравнение с Rust и C++: что в модели Mojo совпадает с Rust, чем она проще, и почему заимствований в привычном для Rust смысле здесь считать не приходится.

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

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