Перейти к содержанию

Уничтожение значения

Как только значение/объект больше не используется, Mojo уничтожает его. Mojo не дожидается окончания блока кода или даже конца выражения, чтобы уничтожить неиспользуемое значение. Он уничтожает значения, используя политику уничтожения “как можно скорее” (ASAP), которая выполняется после каждого вложенного выражения. Даже в таком выражении, как a+b+c+d, Mojo уничтожает промежуточные значения, как только они больше не нужны.

Mojo использует статический анализ компилятора, чтобы найти точку, в которой значение использовалось в последний раз. Затем Mojo немедленно завершает срок действия значения и вызывает деструктор __del__() для выполнения любой необходимой очистки типа.

Например, обратите внимание, когда вызывается деструктор __del__() для каждого экземпляра Balloon:

@fieldwise_init
struct Balloon(Writable):
    var color: String

    fn write_to(self, mut writer: Some[Writer]):
        writer.write(String("a ", self.color, " balloon"))

    fn __del__(deinit self):
        print("Destroyed", String(self))


def main():
    var a = Balloon("red")
    var b = Balloon("blue")
    print(a)
    # a.__del__() вызывается здесь для "red" Balloon

    a = Balloon("green")
    # a.__del__() вызывается немедленно, потому что "green" Balloon никогда не используется

    print(b)
    # b.__del__() запускается здесь
a red balloon
Destroyed a red balloon
Destroyed a green balloon
a blue balloon
Destroyed a blue balloon

Обратите внимание, что каждая инициализация значения сопровождается вызовом деструктора, и a фактически уничтожается несколько раз — по одному разу за каждый раз, когда он получает новое значение.

Также обратите внимание, что эта реализация __del__() на самом деле ничего не делает. Для большинства структур не требуется настраиваемый деструктор, и Mojo автоматически добавляет деструктор без операций, если вы его не зададите.

Метод __del__() принимает свой аргумент, используя соглашение об удалении аргумента, которое указывает, что значение деинициализируется.

Поведение при уничтожении по умолчанию

Возможно, вам интересно, как Mojo может уничтожить тип без пользовательского деструктора или почему полезен безоперационный деструктор. Если тип представляет собой просто набор полей, как в примере с Balloon, Mojo нужно только уничтожить поля: Balloon динамически не выделяет память и не использует какие-либо долговременные ресурсы (например, файловые дескрипторы). При уничтожении значения Balloon не требуется никаких специальных действий.

Когда значение с расширенным значением уничтожается, строковое значение в его цветовом поле больше не используется и также немедленно уничтожается.

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

Поскольку String не требует никакой пользовательской логики деструктора, у него есть деструктор без операций: буквально, метод __del__(), который ничего не делает. Это может показаться бессмысленным, но это означает, что Mojo может вызвать деструктор для любого значения, когда закончится его время жизни. Это упрощает написание универсальных контейнеров и алгоритмов.

Преимущества скорейшего(ASAP) уничтожения

Подобно другим языкам, Mojo следует принципу, согласно которому объекты/значения получают ресурсы в конструкторе (__init__()) и освобождают ресурсы в деструкторе (__del__()). Однако быстрое уничтожение Mojo имеет некоторые преимущества перед уничтожением на основе области действия (например, шаблон RAII на C++, который ожидает окончания области действия кода для уничтожения значений):

  • Немедленное уничтожение значений при последнем использовании прекрасно сочетается с оптимизацией "переместить", которая преобразует пару "копировать+удалить" в операцию "переместить".

  • Уничтожение значений в конце области видимости в C++ проблематично для некоторых распространенных шаблонов, таких как хвостовая рекурсия, поскольку вызов деструктора происходит после хвостового вызова. Это может привести к существенным проблемам с производительностью и памятью для определенных шаблонов функционального программирования, что не является проблемой в Mojo, поскольку вызов деструктора всегда происходит перед конечным вызовом.

Политика уничтожения Mojo больше похожа на то, как работают Rust и Swift, поскольку они оба обеспечивают надежное отслеживание прав собственности и безопасность памяти. Одно из отличий заключается в том, что Rust и Swift требуют использования динамического "отбрасывающего флага" — они поддерживают скрытые теневые переменные, чтобы отслеживать состояние ваших значений и обеспечивать безопасность. Они часто оптимизируются, но подход Mojo полностью устраняет эти накладные расходы, ускоряя генерируемый код и избегая двусмысленности.

Деструктор

