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

Введение в указатели

Указатель - это косвенная ссылка на одно или несколько значений, хранящихся в памяти. Указатель - это значение, которое содержит адрес в памяти и предоставляет API-интерфейсы для хранения и извлечения значений из этой памяти. Значение, на которое указывает указатель, также называется pointee.

Стандартная библиотека Mojo включает в себя несколько типов указателей, которые предоставляют различные наборы функций. Все эти типы указателей являются универсальными — они могут указывать на любой тип значения, а тип значения указывается в качестве параметра. Например, следующий код создает OwnedPointer, который указывает на значение Int:

from memory import OwnedPointer

var ptr: OwnedPointer[Int]
ptr = OwnedPointer(100)

Переменная ptr имеет значение типа OwnedPointer[Int]. Указатель указывает на значение типа Int, как показано на рисунке 1.

изображение

Рис. 1. Указатель и наблюдатель

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

# Обновить инициализированное значение
ptr[] += 10
# Доступ к инициализированному значению
print(ptr[])

Терминология указателя

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

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

  • Nullable Указатели(с нулевым значением): могут указывать на недопустимую ячейку памяти (обычно 0 или “нулевой указатель”). Безопасные указатели не могут быть обнулены.

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

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

  • Неинициализированная память: относится к ячейкам памяти, которые не были инициализированы значением и, следовательно, могут содержать случайные данные. Вновь выделенная память неинициализирована. Безопасные типы указателей не позволяют пользователям получать доступ к неинициализированной памяти. Небезопасные указатели могут выделять блок неинициализированных ячеек памяти, а затем инициализировать их по очереди. Возможность доступа к неинициализированной памяти небезопасна по определению.

  • Копируемые типы: могут быть скопированы неявно (например, путем присвоения значения переменной). Также называются неявно копируемыми типами.

    copied_ptr = ptr
    

    Явно копируемые типы требуют, чтобы пользователь запрашивал копию, используя конструктор с аргументом ключевого слова:

    copied_owned_ptr = OwnedPointer(other=owned_ptr)
    

Типы указателей

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

  • Pointer - это безопасный указатель, который указывает на одно значение, которому он не принадлежит.

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

  • ArcPointer - это умный указатель с подсчетом ссылок, который указывает на принадлежащее значение, которое может использоваться совместно с другими экземплярами ArcPointer.

  • UnsafePointer указывает на одну или несколько последовательных ячеек памяти и может ссылаться на неинициализированную память.

В таблице 1 представлены различные типы указателей:

Pointer OwnedPointer ArcPointer UnsafePointer
Safe Да Да Да Нет
Allocates memory Нет Неявно(1) Неявно(1) Явно
Owns pointee(s) Нет Да Да Нет(2)
Copyable Да Нет(3) Да Да
Nullable Нет Нет Нет Да
Может указывать на неинициализированную память Нет Нет Нет Да
Может указывать на несколько значений (доступ по типу массива) Нет Нет Нет Да
Table 1. Типы указателей

(1) OwnedPointer и ArcPointer неявно выделяют память, когда вы инициализируете указатель значением.

(2) UnsafePointer предоставляет небезопасные методы для инициализации и уничтожения экземпляров сохраненного типа. Пользователь несет ответственность за управление жизненным циклом сохраненных значений.

(3) OwnedPointer явно доступен для копирования, но при явном копировании OwnedPointer сохраненное значение копируется в новый OwnedPointer.

В следующих разделах приведены более подробные сведения о каждом типе указателя.

Pointer

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

  • Сохранение ссылки на связанный тип. Например, объект итератора списка может содержать указатель на исходный список.

  • Передача ячейки памяти для отдельного значения внешнему коду через external_call().

  • Где вам нужен API для возврата долговременной ссылки на значение. (В настоящее время итераторы для стандартных типов библиотечных коллекций, таких как List, возвращают указатели).

Вы можете создать указатель на существующее значение, вызвав конструктор с аргументом ключевого слова to:

from memory import Pointer

ptr = Pointer(to=some_value)

You can also create a Pointer by copying an existing Pointer.

A Pointer carries an origin for the stored value, so Mojo can track the lifetime of the referenced value.

OwnedPointer

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

from memory import OwnedPointer

o_ptr = OwnedPointer(some_big_struct)

Принадлежащий указатель может содержать практически любой тип элемента, но при создании OwnedPointer сохраненный элемент должен быть либо перемещаемым, либо копируемым.

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

Примечание: В настоящее время вы не можете создать Optional[OwnedPointer[T]], поскольку тип Optional работает только с типами, которые являются как перемещаемыми, так и копируемыми. Это ограничивает некоторые варианты использования, которые в противном случае были бы естественными для OwnedPointer, включая самореферентные структуры данных, такие как связанные списки и деревья. (Пока этот вариант использования не будет поддерживаться для OwnedPointer, можно использовать ArcPointer там, где вам нужен интеллектуальный указатель, который может быть необязательным.)

ArcPointer

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

from memory import ArcPointer

attributesDict: Dict[String, String] = {}
attributes = ArcPointer(attributesDict)

В отличие от OwnedPointer, ArcPointer можно свободно копировать. Все экземпляры данного ArcPointer имеют общее количество ссылок, которое увеличивается при копировании ArcPointer и уменьшается при уничтожении экземпляра. Когда количество ссылок достигает нуля, сохраненное значение уничтожается, а выделенная память освобождается.

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

from memory import ArcPointer

struct SharedDict:
    var attributes: ArcPointer[Dict[String, String]]

    fn __init__(out self):
        attributesDict: Dict[String, String] = {}
        self.attributes = ArcPointer(attributesDict)

    fn __copyinit__(out self, other: Self):
        self.attributes = other.attributes

    def __setitem__(mut self, key: String, value: String):
        self.attributes[][key] = value

    def __getitem__(self, key: String) -> String:
        return self.attributes[].get(key, default="")

def main():
    thing1 = SharedDict()
    thing2 = thing1
    thing1["Flip"] = "Flop"
    print(thing2["Flip"])

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

UnsafePointer

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

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

  • Создание высокопроизводительной структуры, подобной массиву, такой как List или Tensor. Один UnsafePointer может обращаться ко многим значениям и дает вам большой контроль над тем, как вы распределяете, используете и освобождаете память. Возможность доступа к неинициализированной памяти означает, что вы можете предварительно выделить блок памяти и постепенно инициализировать значения по мере их добавления в коллекцию.

  • Взаимодействие с внешними библиотеками, включая C++ и Python. Вы можете использовать UnsafePointer для передачи буфера, заполненного данными, во внешнюю библиотеку или из нее.

Дополнительные сведения см. в разделе Unsafe pointers.