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

Копирование и перемещение

Отдать значение другому владельцу можно двумя способами: скопировать или передать. Разница между ними — это разница между «сделать второй экземпляр» и «сменить табличку с именем на том же самом».

Копия стоит денег. Передача — бесплатна.

Сигил ^ означает: значение уходит отсюда навсегда.

@fieldwise_init
struct 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) # ошибка: компилятор не знает, выполнилась ли ветка

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

В Mojo такой тип получается сам собой — достаточно не объявлять трейты копирования:

@fieldwise_init
struct 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 = avar 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() в такой ситуации не вызывается. ^ вы пишете для читателя: из кода видно, где значение заканчивает свой путь.

handles.mojo
# Копирование и перемещение: что бесплатно, а что стоит денег.
@fieldwise_init
struct Connection:
"""Соединение с базой. Копировать его нельзя — только передавать."""
var name: String
def query(self, sql: String):
"""Выполняет запрос."""
print(" ", self.name, "->", sql)
def __deinit__(deinit self):
"""Закрывает соединение, когда оно перестало быть нужным."""
print(" закрыто:", self.name)
@fieldwise_init
struct 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, которую мы уже встречали.

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

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