Mojo вызывает деструктор значения (метод __del__()), когда время жизни значения заканчивается (обычно это точка, в которой значение использовалось в последний раз). Как мы упоминали ранее, Mojo предоставляет деструктор без операций по умолчанию для всех типов, поэтому в большинстве случаев вам не нужно определять метод __del__().

Вам следует определить метод __del__() для выполнения любой очистки, требуемой для этого типа. Обычно это включает в себя освобождение памяти для любых полей, для которых вы динамически выделяли память (например, с помощью UnsafePointer), и закрытие любых долгоживущих ресурсов, таких как дескрипторы файлов.

Однако любая структура, представляющая собой простую коллекцию других типов, не нуждается в реализации деструктора.

Например, рассмотрим эту простую структуру:

@fieldwise_init
struct Balloons:
    var color: String
    var count: Int

Для этого нет необходимости определять деструктор __del__(), потому что это простая коллекция других типов (String и Int), и она не выделяет память динамически.

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

struct HeapArray(Writable):
    var data: UnsafePointer[Int, MutExternalOrigin]
    var size: Int

    fn __init__(out self, *values: Int):
        self.size = len(values)
        self.data = alloc[Int](self.size)
        for i in range(self.size):
            (self.data + i).init_pointee_copy(values[i])

    fn write_to(self, mut writer: Some[Writer]):
        writer.write("[")
        for i in range(self.size):
            writer.write(self.data[i])
            if i < self.size - 1:
                writer.write(", ")
        writer.write("]")

    fn __del__(deinit self):
        print("Destroying", self.size, "elements")
        for i in range(self.size):
            (self.data + i).destroy_pointee()
        self.data.free()

def main():
    var a = HeapArray(10, 1, 3, 9)
    print(a)
[10, 1, 3, 9]
Destroying 4 elements

Деструктор принимает свой аргумент self, используя соглашение deinit об удалении аргумента, которое предоставляет исключительное право собственности на значение и помечает его как уничтоженное в конце выполнения функции.

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

Таким образом, в приведенном выше примере с HeapArray вызов free() для указателя освобождает память, но не вызывает деструкторы для сохраненных значений. Чтобы вызвать деструкторы, используйте метод destroy_pointee(), предоставляемый типом UnsafePointer.

Не следует вызывать деструктор явно. Если вам нужно убедиться, что деструктор вызывается в определенный момент, используйте шаблон отмены, описанный в разделе явное продление срока службы.

Важно отметить, что метод __del__() является "дополнительным" событием очистки, и ваша реализация не отменяет никаких действий по уничтожению по умолчанию. Например, Mojo по-прежнему уничтожает все поля в Balloons, даже если вы добавляете метод __del__(), который ничего не делает:

@fieldwise_init
struct Balloons:
    var color: String
    var count: Int

    fn __del__(deinit self):
        # Моджо уничтожает все поля, когда они используются в последний раз
        pass

Однако значение self внутри деструктора __del__() остается целым (поэтому все поля по-прежнему доступны для использования) до тех пор, пока деструктор не вернется, о чем мы подробнее поговорим в следующем разделе.

Деструкторы не могут генерировать ошибки В настоящее время деструктору Mojo запрещено генерировать ошибку. Это означает, что деструктор должен быть определен как функция fn без ключевого слова raises. Mojo не позволит вам определить деструктор с помощью fn raises или def.

Срок жизни полей

В дополнение к отслеживанию времени жизни всех объектов в программе, Mojo также отслеживает каждое поле структуры независимо. То есть Mojo отслеживает, полностью или частично инициализирован/уничтожен "весь объект", и уничтожает каждое поле независимо с помощью своей политики уничтожения ASAP.

Например, рассмотрим этот код, который изменяет значение поля:

@fieldwise_init
struct Balloons:
    var color: String
    var count: Int


def main():
    var balloons = Balloons("red", 5)
    print(balloons.color)
    # balloons.color.__del__() runs here, because this instance is
    # no longer used; it's replaced below

    balloons.color = "blue"  # Overwrite balloons.color
    print(balloons.color)
    # balloons.__del__() runs here

Поле balloons.color уничтожается после первого print(), потому что Mojo знает, что оно будет перезаписано ниже. Вы также можете увидеть такое поведение при использовании символа переноса:

fn consume(var arg: String):
    pass


fn use(arg: Balloons):
    print(arg.count, arg.color, "balloons.")


