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

Семантика значений

Один вопрос отделяет питониста от понимания Mojo: что такое переменная.

В Python переменная — это имя, привязанное к объекту. Объект живёт сам по себе, имён у него может быть сколько угодно.

В Mojo переменная — это само значение. У значения один владелец, и второго имени у него не бывает.

a = [1, 2, 3]
b = a
b.append(4)
print(len(a), len(b)) # 4 4

Список один, имён два. Многие узнают об этом в первый раз, когда непонятно почему меняются «чужие» данные.

В Mojo эта строка просто не соберётся:

error: value of type 'List[Int]' cannot be implicitly copied, it does not conform to 'ImplicitlyCopyable'
Что это значит

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

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

var b = a.copy() — независимая копия.

var b = a^ — то же значение, новый владелец, a больше недоступна.

🐍 Python
def add(item, target=[]):
target.append(item)
return target
print(add(1)) # [1]
print(add(2)) # [1, 2] — сюрприз
🔥 Mojo
def add(item: Int, var target: List[Int] = []) -> Int:
target.append(item)
return len(target)
# add(1), add(2), add(3) → 1 1 1

Знаменитая питоновская ловушка здесь не воспроизводится, но не потому, что значение по умолчанию пересоздаётся — оно, как и в Python, вычисляется один раз. Спасает другое: var-аргумент получает собственное значение, и правка наружу не утекает. А изменяемому аргументу (mut) значение по умолчанию вообще запрещено — error: 'mut' arguments must not have defaults.

У стандартных контейнеров и у копий, которые компилятор синтезирует сам, поверхностных копий — таких, как у list.copy() в Python, — не бывает:

def main():
var first: List[String] = ["раз", "два"]
var second = first.copy()
second[0] = "изменено"
print(first[0], second[0])
Результат

раз изменено

То же и со структурами: копируется вся вложенность целиком.

@fieldwise_init
struct Point(ImplicitlyCopyable):
var x: Int
@fieldwise_init
struct Line(ImplicitlyCopyable):
var start: Point
var end: Point
def main():
var a = Line(Point(1), Point(2))
var b = a
b.start.x = 99
print(a.start.x, b.start.x)
Результат

1 99

В Python аналогичный код потребовал бы copy.deepcopy, и вы бы каждый раз думали, хватит ли обычной копии. Здесь думать не о чем: копия — значит копия.

Единственная оговорка — метод copy(), написанный руками: что в нём происходит, решает автор структуры. Всё, что синтезирует компилятор, копируется целиком.

Раз копия глубокая, структура может копироваться только если копируются все её поля:

error: cannot synthesize implicit copy constructor because field 'items' has non-implicitly-copyable type 'List[Int]'
Что это значит

Вы объявили структуру ImplicitlyCopyable, но внутри лежит List — а список сам по себе неявно не копируется, потому что это дорого.

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

Объявите структуру Copyable вместо ImplicitlyCopyable: тогда копия будет работать, но её придётся запрашивать явно — через .copy(). Это ровно то поведение, которое вы хотите для контейнера.

Правило простое и полезное: дорогое не копируется молча. Структура из трёх чисел копируется как число; структура со списком внутри — только по запросу.

Самое частое заблуждение новичка: «раз нет ссылок, значит всё копируется на каждом шагу». Это не так — копий гораздо меньше, чем кажется.

copies.mojo
# Где Mojo копирует значение, а где обходится без копии.
@fieldwise_init
struct Tracked(Copyable):
"""Значение, которое сообщает о каждом своём копировании."""
var name: String
var value: Int
def copy(self) -> Self:
"""Создаёт копию и громко об этом сообщает."""
print(" >>> копируем", self.name)
return Tracked(self.name, self.value)
def show(self):
"""Печатает имя и текущее значение."""
print(" ", self.name, "=", self.value)
def look(item: Tracked):
"""Только читает аргумент."""
item.show()
def take(var item: Tracked):
"""Забирает владение аргументом."""
item.show()
def make(name: String) -> Tracked:
"""Создаёт значение и возвращает его наружу."""
return Tracked(name, 100)
def main():
var original = Tracked("оригинал", 1)
print("1. читаем аргумент — копии нет:")
look(original)
print("2. возвращаем из функции — копии нет:")
var made = make("созданный")
made.show()
print("3. копируем явно — копия одна:")
var duplicate = original.copy()
duplicate.name = "копия"
duplicate.value = 2
original.show()
duplicate.show()
print("4. передаём владение — копии нет:")
take(original^)
print("5. копия всегда глубокая:")
var first: List[String] = ["раз", "два"]
var second = first.copy()
second[0] = "изменено"
print(" ", first[0], "|", second[0])
Результат
1. читаем аргумент — копии нет:
   оригинал = 1
2. возвращаем из функции — копии нет:
   созданный = 100
3. копируем явно — копия одна:
   >>> копируем оригинал
   оригинал = 1
   копия = 2
4. передаём владение — копии нет:
   оригинал = 1
5. копия всегда глубокая:
   раз | изменено

Структура печатает сообщение при каждом копировании — и за всю программу оно появилось ровно один раз, там, где мы написали .copy() сами.

Что происходитКопияПочему
look(item) — обычный аргументнетфункция получает ссылку только для чтения
take(var item) с original^нетвладение переехало, данные остались на месте
return Tracked(...) — новое значениенетоно сразу строится там, где будет жить
original.copy()давы попросили
return self.field — готовое полене компилируетсяполе принадлежит структуре: нужен .copy() или ^
items.append(value)не компилируетсяконтейнер станет владельцем: нужен value^ или value.copy()

Две последние строки — не придирка компилятора, а то же правило единственного владельца: положить значение в контейнер или вернуть чужое поле наружу можно только решив, кому оно теперь принадлежит.

ТипСтоимость копии
Int, Float64, Boolнесколько байт, обычно прямо в регистрах
SIMDдо размера векторного регистра — от 4 байт до нескольких сотен
маленькая структура из чиселстолько же, сколько её полей
Stringзависит от длины текста
List, Dictпропорционально числу элементов, с выделением памяти

Граница между трейтами из главы Структуры проходит не строго по этой таблице. Неявно копируются Int, Float64, Bool, SIMD — и String тоже. List и Dict требуют явного .copy().

String попал в первую группу как компромисс: копия строки стоит денег, но встречается настолько часто, что требовать .copy() в каждой второй строке кода было бы мучением.

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

Чем переменная в Mojo отличается от переменной в Python?

В Python переменная — имя, привязанное к объекту, и имён у объекта может быть много. В Mojo переменная — само значение, и владелец у него ровно один. Поэтому питоновский b = a с общим списком здесь невозможен.

Копия списка в Mojo поверхностная или глубокая?

Всегда глубокая: копируются и элементы, и вся вложенность. Аналога copy.deepcopy не нужно, а поверхностной копии, как у питоновского list.copy(), попросту нет.

Почему структура со списком внутри не может быть `ImplicitlyCopyable`?

Потому что копия глубокая: чтобы скопировать структуру, надо скопировать список, а это дорого. Компилятор откажется синтезировать неявный конструктор копирования и предложит объявить Copyable — тогда копия останется возможной, но только по явному .copy().

Копируется ли значение при передаче в функцию обычным аргументом?

Нет. Обычный аргумент — это ссылка только для чтения. Копии нет также при передаче владения через ^ и при возврате из функции только что созданного значения. А вот положить значение в список или вернуть наружу поле структуры без явного ^ или .copy() не выйдет: там решается, кто станет новым владельцем.

Конвенции передачи аргументов: что именно означают mut, var, ref и out в сигнатуре и как выбрать нужный.

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

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