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

Времена жизни, источники(origins) и ссылки

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

Компилятор Mojo использует специальное значение, называемое источник(origin), для отслеживания времени жизни переменных и достоверности ссылок.

В частности, источник(origin) отвечает на два вопроса:

  • Какой переменной "принадлежит" это значение?
  • Можно ли изменить значение, используя эту ссылку?

Для примера рассмотрим следующий код:

def print_str(s: String):
    print(s)

def main():
    name: String = "Joan"
    print_str(name)
Joan

Строка name = "Joan" объявляет переменную с идентификатором (name) и логическим пространством для хранения строкового значения. Когда вы передаете name в функцию print_str(), функция получает неизменяемую ссылку на значение. Таким образом, и name, и s ссылаются на одно и то же логическое пространство памяти и имеют связанные значения источника(origin), что позволяет компилятору Mojo анализировать их.

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

В большинстве случаев исходные данные обрабатываются компилятором автоматически. Однако в некоторых случаях вам потребуется взаимодействовать с исходными данными напрямую:

  • При работе со ссылками, в частности с аргументами ref и возвращаемыми значениями ref.

  • При работе с такими типами, как Pointer или Span, которые параметризуются в зависимости от источника данных, на которые они ссылаются.

В этом разделе также рассматриваются аргументы ref и возвращаемые значения ref, которые позволяют функциям принимать аргументы и предоставлять возвращаемые значения в качестве ссылок с параметрическим источником.

Работа с источниками

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

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

Типы источников

Mojo предоставляет структуру и набор псевдонимов типов(значений времени выполнения), которые вы можете использовать для указания исходных типов. Как следует из названий, значения ImmutOrigin и MutOrigin comptime представляют неизменяемые и изменяемые исходные данные соответственно:

struct ImmutRef[origin: ImmutOrigin]:
    pass

Или вы можете использовать структуру Origin для указания источника с параметрической изменчивостью:

struct ParametricRef[
    is_mutable: Bool,
    //,
    origin: Origin[is_mutable]
]:
    pass

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

Параметр is_mutable здесь является параметром только для вывода. Также часто выводится исходное значение. Например, следующий код создает указатель на существующее значение, но не требует указания источника — источник выводится из существующего значения.

from memory import Pointer

def use_pointer():
    a = 10
    ptr = Pointer(to=a)

Origin sets

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

Origin values

Большинство исходных значений создаются компилятором. У разработчика есть несколько способов указать исходные значения:

  • Статическое происхождение. Значение StaticConstantOrigin comptime представляет собой неизменяемые значения, которые сохраняются в течение всего времени работы программы. Значения строковых литералов имеют StaticConstantOrigin.
  • Производный источник. Волшебная функция origin_of() возвращает источник, связанный с переданным значением (или значениями). Предполагаемый источник. Вы можете использовать предполагаемые параметры для определения источника значения, передаваемого в функцию.
  • Внешние источники. Внешние источники, MutOrigin.external и ImmutOrigin.external представляют значения, которые не отслеживаются средством проверки времени жизни, например, динамически выделяемую память.
  • Источники с подстановочными знаками. Значения ImmutAnyOrigin и MutAnyOrigin comptime - это особые случаи, указывающие на ссылку, которая может обращаться к любому текущему значению.

Static origins

Вы можете использовать статический источник StaticConstantOrigin, если у вас есть значение, которое существует в течение всего времени работы программы.

Например, метод StringLiteral as_string_slice() возвращает StringSlice, указывающий на исходный строковый литерал. Строковые литералы статичны — они выделяются во время компиляции и никогда не уничтожаются, — поэтому фрагмент создается с неизменяемым статическим источником.

Derived origins

Используйте оператор origin_of(value), чтобы получить источник значения. Аргумент origin_of() может принимать произвольное выражение, которое выдает одно из следующих значений:

  • Исходное значение.

  • Значение, указанное в ячейке памяти.

Например:

origin_of(self)
origin_of(x.y)
origin_of(foo())

Оператор origin_of() анализируется статически во время компиляции; выражения, передаваемые в origin_of(), никогда не вычисляются. (Например, когда компилятор анализирует origin_of(foo()), он не запускает функцию foo().)

Следующая структура хранит строковое значение с помощью OwnedPointer: интеллектуального указателя, который содержит собственное значение. Метод as_ptr() возвращает указатель на сохраненную строку, используя тот же источник, что и исходный OwnedPointer.

from memory import OwnedPointer, Pointer