fn consume_and_use():
    var balloons = Balloons("blue", 8)
    consume(balloons.color^)
    # String.__moveinit__() runs here, which invalidates balloons.color
    # Now balloons is only partially initialized

    # use(balloons)  # This fails because balloons.color is uninitialized

    balloons.color = String("orange")  # All together now
    use(balloons)  # This is ok
    # balloons.__del__() runs here (and only if the object is whole)

Обратите внимание, что код передает право собственности на поле name функции consume(). В течение некоторого времени после этого поле name не инициализируется. Затем name повторно инициализируется перед передачей в функцию use(). Если вы попытаетесь вызвать use() до повторной инициализации name, Mojo отклонит код с ошибкой неинициализированного поля.

Кроме того, если вы повторно не инициализируете имя к концу срока жизни pet, компилятор будет жаловаться, потому что он не может уничтожить частично инициализированный объект.

Политика Mojo здесь мощная и намеренно прямолинейная: поля могут быть временно перенесены, но "весь объект" должен быть создан с помощью инициализатора агрегатного типа и уничтожен с помощью агрегатного деструктора. Это означает, что невозможно создать объект, инициализировав только его поля, и точно так же невозможно уничтожить объект, уничтожив только его поля.

Время жизни поля при уничтожении и перемещении

Как конструктор перемещения, так и деструктор-потребитель принимают свой операнд с помощью соглашения об аргументе deinit. Это предоставляет исключительное право собственности на значение и помечает его как уничтоженное в конце выполнения функции. В теле функции к полям по-прежнему применяется политика Mojo ASAP: каждое поле уничтожается сразу же после его последнего использования.

Просто напомню, что конструктор перемещения и деструктор выглядят следующим образом:

struct TwoStrings:
    fn __moveinit__(out self, deinit existing: Self):
        # Инициализирует новое `self`, используя содержимое `существующего`
    fn __del__(deinit self):
        # Уничтожение всех ресурсов `self`

Как и соглашение об аргументе var, соглашение deinit предоставляет аргументу исключительное право собственности на значение. Но в отличие от аргумента var, Mojo не вставляет вызов деструктора для аргумента в конце функции — потому что соглашение deinit уже помечает его как значение, которое находится в процессе уничтожения.

Например, следующий код показывает, как поля уничтожаются внутри деструктора.

fn consume(var str: String):
    print("Consumed", str)

@fieldwise_init
struct TwoStrings(Copyable):
    var str1: String
    var str2: String

    fn __del__(deinit self):
        # self value is whole at the beginning of the function
        self.dump()
        # After dump(): str2 is never used again, so str2.__del__() runs now

        consume(self.str1^)
        # self.str1 has been transferred so str1 becomes uninitialized, and
        # no destructor is called for str1.
        # self.__del__() is not called (avoiding an infinite loop).

    fn dump(mut self):
        print("str1:", self.str1)
        print("str2:", self.str2)


def main():
    var two_strings = TwoStrings("foo", "bar")

Явное продление срока жизни

В большинстве случаев быстрое уничтожение Mojo ”просто срабатывает". В очень редких случаях может потребоваться явно отметить последнее использование значения, чтобы контролировать запуск его деструктора. Думайте об этом как о явном маркере последнего использования для средства проверки срока службы, а не как о шаблоне общего назначения.

Вы могли бы сделать это:

  • При написании тестов, которые намеренно создают неиспользуемые значения (чтобы избежать предупреждений или устранения ошибок в коде).

  • При написании небезопасного/сложного кода (например, кода, который манипулирует источником значения).

  • Когда вам нужно детерминированное время действия деструктора относительно определенных побочных эффектов (таких как ведение журнала или профилирование).

Отметьте последнее использование, присвоив значение шаблону _ в том месте, где его можно уничтожить. Это устанавливает последнее использование в этой строке, поэтому деструктор запускается сразу после инструкции:

# Без явного расширения: s в последний раз используется в вызове print(), поэтому сразу 
# после этого он удаляется
var s = "abc"
print(s)  # s.__del__() запускается после этой строки

# С явным расширением: нажмите "последнее использование" в строке "отбросить".
var t = "xyz"
print(t)

# ... некоторое время спустя
_ = t  # t.__del__() запускается после этой строки

В предыдущих версиях Mojo при отбрасывании типа, доступного только для перемещения, требовался символ переноса (^). Это больше не требуется, поскольку компилятор фактически не перемещает отброшенное значение. Для получения дополнительной информации о символе переноса смотрите раздел о передаче прав собственности.