Жизнь значения¶
Срок жизни значения в Mojo начинается с момента инициализации переменной и продолжается до тех пор, пока значение не будет использовано в последний раз, после чего Mojo уничтожает его. На этой странице описано, как создается, копируется и перемещается каждое значение в Mojo.
Все типы данных в Mojo, включая базовые типы в стандартной библиотеке, такие как Bool, Int и String, определяются как структуры. Это означает, что создание и уничтожение любого фрагмента данных выполняется по одним и тем же правилам жизненного цикла, и вы можете определять свои собственные типы данных, которые работают точно так же.
В структурах Mojo нет методов жизненного цикла по умолчанию, таких как конструктор, конструктор копирования или конструктор перемещения. Это означает, что вы можете определить структуру без конструктора, но тогда вы не сможете создать ее экземпляр, и она будет полезна только как своего рода пространство имен для статических методов. Например:
struct NoInstances:
var state: Int
@staticmethod
fn print_hello():
print("Hello world!")
Без конструктора это не может быть создано, поэтому у него нет жизненного цикла. Поле state также бесполезно, поскольку его невозможно инициализировать (структуры Mojo не поддерживают значения полей по умолчанию — вы должны инициализировать их в конструкторе).
Итак, единственное, что вы можете сделать, это вызвать статический метод:
NoInstances.print_hello()
Hello world!
Конструктор¶
Чтобы создать экземпляр типа Mojo, ему нужен метод конструктора __init__(). Основная задача конструктора - инициализировать все поля. Например:
struct MyPet:
var name: String
var age: Int
fn __init__(out self, name: String, age: Int):
self.name = name
self.age = age
Теперь мы можем создать экземпляр:
var mine = MyPet("Loki", 4)
Экземпляр MyPet также можно прочитать и уничтожить, но в настоящее время его нельзя скопировать или переместить.
Мы считаем, что это хорошая отправная точка по умолчанию, поскольку в нем нет встроенных событий жизненного цикла и неожиданного поведения. Вы — автор типа — должны явно решить, можно ли копировать или перемещать тип и каким образом, реализовав конструкторы копирования и перемещения.
Показанный выше шаблон — конструктор, который принимает аргумент для каждого из полей структуры и инициализирует поля непосредственно из аргументов, — называется полевым конструктором. Это достаточно распространенный шаблон, по которому Mojo включает в себя декоратор @fieldwise_init для синтеза полевого конструктора. Таким образом, вы можете переписать предыдущий пример следующим образом:
@fieldwise_init
struct MyPet:
var name: String
var age: Int
Mojo не требует наличия деструктора для уничтожения экземпляра. Но в некоторых случаях вам может потребоваться определить пользовательский деструктор для освобождения ресурсов (например, если структура динамически выделяет память с помощью
UnsafePointer).Имя "конструктора" В классе Python создание объектов происходит с помощью методов
__new__()и__init__(), поэтому метод__init__()технически является просто инициализатором атрибутов (но его часто все еще называют конструктором). Однако в структуре Mojo нет метода__new__(), поэтому мы предпочитаем всегда вызывать конструктор__init__().
Перегрузка конструктора¶
Как и любая другая функция/метод, вы можете перегрузить конструктор __init__(), чтобы инициализировать объект другими аргументами. Например, вам может понадобиться конструктор по умолчанию, который устанавливает некоторые значения по умолчанию и не принимает никаких аргументов, а затем дополнительные конструкторы, которые принимают больше аргументов.
Просто имейте в виду, что для изменения любых полей каждый конструктор должен объявить аргумент self с помощью соглашения out. Если вы хотите вызвать один конструктор из другого, вы просто вызываете этот конструктор, как если бы вы делали это извне (вам не нужно передавать self).
Например, вот как вы можете делегировать работу перегруженному конструктору:
struct MyPet:
var name: String
var age: Int
fn __init__(out self):
self.name = ""
self.age = 0
fn __init__(out self, name: String):
self = MyPet()
self.name = name
Инициализация поля¶
Обратите внимание, что в предыдущем примере к концу каждого конструктора все поля должны быть инициализированы. Это единственное требование к конструктору.
На самом деле, конструктор __init__() достаточно умен, чтобы считать объект self полностью инициализированным даже до завершения работы конструктора, при условии, что все поля инициализированы. Например, этот конструктор может передавать self сразу после инициализации всех полей:
fn use(arg: MyPet):
pass
struct MyPet:
var name: String
var age: Int
fn __init__(out self, name: String, age: Int, cond: Bool):
self.name = name
if cond:
self.age = age
use(self) # Safe to use immediately!
self.age = age
use(self) # Safe to use immediately!
Конструкторы и неявное преобразование¶
Mojo поддерживает неявное преобразование из одного типа в другой. Неявное преобразование может произойти при выполнении одного из следующих действий:
- Вы присваиваете значение одного типа переменной другого типа.
- Вы передаете значение одного типа функции, для которой требуется другой тип.
- Вы возвращаете значение одного типа из функции, которая определяет другой возвращаемый тип.
Во всех случаях поддерживается неявное преобразование, когда целевой тип определяет конструктор, соответствующий следующим критериям:
- Объявляется с помощью
@implicitдекоратор. - Имеет единственный обязательный аргумент исходного типа, не связанный с ключевым словом.
Например:
var a = Source()
var b: Target = a
Mojo неявно преобразует Source значение a в Target значение, если Target определяет соответствующий конструктор следующим образом:
struct Target:
@implicit
fn __init__(out self, s: Source): ...
При неявном преобразовании приведенное выше назначение по существу идентично:
var b = Target(a)
В общем, типы должны поддерживать неявные преобразования только в том случае, если преобразование выполняется без потерь и в идеале недорого. Например, преобразование целого числа в число с плавающей запятой обычно происходит без потерь (за исключением очень больших положительных и отрицательных целых чисел, где преобразование может быть приблизительным), но преобразование числа с плавающей запятой в целое число с большой вероятностью приведет к потере информации. Таким образом, Mojo поддерживает неявное преобразование из Int в Float64, но не наоборот.
Конструктор, используемый для неявного преобразования, может принимать необязательные аргументы, поэтому следующий конструктор также будет поддерживать неявное преобразование из исходного кода в целевой:
struct Target:
@implicit
fn __init__(out self, s: Source, reverse: Bool = False): ...
Неявное преобразование может завершиться неудачей, если Mojo не может однозначно сопоставить преобразование с конструктором. Например, если целевой тип имеет два перегруженных конструктора, которые принимают разные типы, и каждый из этих типов поддерживает неявное преобразование из исходного типа, у компилятора есть два одинаково допустимых пути для преобразования значений:
struct A:
@implicit
fn __init__(out self, s: Source): ...
struct B:
@implicit
fn __init__(out self, s: Source): ...
struct OverloadedTarget:
@implicit
fn __init__(out self, a: A): ...
@implicit
fn __init__(out self, b: B): ...
var t = OverloadedTarget(Source()) # Error: ambiguous call to '__init__': each
# candidate requires 1 implicit conversion
В этом случае вы можете устранить проблему, явно выполнив приведение к одному из промежуточных типов. Например:
var t = OverloadedTarget(A(Source())) # OK
Mojo применяет к переменной не более одного неявного преобразования. Например:
var t: OverloadedTarget = Source() # Error: can't implicitly convert Source
# to Target
Это приведет к сбою, поскольку нет прямого преобразования из Source в OverloadedTarget.
Для структур с одним полем вы можете сгенерировать неявный конструктор с помощью декоратора @fieldwise_init("implicit").
@fieldwise_init("implicit")
struct Counter:
var count: Int
def main():
var c: Counter = 5 # неявно преобразует из Int
Конструктор копирования¶
В Mojo значение может быть скопировано как явно, так и неявно:
# Explicit copy
var s = "Test string"
var s2 = s.copy()
# Implicit copy
var i = 15
var i2 = i
Чтобы сделать структуру явно доступной для копирования, вам необходимо:
- Добавить trait
Copyable. - (Опционально) при необходимости определить пользовательский метод
__copyinit__().
Добавив свойство Copyable, Mojo может сгенерировать для вас метод __copyinit__() по умолчанию, если вы не написали его самостоятельно. Этот метод по умолчанию копирует каждое поле существующего значения в новое значение. Свойство Copyable также определяет метод copy() по умолчанию, который обеспечивает более удобный способ копирования значения, чем прямой вызов конструктора копирования.
@fieldwise_init
struct MyPet(Copyable):
var name: String
var age: Int
Теперь этот код работает для создания копии:
var mine = MyPet("Loki", 4)
var yours = mine.copy()
Технически, вы могли бы создать структуру с помощью конструктора копирования и не добавлять копируемый трейт(
Copyable), но это не рекомендуется. Mojo смог бы скопировать значение, но вы не смогли бы использовать структуру с какими-либо универсальными контейнерами или функциями, для которых требуется копируемый трейт.
Созданный конструктор копирования просто копирует каждое поле из существующего значения в новое. Например, если вы написали метод __copyinit__() для MyPet, это будет выглядеть следующим образом:
fn __copyinit__(out self, existing: Self):
self.name = existing.name
self.age = existing.age
Этот конструктор копирования по умолчанию работает в большинстве случаев, но есть несколько случаев, когда вам нужно определить пользовательский конструктор копирования:
- Одно или несколько полей структуры недоступны для копирования.
- Структура содержит тип, не являющийся владельцем (например,
UnsafePointer), и вы хотите создать глубокую копию данных. - Структура содержит другие ресурсы (например, файловые дескрипторы или сетевые сокеты), которыми необходимо управлять.
Пользовательский конструктор копирования¶
Что отличает поведение Mojo при копировании от других языков, так это то, что __copyinit__() предназначен для выполнения глубокого копирования всех полей в типе (в соответствии с семантикой значений). То есть он копирует значения, выделенные в куче, а не просто копирует указатель.
Однако компилятор Mojo не применяет этого, поэтому ответственность за реализацию __copyinit__() с семантикой значений лежит на авторе типа.
Например, вот новый тип HeapArray с пользовательским конструктором копирования, который выполняет глубокое копирование:
struct HeapArray(Copyable):
var data: UnsafePointer[Int, MutExternalOrigin]
var size: Int
var cap: Int
fn __init__(out self, size: Int, val: Int):
self.size = size
self.cap = size * 2
self.data = alloc[Int](self.cap)
for i in range(self.size):
(self.data + i).init_pointee_copy(val)
fn __copyinit__(out self, existing: Self):
# Deep-copy the existing value
self.size = existing.size
self.cap = existing.cap
self.data = alloc[Int](self.cap)
for i in range(self.size):
(self.data + i).init_pointee_copy(existing.data[i])
# The lifetime of `existing` continues unchanged
fn __del__(deinit self):
# We must free the heap-allocated data, but
# Mojo knows how to destroy the other fields
for i in range(self.size):
(self.data + i).destroy_pointee()
self.data.free()
fn append(mut self, val: Int):
# Update the array for demo purposes
if self.size < self.cap:
(self.data + self.size).init_pointee_copy(val)
self.size += 1
else:
print("Out of bounds")
fn dump(self):
# Print the array contents for demo purposes
print("[", end="")
for i in range(self.size):
if i > 0:
print(", ", end="")
print(self.data[i], end="")
print("]")
Обратите внимание, что __copyinit__() не копирует значение UnsafePointer(это приведет к тому, что скопированное значение будет ссылаться на тот же адрес памяти данных, что и исходное значение, которое является неполной копией). Вместо этого мы инициализируем новый UnsafePointer для выделения нового блока памяти, а затем копируем все значения, выделенные в куче (это глубокая копия).
Таким образом, когда мы копируем экземпляр HeapArray, каждая копия имеет свой собственный набор значений в куче, поэтому изменения в одном массиве не влияют на другой, как показано здесь:
fn copies():
var a = HeapArray(2, 1)
var b = a.copy() # Вызывает конструктор копирования
a.dump() # Prints [1, 1]
b.dump() # Prints [1, 1]
b.append(2) # Изменяет скопированные данные
b.dump() # Prints [1, 1, 2]
a.dump() # Prints [1, 1] (the original did not change)
Следует отметить еще две особенности метода __copyinit__():
-
Существующим аргументом является тип
Self(с заглавной буквой "S").Self- это псевдоним для текущего имени типа (в данном примере -HeapArray). Использование этого псевдонима является наилучшей практикой, позволяющей избежать любых ошибок при обращении к текущему имени структуры. -
Существующий аргумент является неизменяемым, поскольку по умолчанию используется соглашение о чтении аргументов(
read) — это хорошо, потому что эта функция не должна изменять содержимое копируемого значения.
В
HeapArrayмы должны использовать деструктор__del__(), чтобы освободить данные, размещенные в куче, когда заканчивается срок службыHeapArray, но Mojo автоматически уничтожает все остальные поля, когда заканчивается их соответствующий срок службы.
Если ваш тип не использует никаких указателей для данных, размещенных в куче, то написание конструктора и конструктора копирования - это все стандартный код, который вам не нужно писать. Для большинства структур, которые явно не управляют памятью, вы можете просто добавить свойство Copyable в свое определение структуры, и Mojo синтезирует метод __copyinit__().
Mojo также вызывает конструктор копирования, когда значение передается функции, которая принимает аргумент как var, и когда время жизни данного значения на этом этапе не заканчивается. Если время жизни значения на этом заканчивается (обычно обозначается символом переноса ^), то Mojo вместо этого вызывает конструктор перемещения.
Неявно копируемые типы¶
Чтобы сделать тип неявно копируемым, добавьте трейт ImplicitlyCopyable:
@fieldwise_init
struct MyPair(ImplicitlyCopyable):
var first: Int
var second: Int
def main():
pair = MyPair(3, 4)
copy = pair
print(pair.first, copy.second)
Трейт ImplicitlyCopyable наследуется от Copyable, поэтому добавление ImplicitlyCopyable к вашему типу дает вам конструктор копирования и метод copy() по умолчанию. (Это также обеспечивает автоматическое соответствие при перемещении). Этот трейт не определяет никаких собственных методов: он служит сигналом для компилятора о том, что он может вставлять вызовы в __copyinit__() по мере необходимости. Например, в предыдущем примере компилятор мог сгенерировать код, эквивалентный:
pair = MyPair(3, 4)
copy = MyPair.__copyinit__(pair)
Тип должен быть доступен для неявного копирования только в том случае, если копирование типа обходится недорого и не имеет побочных эффектов. Ненужные копии могут привести к значительному снижению памяти и производительности, поэтому используйте эту функцию с осторожностью.
В частности, любой тип, который динамически выделяет память или управляет другими ресурсами, вероятно, не должен быть неявно копируемым.
Конструктор перемещения¶
Хотя копирование значений обеспечивает предсказуемое поведение, соответствующее семантике значений Mojo, копирование некоторых типов данных может существенно снизить производительность.
Mojo использует конструктор перемещения для передачи права собственности на значение из одной переменной в другую без копирования ее полей. Значение, которое можно скопировать, но у которого нет конструктора перемещения, все равно можно перенести, создав копию, а затем удалив оригинал. Таким образом, в этом случае конструктор перемещения служит для оптимизации.
Чтобы сделать тип перемещаемым:
- Добавьте свойство
Movableили приведите его в соответствие сCopyable. - (Опционально) реализуйте пользовательский метод
__moveinit__().
Обратите внимание, что типы значений, передаваемые через регистр, автоматически изменяются и не могут определять пользовательский метод __moveinit__().
Вот изменяемая версия структуры MyPet:
@fieldwise_init
struct MyPet(Copyable):
var name: String
var age: Int
Вот пример, показывающий, как вызвать конструктор перемещения для MyPet:
fn moves():
var a = MyPet("Bobo", 2)
print(a.name) # Prints "bobo"
var b = a^ # the lifetime of `a` ends here
print(b.name) # prints "bobo"
# print(a.name) # ERROR: use of uninitialized value 'a'
Если вы реализуете Movable и не определяете конструктор перемещения, Mojo генерирует конструктор перемещения по умолчанию для вас. Этот конструктор перемещения просто перемещает каждое из полей в новый экземпляр.
Созданный конструктор перемещения для MyPet выглядел бы примерно так, если бы вы написали его самостоятельно:
fn __moveinit__(out self, deinit existing: Self):
self.name = existing.name^
self.age = existing.age
Конструктор перемещения принимает свой existing, используя deinit, которое предоставляет исключительное право собственности на значение и помечает его как уничтоженное в конце выполнения функции.
Конструктор перемещения использует символ переноса (^), чтобы указать, что право собственности на значение имени передается от existing к self. Для типов, передаваемых через регистр, таких как Int, символ переноса опущен: типы, передаваемые через регистр, всегда рассматриваются как перемещаемые, но они не могут определять пользовательские конструкторы или деструкторы перемещения, поэтому для перемещения нет специальной логики.
В конце метода __moveinit__() Mojo немедленно аннулирует исходную переменную, предотвращая любой доступ к ней. Он не вызывает деструктор, поскольку это привело бы к уничтожению ресурсов, которые были переданы новому экземпляру. Аннулирование исходной переменной важно для предотвращения ошибок памяти в данных, выделенных в куче, таких как ошибки использования после освобождения и двойного освобождения.
Конструктор перемещения не требуется для передачи права собственности на значение. Mojo может скопировать значение и аннулировать исходный экземпляр. Вы можете узнать больше в разделе о передаче права собственности. Если копирование типа требует больших затрат, гораздо эффективнее переместить его с помощью
__moveinit__(). Например, если тип содержит данные, размещенные в куче,__copyinit__()обычно требуется выделить новое хранилище для создания глубокой копии данных. Для типов без выделенных в куче полей конструктор перемещения не дает реальной пользы. Создание копий простых типов данных в стеке, таких как целые числа, числа с плавающей точкой и логические значения, обходится очень дешево. Тем не менее, если вы разрешаете копировать свой тип, то, как правило, нет причин запрещать перемещения.
Пользовательский конструктор перемещения¶
In practice, structs very rarely require a custom moveinit(). A type might require a custom moveinit() if it has a pointer to itself or one of its fields, for example, since the struct's location in memory changes when it's moved.
The moveinit() method performs a consuming move: it transfers ownership of a value from one variable to another when the original variable's lifetime ends (also called a "destructive move").
A critical feature of moveinit() is that it takes the incoming value as a deinit argument, meaning this method gets unique ownership of the value. Moreover, because this is a dunder method that Mojo calls only when performing a move (during ownership transfer), the existing argument is guaranteed to be a mutable reference to the original value, not a copy (unlike other methods that may declare an argument as var, but might receive the value as a copy if the method is called without the ^ transfer sigil). That is, Mojo calls this move constructor only when the original variable's lifetime actually ends at the point of transfer.
Типы, доступные только для перемещения, и неперемещаемые¶
Чтобы гарантировать, что ваш тип не может быть скопирован неявным образом, вы можете сделать его "доступным только для перемещения", сделав его Movable, но не Copyable. Тип, предназначенный только для перемещения, может быть передан другим переменным и функциям с любым соглашением об аргументах (read, mut и var). Единственная загвоздка в том, что вы должны использовать символ ^ для завершения срока службы типа, предназначенного только для перемещения, при присвоении его новой переменной или при передаче это как аргумент var. OwnedPointer - это пример типа, доступного только для перемещения: поскольку он предназначен для обеспечения четкого единоличного владения сохраненным значением, OwnedPointer можно перемещать, но не копировать.
В некоторых (редких) случаях вы можете не захотеть, чтобы тип был доступен для копирования или перемещения. Атомарный(Atomic) тип - это пример типа, который не является ни копируемым, ни перемещаемым.
Тривиальные типы¶
До сих пор мы говорили о значениях, которые хранятся в памяти, что означает, что у них есть идентификатор (адрес), который может передаваться между функциями ("по ссылке"). Это удобно для большинства типов, и это безопасное значение по умолчанию для больших объектов с дорогостоящими операциями копирования. Однако это неэффективно для таких крошечных объектов, как одно целое число или число с плавающей запятой. Мы называем эти типы "тривиальными", потому что они представляют собой просто "пакеты битов", которые следует копировать, перемещать и уничтожать без использования каких-либо пользовательских методов жизненного цикла.
Тривиальные типы - это наиболее распространенные типы, которые нас окружают, и с точки зрения языка, Mojo не нуждается в специальной поддержке для них, записанных в struct. Обычно эти значения настолько малы, что их следует передавать в регистрах процессора, а не косвенно через память.
Таким образом, Mojo предоставляет конструктор структур для объявления этих типов значений: @register_passable("trivial"). Этот декоратор сообщает Mojo, что тип должен быть доступен для копирования и перемещения, но для этого у него нет пользовательской логики (нет пользовательского конструктора копирования или конструктора перемещения). Он также сообщает Mojo передавать значение в регистры процессора, когда это возможно, что дает очевидные преимущества в производительности.
Вы увидите этот декоратор для таких типов, как Int, в стандартной библиотеке:
@register_passable("trivial")
struct Int:
...
Мы ожидаем, что этот декоратор будет широко использоваться в типах стандартных библиотек Mojo, но для общего кода прикладного уровня его можно игнорировать.
Тривиальные методы жизненного цикла¶
Использование декоратора для идентификации тривиальных типов будет постепенно прекращено в пользу более детализированного набора псевдонимов, которые представляют собой логические флаги, устанавливаемые компилятором:
-
Трейт
Copyableопределяет псевдоним__copyinit__is_trivial, который является подсказкой для оптимизации, гарантирующей, что значение может быть скопировано по битам из одного местоположения в другое без каких-либо побочных эффектов. -
Трейт
Movableопределяет псевдоним__moveinit__is_trivial, который является подсказкой для оптимизации, гарантирующей, что значение может быть перемещено по его битам из одного местоположения в другое без каких-либо побочных эффектов. -
Трейт
AnyTypeопределяет псевдоним__del__is_trivial, который указывает на то, что метод__del__()не работает.