struct BoxedString:
    var o_ptr: OwnedPointer[String]

    fn __init__(out self, value: String):
        self.o_ptr = OwnedPointer(value)

    fn as_ptr(mut self) -> Pointer[String, origin_of(self.o_ptr)]:
        return Pointer(to=self.o_ptr[])

Обратите внимание, что метод as_ptr() принимает свой аргумент self как mut self. Если бы он использовал соглашение об использовании аргументов чтения по умолчанию, оно было бы неизменяемым, и производное значение origin (origin_of(self.o_ptr)) также было бы неизменяемым.

Вы также можете передать несколько выражений в origin_of(), чтобы выразить объединение двух или более источников:

origin_of(a, b)

Origin unions

Объединение двух или более источников создает новый источник, который ссылается на все исходные источники для продления срока службы (таким образом, объединение источников a и b продлевает оба срока службы).

Объединение источников является изменяемым тогда и только тогда, когда изменяемы все входящие в него источники. Пример приведен в разделе Возвращаемые значения с исходными данными объединения.

Inferred origins

Поскольку исходные данные являются параметрами, компилятор может вывести исходное значение из аргумента, переданного функции или методу, как описано в разделе Вывод параметров. Это позволяет функции возвращать значение, имеющее тот же источник, что и переданный ей аргумент.

Пример использования предполагаемого источника приведен в разделе, посвященном аргументам ref.

External origins

Внешние источники, MutOrigin.external и ImmutOrigin.external представляют значения, которые не являются псевдонимами ни одного существующего значения. То есть они указывают на память, которая не принадлежит какой-либо другой переменной и, следовательно, не отслеживается средством проверки времени жизни. Например, функция alloc() возвращает небезопасный указатель на новый динамически выделяемый блок памяти с источником MutOrigin.external. Источник указывает на то, что память не управляется системой владения Mojo. Когда вы используете небезопасный API, подобный этому, вы сами несете ответственность за управление временем жизни: например, структура, которая выделяет память, как правило, должна освобождать эту память в своем деструкторе.

Wildcard origins

Подстановочные знаки, ImmutAnyOrigin и MutAnyOrigin - это особые случаи, указывающие на ссылку, которая может обращаться к любому текущему значению. Ранее они широко использовались для небезопасных указателей. Использование указателя с подстановочным знаком origin в области видимости эффективно отключает быстрое уничтожение Mojo любых значений в этой области, пока указатель активен. Соответственно, использование подстановочных знаков origin не рекомендуется и должно использоваться в качестве последнего средства.

Working with references

Вы можете использовать ключевое слово ref с аргументами и возвращаемыми значениями, чтобы указать ссылку с параметрической изменяемостью. То есть они могут быть как изменяемыми, так и неизменяемыми.

Изнутри вызываемой функции аргумент ref выглядит как аргумент read или mut.

Возвращаемое значение ref выглядит как любое другое значение, возвращаемое вызывающей функцией, но это ссылка на существующее значение, а не копия.

ref аргументы

Соглашение об использовании аргумента ref позволяет указать параметрическую изменяемость аргумента: то есть вам не нужно заранее знать, будет ли передаваемый аргумент изменяемым или неизменяемым. Существует несколько причин, по которым вы можете захотеть использовать аргумент ref:

  • Вы хотите принять аргумент с параметрической изменчивостью.

  • Вы хотите привязать время жизни одного аргумента к времени жизни другого аргумента.

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

Синтаксис для аргумента ref следующий:

ref arg_name: arg_type

Или:

ref [origin_specifier(s)] arg_name: arg_type

В первой форме происхождение и изменяемость аргумента ref определяются из значения, переданного в поле. Вторая форма содержит предложение origin, состоящее из одного или нескольких спецификаторов origin, заключенных в квадратные скобки. Спецификатор origin может быть любым:

  • Исходное значение.

  • Произвольное выражение, которое рассматривается как сокращение от origin_of(выражение). Другими словами, следующие объявления эквивалентны:

    ref [origin_of(self)]
    ref [self]
    
  • Значение адресного пространства.

  • Символ подчеркивания (_), указывающий на то, что исходная точка не привязана. Это эквивалентно отсутствию указания источника.

    def add_ref(ref a: Int, b: Int) -> Int:
        return a+b
    

Вы также можете указать источник в явном виде. Это полезно, если вы хотите ограничить аргумент либо значением ImmutOrigin, либо значением MutOrigin, или если вы хотите привязать возвращаемое функцией значение к источнику аргумента.

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

В следующем примере функция to_byte_span() принимает List[Byte] и возвращает Span[Byte] с тем же началом, что и у списка:

from collections import List
from memory import Span

