Владение значением¶
Проблема, с которой вы можете столкнуться при использовании некоторых языков программирования, заключается в том, что вам приходится вручную выделять и освобождать память. Когда нескольким частям программы требуется доступ к одной и той же памяти, становится трудно отследить, кому "принадлежит" значение, и определить, когда наступит подходящее время для его освобождения. Если вы допустите ошибку, это может привести к ошибке "использование после освобождения", ошибке "двойное освобождение" или ошибке "утечка памяти", любая из которых может привести к катастрофическим последствиям.
Mojo помогает избежать этих ошибок, гарантируя, что каждому значению одновременно принадлежит только одна переменная, и при этом позволяет обмениваться ссылками с другими функциями. Когда срок службы владельца заканчивается, Mojo уничтожает значение. Программисты по-прежнему несут ответственность за то, чтобы любой тип, который выделяет ресурсы (включая память), также освобождал эти ресурсы в своем деструкторе. Система владения Mojo гарантирует оперативный вызов деструкторов.
На этой странице мы объясним правила, управляющие этой моделью владения, и как указать различные соглашения об аргументах, которые определяют, как значения передаются в функции.
Краткое описание владения значением¶
Основные правила, по которым работает модель владения Mojo, заключаются в следующем:
- У каждого значения одновременно есть только один владелец.
- Когда срок службы владельца заканчивается, Mojo уничтожает значение.
- Если существуют ссылки на значение, Mojo продлевает срок службы владельца.
Переменные и ссылки¶
Переменная является владельцем своего значения. Структура является владельцем своих полей.
Ссылка позволяет получить доступ к значению, принадлежащему другой переменной. Ссылка может иметь как изменяемый, так и неизменяемый доступ к этому значению.
Ссылки Mojo создаются при вызове функции: аргументы функции могут передаваться как изменяемые или неизменяемые ссылки. Функция также может возвращать ссылку вместо значения. Чтобы зафиксировать возвращаемую ссылку, вы можете использовать привязку ссылок:
ref value_ref = list[0]
Соглашения об аргументах¶
Во всех языках программирования качество и производительность кода сильно зависят от того, как функции обрабатывают значения аргументов. То есть, является ли значение, полученное функцией, уникальным или ссылочным, и является ли оно изменяемым или неизменяемым, имеет ряд последствий, которые определяют удобочитаемость, производительность и безопасность языка.
В Mojo мы хотим предоставить полноценную семантику по умолчанию, которая обеспечивает согласованное и предсказуемое поведение. Но, как язык системного программирования, мы также должны предоставлять полный контроль над оптимизацией памяти, что обычно требует ссылочной семантики. Хитрость заключается в том, чтобы ввести ссылочную семантику таким образом, чтобы гарантировать сохранность всего кода в памяти, отслеживая время жизни каждого значения и уничтожая каждое из них в нужное время (и только один раз). Все это стало возможным в Mojo благодаря использованию соглашений об аргументах, которые гарантируют, что у каждого значения одновременно есть только один владелец.
Соглашение об аргументах определяет, является ли аргумент изменяемым или неизменяемым, и принадлежит ли значение функции. Каждое соглашение определяется ключевым словом в начале объявления аргумента:
-
read: Функция получает неизменяемую ссылку. Это означает, что функция может считывать исходное значение (оно не является копией), но не может изменять его. -
mut: Функция получает изменяемую ссылку. Это означает, что функция может считывать и изменять исходное значение (это не копия). -
var: Функция становится владельцем значения. Это означает, что функция обладает исключительным правом собственности на аргумент. Вызывающий объект может выбрать передачу права собственности на существующее значение этой функции, но это происходит не всегда. Вызываемый объект может получить вновь созданное значение или копию существующего значения. -
ref: Функция получает ссылку с параметрической изменчивостью, то есть ссылка может быть как изменяемой, так и неизменяемой. Вы можете рассматривать аргументыrefкак обобщение соглашений о чтении и преобразовании. Аргументыref- это сложная тема, и более подробно они описаны в разделах "Время жизни", "Происхождение" и "Ссылки". -
out: Специальное соглашение, используемое для аргументаselfв конструкторах и для именованных результатов. Аргументoutне инициализируется в начале работы функции и должен быть инициализирован до возврата функции. Хотя аргументыoutотображаются в списке аргументов, они никогда не передаются вызывающей стороной. -
deinit: Специальное соглашение, используемое в методах жизненного цикла destructor и consuming-move. Аргументdeinitинициализируется в начале функции и неинициализируется при возврате функции.
Например, у этой функции есть один аргумент, который является изменяемой ссылкой, и один - неизменяемый:
fn add(mut x: Int, read y: Int):
x += y
fn main():
var a = 1
var b = 2
add(a, b)
print(a)
3
Вероятно, вы уже видели некоторые аргументы функций, в которых не объявляется соглашение. По умолчанию все аргументы считываются. В следующих разделах мы более подробно объясним каждое из этих соглашений об аргументах.
Неизменяемые аргументы (read)¶
Для всех аргументов по умолчанию используется соглашение о чтении. Вызываемый объект получает неизменяемую ссылку на значение аргумента.
Например:
def print_list(list: List[Int]):
print(list.__str__())
def main():
var values = [1, 2, 3, 4]
print_list(values)
[1, 2, 3, 4]
Здесь функция print_list() может считывать данные из аргумента list, но не изменять его. Cписок - это ссылка на значения в функции main(), а не копия.
В общем, передача неизменяемой ссылки гораздо эффективнее при обработке больших или дорогостоящих для копирования значений, поскольку конструктор копирования и деструктор не вызываются для аргумента чтения.
Сравнение с C++ и Rust¶
Соглашение об использовании аргумента read в Mojo в некотором смысле аналогично передаче аргумента с помощью const& в C++, что также позволяет избежать копирования значения и отключает возможность изменения в вызываемом объекте. Однако соглашение о чтении отличается от const& в C++ двумя важными способами:
-
Компилятор Mojo реализует проверку срока службы, которая гарантирует, что значения не будут уничтожены при наличии неопубликованных ссылок на эти значения.
-
Небольшие значения, такие как
Int,FloatиSIMD, всегда передаются в машинных регистрах — вместо того, чтобы использовать дополнительное косвенное обращение или оптимизироваться на каждом узле вызова — потому что эти типы объявляются с помощью декоратора@register_passable. Это значительное повышение производительности по сравнению с такими языками, как C++ и Rust.
Основное различие между Rust и Mojo заключается в том, что Mojo не требует наличия сигила на стороне вызывающей стороны для передачи по неизменяемой ссылке. Кроме того, Mojo более эффективен при передаче небольших значений, а Rust по умолчанию использует перемещение значений, а не передачу их путем заимствования. Эти политические и синтаксические решения позволяют Mojo создать более простую в использовании модель программирования.
Изменяемые аргументы (mut)¶
Если вы хотите, чтобы ваша функция получала изменяемую ссылку, добавьте ключевое слово mut перед именем аргумента. Вы можете представить себе mut следующим образом: это означает, что любые изменения значения внутри функции видны за пределами функции.
Например, эта функция mutate() обновляет исходное значение списка:
def print_list(list: List[Int]):
print(list.__str__())
def mutate(mut l: List[Int]):
l.append(5)
def main():
var values = [1, 2, 3, 4]
mutate(values)
print_list(values)
[1, 2, 3, 4, 5]
Это ведет себя как оптимизированная замена для этого:
def print_list(list: List[Int]):
print(list.__str__())
def mutate_copy(l: List[Int]) -> List[Int]:
# def создает неявную копию списка, потому что он был изменен
l.append(5)
return l
def main():
var values = [1, 2, 3, 4]
values = mutate_copy(values)
print_list(values)
[1, 2, 3, 4, 5]
Хотя код, использующий mut, не намного короче, он экономит больше памяти, поскольку не создает копию значения.
Однако помните, что значения, передаваемые как mut, уже должны быть изменяемыми. Например, если вы попытаетесь взять считанное значение и передать его другой функции как mut, вы получите ошибку компилятора, потому что Mojo не может сформировать изменяемую ссылку из неизменяемой ссылки.
Вы не можете определить значения по умолчанию для аргументов
mut.
Исключительность аргумента¶
Mojo обеспечивает исключительность аргументов для изменяемых ссылок. Это означает, что если функция получает изменяемую ссылку на значение (например, аргумент mut), она не может получать никаких других ссылок на то же значение — изменяемых или неизменяемых. Таким образом, изменяемая ссылка не может содержать никаких других ссылок, которые бы ее псевдонимировали.
Например, рассмотрим следующий пример кода:
fn append_twice(mut s: String, other: String):
# Mojo знает, что "s" и "other" не могут быть одной и той же строкой.
s += other
s += other
fn invalid_access():
var my_string = "o" # Создайте строковое значение во время выполнения
# ошибка: передача mut `my_string` недопустима, так как она также передана для чтения.
append_twice(my_string, my_string)
print(my_string)
Этот код сбивает с толку, потому что пользователь может ожидать, что результат будет ooo, но поскольку первое добавление изменяет как s, так и other, фактический результат будет oooo. Обеспечение исключительности изменяемых ссылок не только предотвращает ошибки при кодировании, но и позволяет компилятору Mojo оптимизировать код в некоторых случаях.
Один из способов избежать этой проблемы, когда вам действительно нужна как изменяемая, так и неизменяемая ссылка (или нужно передать одно и то же значение двум аргументам), - это создать копию:
fn valid_access():
var my_string = "o" # Создайте строковое значение во время выполнения
var other_string = my_string # Создайте копию строкового значения
append_twice(my_string, other_string)
print(my_string)
Обратите внимание, что исключительность аргументов не применяется для тривиальных типов, передаваемых через регистр (таких как Int и Bool), поскольку они всегда передаются с помощью copy. При передаче одного и того же значения в два аргумента Int вызываемый объект получит две копии значения.
Аргументы передачи (var и ^)¶
И, наконец, если вы хотите, чтобы ваша функция получала право собственности на значение, добавьте ключевое слово var перед именем аргумента.
Это соглашение часто сочетается с использованием символа ^ "transfer" в переменной, которая передается в функцию, что завершает срок службы этой переменной.
Технически, ключевое слово var не гарантирует, что полученное значение является исходным — оно гарантирует только то, что функция получит уникальное право собственности на значение. Это происходит одним из трех способов:
-
Вызывающий объект передает аргумент с символом
^, который завершает время существования этой переменной (переменная становится неинициализированной), и право собственности передается функции. -
Вызывающий объект не использует символ
^, и в этом случае Mojo копирует значение. Если тип не поддается копированию, это ошибка времени компиляции. -
Вызывающий объект передает только что созданное "принадлежащее" значение, например значение, возвращаемое из функции. В этом случае значение не принадлежит ни одной переменной, и оно может быть передано непосредственно вызываемому объекту. Например:
def take(var s: String): pass def main(): take("A brand-new String!")
Следующий код работает путем создания копии строки, поскольку функция take_text() использует соглашение var, а вызывающий объект не включает символ передачи:
fn take_text(var text: String):
text += "!"
print(text)
fn main():
var message = "Hello" # Создайте строковое значение во время выполнения
take_text(message)
print(message)
Hello!
Hello
Однако, если вы добавите символ ^ при вызове функции take_text(), компилятор пожалуется на print(message), потому что в этот момент переменная message больше не инициализируется. То есть эта версия не компилируется:
fn main():
var message = "Hello" # Создайте строковое значение во время выполнения
take_text(message^)
print(message) # ошибка: использование неинициализированного значения 'message'
Это важная функция проверки срока службы Mojo, поскольку она гарантирует, что никакие две переменные не могут иметь одинаковое значение. Чтобы устранить ошибку, вы не должны использовать переменную message после завершения ее срока службы с помощью символа ^. Итак, вот исправленный код:
fn take_text(var text: String):
text += "!"
print(text)
fn main():
var message = "Hello" # Создайте строковое значение во время выполнения
take_text(message^)
Hello!
Независимо от того, как функция получает значение, когда она объявляет аргумент как var, можно быть уверенным, что у нее есть уникальный изменяемый доступ к этому значению. Поскольку значение является собственностью, оно уничтожается при завершении работы функции, если только функция не передаст значение в другое место.
Например, в следующем примере функция add_to_list() берет строку и добавляет ее к списку. Право собственности на строку передается списку, поэтому она не уничтожается при завершении работы функции. С другой стороны, функция consume_string() не передает свое значение var, поэтому значение уничтожается в конце выполнения функции.
def add_to_list(var name: String, mut list: List[String]):
list.append(name^)
# имя неинициализировано, уничтожать нечего
def consume_string(var s: String):
print(s)
# s здесь уничтожен
Детали осуществления передачи¶
В Mojo не следует путать "передачу права собственности" с "операцией перемещения" — это не совсем одно и то же.
Существует несколько способов, которыми Mojo может передать право собственности на ценность:
-
Если тип реализует конструктор перемещения,
__moveinit__(), Mojo может вызвать этот метод, если значение этого типа передается в функцию в качестве аргументаvar, и время жизни исходной переменной заканчивается в той же точке (с использованием символа^или без него). -
Если тип реализует конструктор копирования
__copyinit__(), а не__moveinit__(), Mojo может скопировать значение и уничтожить старое значение. -
В некоторых случаях Mojo может полностью оптимизировать операцию перемещения, оставляя значение в той же ячейке памяти, но обновляя его принадлежность. В этих случаях значение может быть передано без вызова конструкторов
__copyinit__()или__moveinit__().
Чтобы соглашение var работало без символа передачи, тип значения должен быть доступен для копирования (через __copyinit__()).