Времена жизни и origins
This content is not available in your language yet.
Самая коварная ошибка при работе с памятью — ссылка, которая пережила то, на что указывала. В C она называется висячим указателем и приводит к чтению чужих данных. В Python её просто не бывает: пока на объект есть обычная ссылка, он жив.
Mojo идёт третьим путём: сборщика мусора нет, но компилятор доказывает, что ссылка не переживёт своё значение. Инструмент для этого доказательства называется origin — происхождение.
Что такое origin
Заголовок раздела «Что такое 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: изменили вектор во время обхода — и получили неопределённое поведение, которое проявится когда-нибудь потом и не там. Если ссылку вы держите явно, здесь программа просто не соберётся.
xs = [1, 2, 3]it = iter(xs)xs.append(4)# RuntimeError только у dict и set,# у списка — молча странное поведение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 ...Возвращайте значение, а не ссылку на него, — как раз для этого владение и передаётся.
Всё вместе
Заголовок раздела «Всё вместе»# Ссылки, которые переживают возврат из функции, — и их происхождение.
@fieldwise_initstruct 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 смысле здесь считать не приходится.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0