def to_byte_span[
    is_mutable: Bool,
    //,
    origin: Origin[is_mutable],
](ref [origin]list: List[Byte]) -> Span[Byte, origin]:
    return Span(list)

def main():
    list: List[Byte] = [77, 111, 106, 111]
    span = to_byte_span(list)

В этом примере параметр origin выводится из аргумента list и затем используется в качестве источника для возвращаемого значения Span.

Поскольку значение Span определяется как исходное значение аргумента list, компилятор Mojo может идентифицировать данные span как принадлежащие списку. Интервал будет иметь тот же срок службы, что и список, и этот интервал будет изменяемым, если список является изменяемым.

ref return values

Как и аргументы ref, возвращаемые значения ref позволяют функции возвращать изменяемую или неизменяемую ссылку на значение. Синтаксис возвращаемого значения ref следующий:

-> ref [origin_specifier(s)] arg_type

Обратите внимание, что для возвращаемого значения ref необходимо указать спецификатор origin. Значения, допустимые для спецификаторов origin, совпадают с теми, которые указаны для аргументов ref.

возвращаемые значения ref могут быть эффективным способом обработки обновляемых элементов в коллекции. Стандартный способ сделать это - реализовать методы __getitem__() и __setitem__(). Они вызываются для чтения из подписанного элемента в коллекции и записи в него:

value = list[a]
list[b] += 10

Используя аргумент ref, __getitem__() может возвращать изменяемую ссылку, которую можно изменять напрямую. У этого есть свои плюсы и минусы по сравнению с использованием метода __setitem__():

  • Изменяемая ссылка более эффективна — одно обновление не разбивается на два метода. Однако значение, на которое ссылается ссылка, должно находиться в памяти.

  • Пара __getitem__()/__setitem__() позволяет запускать произвольный код при извлечении и установке значений. Например, `__setitem__() может проверять или ограничивать входные значения.

Например, в следующем примере NameList имеет метод __getitem__(), который возвращает ссылку:

struct NameList:
    var names: List[String]

    def __init__(out self, *names: String):
        self.names = []
        for name in names:
            self.names.append(name)

    def __getitem__(ref self, index: Int) ->
        ref [self.names] String:
        if (index >=0 and index < len(self.names)):
            return self.names[index]
        else:
            raise Error("index out of bounds")

def main():
    list = NameList("Thor", "Athena", "Dana", "Vrinda")
    ref name = list[2]
    print(name)
    name += "?"
    print(list[2])
Dana
Dana?

Обратите внимание на использование синтаксиса ref name для создания ссылочной привязки.

Если вы присваиваете переменной возвращаемое значение ref, переменная получает копию элемента, на который ссылается ссылка. Используйте ссылочную привязку, если вам нужно сохранить ссылку для использования в будущем:

var name_copy = list[2]  # owned copy of list[2]
ref name_ref = list[2]  # reference to list[2]

Parametric mutability of return values

Другим преимуществом возвращаемых аргументов ref является возможность поддерживать параметрическую изменчивость. Например, вспомните сигнатуру метода __getitem__(), описанного выше:

def __getitem__(ref self, index: Int) ->
    ref [self] String:

Поскольку источник возвращаемого значения привязан к источнику self, возвращаемая ссылка будет изменяемой, если метод был вызван с использованием изменяемой ссылки. Метод по-прежнему работает, если у вас есть неизменяемая ссылка на список имен, но он возвращает неизменяемую ссылку:

fn pass_immutable_list(list: NameList) raises:
    print(list[2])
    # list[2] += "?" # Ошибка, этот список является неизменяемым

def main():
    list = NameList("Sophie", "Jack", "Diana")
    pass_immutable_list(list)
Diana

Без параметрической изменчивости вам нужно было бы написать две версии __getitem__(), одна из которых принимает неизменяемое self, а другая - изменяемое self.

Return values with union origins

Возвращаемое значение ref может включать в себя несколько значений в своем оригинальном спецификаторе, что приводит к объединению источников. Например, следующая функция pick_one() возвращает ссылку на одну из двух входных строк с источником, который является объединением обоих источников.

def pick_one(cond: Bool, ref a: String, ref b: String) -> ref [a, b] String:
    return a if cond else b

Поскольку компилятор не может статически определить, какая ветвь будет выбрана, эта функция должна использовать исходный код объединения [a, b]. Это гарантирует, что компилятор продлит время жизни обоих значений до тех пор, пока сохраняется возвращаемая ссылка.

Возвращаемая ссылка является изменяемой, если изменяемы как a, так и b.