Копирование и перемещение
This content is not available in your language yet.
Отдать значение другому владельцу можно двумя способами: скопировать или передать. Разница между ними — это разница между «сделать второй экземпляр» и «сменить табличку с именем на том же самом».
Копия стоит денег. Передача — бесплатна.
Что делает ^
Заголовок раздела «Что делает ^»Сигил ^ означает: значение уходит отсюда навсегда.
@fieldwise_initstruct Snapshot(Copyable): var rows: List[Int]
def copy(self) -> Self: print("копируем") return Snapshot(self.rows.copy())
def main(): var first = Snapshot([1, 2, 3]) var second = first^ print("после перемещения:", len(second.rows))после перемещения: 3
Обратите внимание: слова «копируем» в выводе нет. Данные никуда не переезжали и не дублировались — сменился только владелец. Именно поэтому перемещение не зависит от объёма данных: передать список из миллиона элементов стоит ровно столько же, сколько список из трёх.
После перемещения переменной нет
Заголовок раздела «После перемещения переменной нет»Мы уже видели эту ошибку в первой главе раздела, но здесь она встречается чаще всего:
error: use of uninitialized value 'a'
Значение ушло к новому владельцу, а вы обращаетесь к старой переменной. Компилятор считает её пустой — как если бы ей вообще ничего не присваивали.
Работайте с новым владельцем. Если значение нужно в обоих местах,
вам нужна была копия: .copy() вместо ^.
Два случая, где эта ошибка удивляет:
Перемещение внутри цикла. На первой итерации значение уходит, а дальше брать нечего:
var handle = Connection("файл")for _ in range(3): use(handle^) # ошибка компиляции: на второй итерации брать нечегоПеремещение внутри if. Здесь компилятор строг: даже если ветка может
и не выполниться, после неё переменная считается возможно пустой.
if condition: use(handle^)print(handle.name) # ошибка: компилятор не знает, выполнилась ли веткаMove-only типы
Заголовок раздела «Move-only типы»Некоторые вещи нельзя копировать по смыслу. Открытый файл, сетевое соединение, захваченная блокировка — второй экземпляр такого объекта не просто бесполезен, он опасен: закроют дважды, освободят дважды.
В Mojo такой тип получается сам собой — достаточно не объявлять трейты копирования:
@fieldwise_initstruct Connection: var name: StringВсё, это move-only тип. Передавать его можно сколько угодно, а копию не сделать даже явно:
error: 'Connection' value has no attribute 'copy'
Метода copy() у типа просто нет: он появляется только вместе
с трейтом Copyable.
Если копия действительно нужна — добавьте Copyable и напишите, что
значит «скопировать» для вашего типа. Если не нужна — всё правильно,
передавайте значение через ^.
Сравните с Python: там second = first даёт два имени для одного файла,
и закрыть его может любой из них — второй продолжит им пользоваться
и получит ValueError: I/O operation on closed file уже во время работы
программы. С сырым дескриптором хуже: двойной os.close(fd) в лучшем
случае даст OSError, а если номер успели переиспользовать — закроет
чужой файл.
Напоминание про три уровня
Заголовок раздела «Напоминание про три уровня»| Что объявлено | Копия | Перемещение |
|---|---|---|
| ничего | нельзя вообще | var b = a^ |
Copyable | явная: a.copy() | var b = a^ |
ImplicitlyCopyable | обычное var b = a | var b = a^ |
Перемещение доступно всегда и никаких трейтов не требует. Трейты управляют только копированием.
Перемещать из поля структуры нельзя
Заголовок раздела «Перемещать из поля структуры нельзя»Ещё одна ошибка, на которую натыкаются рано:
error: field 'box.item' destroyed out of the middle of a value, preventing the overall value from being destroyed
Вы попытались забрать поле из структуры: var taken = box.item^.
Тогда в структуре образуется дыра — часть её уничтожена, часть жива,
и уничтожить её целиком уже невозможно.
Работает три способа:
- метод, принимающий
deinit self, — он разбирает структуру целиком:def into_item(deinit self) -> Item: return self.item^, вызовbox^.into_item(); - копия поля:
box.item.copy(); - хранить поле в
Optionalи забирать через.take().
А вот переместить структуру целиком и потом забрать поле не помогает: дыра просто переедет в новую переменную, и ошибка повторится.
Правило касается только полей, которые нужно как-то уничтожать. Забрать
^ из поля типа Int компилятор разрешит — там и уничтожать нечего,
он лишь заметит, что сигил бесполезен:
warning: transfer from a value of trivial register type 'Int' has no effect and can be removedОбмен без копий
Заголовок раздела «Обмен без копий»Для частой задачи «поменять два значения местами» в стандартной библиотеке есть готовая функция — она работает перемещениями, без единой копии:
def main(): var left = String("раз") var right = String("два") swap(left, right) print(left, right)два раз
Сколько что стоит
Заголовок раздела «Сколько что стоит»| Операция | Стоимость |
|---|---|
var b = a^ — перемещение | размер самой структуры, обычно несколько машинных слов; куча не трогается |
var b = a для Int и маленькой структуры | несколько байт |
копия String | выделение памяти под текст |
a.copy() для List, Dict | пропорционально числу элементов, с выделением памяти |
| передача обычным аргументом | ничего, это ссылка |
Про перемещение важно понимать точно: оно копирует само значение — для списка это указатель, длина и ёмкость, три машинных слова, — но не трогает данные в куче. Поэтому переместить список из миллиона элементов стоит ровно столько же, сколько список из трёх.
Практическое правило: если после операции старое значение вам больше
не нужно — пишите ^.
Только не ради скорости: на последнем использовании компилятор и сам
не копирует, а перемещает — проверено, метод copy() в такой ситуации
не вызывается. ^ вы пишете для читателя: из кода видно, где значение
заканчивает свой путь.
Всё вместе
Заголовок раздела «Всё вместе»# Копирование и перемещение: что бесплатно, а что стоит денег.
@fieldwise_initstruct Connection: """Соединение с базой. Копировать его нельзя — только передавать."""
var name: String
def query(self, sql: String): """Выполняет запрос.""" print(" ", self.name, "->", sql)
def __deinit__(deinit self): """Закрывает соединение, когда оно перестало быть нужным.""" print(" закрыто:", self.name)
@fieldwise_initstruct Snapshot(Copyable): """Выгрузка данных. Копировать можно, но видно, что это не бесплатно."""
var rows: List[Int]
def copy(self) -> Self: """Создаёт копию и сообщает, сколько строк пришлось скопировать.""" print(" копируем строк:", len(self.rows)) return Snapshot(self.rows.copy())
def run(var connection: Connection, sql: String): """Забирает соединение себе — и закрывает его, когда закончит.""" connection.query(sql)
def main(): print("1. перемещение: объект тот же, копии нет") var primary = Connection("основное") var moved = primary^ moved.query("SELECT 1")
print("2. владение уходит в функцию — она же и закрывает") run(moved^, "SELECT 2") print(" вернулись из функции")
print("3. копия — только по явной просьбе") var snapshot = Snapshot([1, 2, 3]) var backup = snapshot.copy() print(" строк в оригинале и копии:", len(snapshot.rows), len(backup.rows))
print("4. соединения кладём в список — тоже перемещением") var pool = List[Connection]() pool.append(Connection("первое")) pool.append(Connection("второе")) print(" в пуле:", len(pool))
print("5. обмен значениями без единой копии") var left = String("раз") var right = String("два") swap(left, right) print(" ", left, right)1. перемещение: объект тот же, копии нет основное -> SELECT 1 2. владение уходит в функцию — она же и закрывает основное -> SELECT 2 закрыто: основное вернулись из функции 3. копия — только по явной просьбе копируем строк: 3 строк в оригинале и копии: 3 3 4. соединения кладём в список — тоже перемещением закрыто: первое закрыто: второе в пуле: 2 5. обмен значениями без единой копии два раз
Разберём два места, где вывод выглядит неожиданно.
«закрыто: основное» печатается раньше, чем «вернулись из функции».
Соединение ушло в run вместе с владением, и закрылось там же, внутри
функции, — а не после возврата.
Соединения из пула закрываются до строки «в пуле: 2». Это то самое
правило из главы Зачем нужно владение: значение живёт
до последнего использования, и гранулярность здесь мельче строки.
Последнее использование pool — вычисление len(pool); как только длина
посчитана, список больше не нужен и уничтожается, хотя print ещё
не успел ничего напечатать.
🎯 Проверь себя
Сколько стоит переместить список из миллиона элементов?
Практически столько же, сколько список из трёх элементов. Перемещение копирует только само значение — для списка это указатель, длина и ёмкость, — а данные остаются на своём месте в куче.
Как сделать тип, который нельзя копировать?
Не объявлять у него трейтов копирования. Структура без Copyable
и ImplicitlyCopyable перемещается через ^, а метода copy()
у неё просто нет. Так делают для соединений, файлов и блокировок —
всего, что нельзя иметь в двух экземплярах.
Почему нельзя написать `var item = box.field^`?
Потому что в структуре осталась бы дыра: одно поле уничтожено,
остальные живы, и уничтожить структуру целиком уже нельзя. Выходы:
метод с deinit self, копия поля или Optional с .take().
Почему перемещение внутри `if` делает переменную недоступной и после `if`?
Компилятор не отслеживает, выполнилась ветка или нет: раз в каком-то из путей значение ушло, дальше переменная считается пустой. Правило простое и предсказуемое — это лучше, чем анализ, который иногда срабатывает, а иногда нет.
Что дальше
Заголовок раздела «Что дальше»Времена жизни и origins: как компилятор
доказывает, что ссылка не переживёт то, на что указывает, — и что делать
с ошибкой про origin_of, которую мы уже встречали.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0