Семантика значений
This content is not available in your language yet.
Один вопрос отделяет питониста от понимания Mojo: что такое переменная.
В Python переменная — это имя, привязанное к объекту. Объект живёт сам по себе, имён у него может быть сколько угодно.
В Mojo переменная — это само значение. У значения один владелец, и второго имени у него не бывает.
Классическая ловушка Python
Заголовок раздела «Классическая ловушка Python»a = [1, 2, 3]b = ab.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 больше недоступна.
def add(item, target=[]): target.append(item) return target
print(add(1)) # [1]print(add(2)) # [1, 2] — сюрприз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_initstruct Point(ImplicitlyCopyable): var x: Int
@fieldwise_initstruct 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().
Это ровно то поведение, которое вы хотите для контейнера.
Правило простое и полезное: дорогое не копируется молча. Структура из трёх чисел копируется как число; структура со списком внутри — только по запросу.
Где копий не происходит вовсе
Заголовок раздела «Где копий не происходит вовсе»Самое частое заблуждение новичка: «раз нет ссылок, значит всё копируется на каждом шагу». Это не так — копий гораздо меньше, чем кажется.
# Где Mojo копирует значение, а где обходится без копии.
@fieldwise_initstruct 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 в сигнатуре и как выбрать нужный.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0