Сравнение с Rust и C++
Если вы писали на Rust, половина этого раздела показалась вам знакомой. Если на C++ — знакомой показалась другая половина. Эта глава расставляет точки: что совпадает, что нет и почему.
Что совпадает с Rust
Заголовок раздела «Что совпадает с Rust»Единственный владелец. У значения один хозяин, передача владения — явная операция.
Использовать после передачи нельзя. Обе системы ловят это на этапе компиляции, просто говорят по-разному:
| Язык | Сообщение |
|---|---|
| Rust | error[E0382]: borrow of moved value: `a` |
| Mojo | error: use of uninitialized value 'a' |
Ссылка не переживёт источник. Rust называет это временем жизни ('a),
Mojo — происхождением (origin). Идея одна: компилятор знает, откуда взялась
ссылка, и не даёт ей пережить владельца.
Инвалидация ссылок ловится. Классический пример — взять ссылку на элемент и добавить элемент в контейнер:
| Язык | Что будет |
|---|---|
| Rust | error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable |
| Mojo | error: use of invalidated interior reference 'xs["element"]' |
| C++ | компилируется, печатает мусор |
Последняя строка — не преувеличение. Мы взяли указатель на первый элемент
std::vector, добавили сто элементов и напечатали значение по указателю:
получили 1458768981 — обрывок чужого адреса из освобождённой памяти.
При каждом запуске число другое, и компилятор ни разу не сказал ни слова.
Чем Mojo проще Rust
Заголовок раздела «Чем Mojo проще Rust»Заимствования не нужно считать
Заголовок раздела «Заимствования не нужно считать»Главное правило Rust — одна изменяемая ссылка или сколько угодно неизменяемых. Правило спасает от гонок данных, но за него платят: даже безобидный код приходится переписывать.
let mut v = vec![1, 2, 3];let first = &mut v[0];let second = &mut v[1]; // error[E0499]*first = 10;*second = 20;Rust запрещает это, хотя элементы разные и конфликта нет: cannot borrow `v` as mutable more than once at a time. Компилятор сам подсказывает
обходной путь — split_at_mut.
Обратите внимание на две последние строки: без них пример собрался бы. Rust давно не считает заимствование живым до конца блока — оно заканчивается на последнем использовании. Ошибка появляется именно тогда, когда обеими ссылками действительно пользуются.
В Mojo то же самое просто работает:
def main(): var scores: List[Int] = [1, 2, 3] var low = Pointer(to=scores[0]) var high = Pointer(to=scores[1]) low[] = 10 high[] = 20 print(scores[0], scores[1], scores[2])10 20 3
Но исключительность всё-таки есть — на границе вызова
Заголовок раздела «Но исключительность всё-таки есть — на границе вызова»Это не значит, что Mojo разрешает всё. Проверка есть, только она уже и точнее: два изменяемых аргумента одного вызова не могут указывать на одно значение.
error: aliasing values passed mutably to 'x' argument and passed mutably to 'y' argument in 'bump' call
Вы передали одну и ту же переменную в два mut-аргумента:
bump(value, value). Внутри функции получились бы две изменяемые
ссылки на одно место — и результат зависел бы от порядка операций.
Передайте разные значения либо перестройте функцию так, чтобы изменяемый аргумент был один.
Разница в подходе: Rust требует исключительности от каждого заимствования, Mojo проверяет её только на границе вызова. Проверок меньше — и ошибок ловится меньше тоже.
Время жизни пишется проще
Заголовок раздела «Время жизни пишется проще»Rust требует объявлять параметры времени жизни и продевать их через сигнатуры. В Mojo вы пишете место, откуда берётся ссылка:
| Задача | Rust | Mojo |
|---|---|---|
| ссылка на аргумент | fn f<'a>(x: &'a str) -> &'a str | def f(ref x: String) -> ref [x] String |
| ссылка на поле | fn get(&self) -> &i32 | def get(ref self) -> ref [self.value] Int |
| ссылка на элемент | fn first(&self) -> &i32 | def first(ref self) -> ref [self.items[0]] Int |
Отдельных имён вроде 'a придумывать не нужно: их роль играет само
выражение доступа.
Структуру, которая хранит ссылку, параметризуют происхождением — это прямой
аналог struct Holder<'a>:
@fieldwise_initstruct View[o: Origin]: var target: Pointer[Int, Self.o]
def show(self): print(self.target[])
def main(): var value = 42 var view = View(Pointer(to=value)) view.show()42
Обратите внимание на Self.o: внутри тела структуры к её собственным
параметрам обращаются через Self — то же правило, что и в главе
Структуры.
Чем Mojo отличается от обоих
Заголовок раздела «Чем Mojo отличается от обоих»Значение умирает раньше
Заголовок раздела «Значение умирает раньше»Здесь Mojo расходится и с Rust, и с C++. В обоих значение живёт до конца области видимости; в Mojo — до последнего использования.
Один и тот же сценарий на трёх языках:
Rust C++ Mojo────────────────────────────────────────────используем используем используемконец main конец main уничтожен ←уничтожен ← уничтожен ← конец mainМы собрали и запустили все три программы: Rust и C++ печатают «уничтожен» последней строкой, Mojo — сразу после последнего обращения к значению. Для больших буферов и открытых ресурсов это заметная разница.
Перемещённое значение не остаётся «пустым объектом»
Заголовок раздела «Перемещённое значение не остаётся «пустым объектом»»В C++ после std::move объект жив: он в «валидном, но неуточнённом
состоянии» (valid but unspecified). Не путать с неопределённым
поведением: обращаться к объекту законно, стандарт лишь не обещает,
что именно вы получите. На практике это и есть источник сюрпризов.
std::string a = "файл";std::string b = std::move(a);std::cout << "a = '" << a << "', длина " << a.size();В нашем прогоне вывелось a = '', длина 0: строка опустела, но код
собрался и выполнился. В Mojo такой код просто не компилируется —
переменная после ^ не существует, а не «пуста».
Копия не случается молча
Заголовок раздела «Копия не случается молча»C++ копирует по умолчанию. Передали вектор по значению — получили копию, и никто вас не предупредил:
void takes(std::vector<int> v) { ... }
std::vector<int> big(1000000, 7);takes(big); // молча скопировался миллион элементовПроверено: копия действительно создаётся — адрес буфера внутри функции другой, — оригинал остаётся цел, предупреждений ноль.
В Mojo молчаливой копии не бывает вовсе, но по другой причине, чем можно
подумать. Прямой перевод этой сигнатуры — def takes(v: List[Int]) —
компилируется и работает, только копии там нет: аргумент по умолчанию
заимствуется. А если функции действительно нужно владение
(def takes(var v: List[Int])), компилятор откажется собирать вызов
и попросит выбрать между .copy() и ^.
Rust ведёт себя как Mojo: Vec не Copy, передача по значению
перемещает, копия — явный clone().
Чего в Mojo нет
Заголовок раздела «Чего в Mojo нет»| Возможность | Есть в | Комментарий |
|---|---|---|
Разделяемая изменяемость с проверкой в рантайме (RefCell, Cell) | Rust | В Mojo таких типов нет |
Слабые ссылки (Weak, weak_ptr) | Rust, C++ | Отдельного типа нет, хотя счётчик слабых ссылок у ArcPointer есть |
| Проверка гонок данных на этапе компиляции | Rust (Send, Sync) | В Mojo такой системы трейтов нет |
| Наследование и виртуальные функции | C++ | В Mojo нет ни классов, ни наследования структур |
| Исключения с иерархией типов | C++ | У функции в Mojo один тип ошибки: Error или своя структура (raises MyError); наследования нет |
Первые три строки — следствие возраста: самому языку три года, а стабильному API — год. Две последние — осознанные решения, о которых мы говорили в главах Структуры и Обработка ошибок.
Всё вместе
Заголовок раздела «Всё вместе»# Что Mojo разрешает и запрещает — там, где Rust и C++ ведут себя иначе.
@fieldwise_initstruct Noisy: """Сообщает о своём уничтожении, чтобы был виден момент."""
var name: String
def __deinit__(deinit self): print(" уничтожен:", self.name)
@fieldwise_initstruct View[o: Origin]: """Структура, хранящая ссылку. Прямой аналог Rust-структуры с 'a."""
var target: Pointer[Int, Self.o]
def show(self): """Читает значение по ссылке.""" print(" через ссылку:", self.target[])
def main(): print("1. значение умирает сразу после последнего использования") var first = Noisy("первый") print(" используем:", first.name) print(" а эта строка печатается уже после уничтожения")
print("2. две изменяемые ссылки на разные элементы — можно") var scores: List[Int] = [1, 2, 3] var low = Pointer(to=scores[0]) var high = Pointer(to=scores[1]) low[] = 10 high[] = 20 print(" ", scores[0], scores[1], scores[2])
print("3. структура, которая хранит ссылку") var value = 42 var view = View(Pointer(to=value)) view.show()1. значение умирает сразу после последнего использования используем: первый уничтожен: первый а эта строка печатается уже после уничтожения 2. две изменяемые ссылки на разные элементы — можно 10 20 3 3. структура, которая хранит ссылку через ссылку: 42
Коротко
Заголовок раздела «Коротко»Пришли из Rust. Модель узнаваема, но заимствования считать не нужно:
исключительность проверяется только между изменяемыми аргументами одного
вызова. Времена жизни пишутся выражением доступа вместо 'a. Уничтожение
происходит раньше, чем вы привыкли.
Пришли из C++. ^ похож на std::move, но после него переменной
не существует, а не «она в неопределённом состоянии». Копии не случаются
молча. Деструктор срабатывает не в конце блока, а сразу после последнего
использования.
Пришли из Python. Всё это — цена за отсутствие сборщика мусора. Взамен вы получаете предсказуемое освобождение ресурсов и скорость, ради которой Mojo и создавался.
Что дальше
Заголовок раздела «Что дальше»Раздел о владении закончен. Дальше — метапрограммирование: код, который выполняется при сборке программы.
🎯 Проверь себя
Почему в Mojo можно взять две изменяемые ссылки на разные элементы списка, а в Rust нельзя?
Rust проверяет исключительность заимствований везде и не различает
элементы одного контейнера — отсюда E0499. Mojo проверяет уже:
только то, что два изменяемых аргумента одного вызова не указывают
на одно значение. Меньше борьбы с компилятором, но и гарантий чуть
меньше.
Чем `^` в Mojo отличается от `std::move` в C++?
После std::move объект жив и находится в валидном, но неопределённом
состоянии — им можно пользоваться и получить сюрприз. После ^
переменной просто нет: обращение к ней не компилируется.
Когда уничтожается значение в Rust, C++ и Mojo?
В Rust и C++ — в конце области видимости. В Mojo — сразу после последнего использования, то есть обычно раньше. Проверено запуском одинакового сценария на всех трёх языках.
Чем в Mojo заменяют параметр времени жизни `'a` из Rust?
Выражением доступа: ref [x], ref [self.value], ref [items[0]].
Отдельное имя придумывать не нужно. Структуру, хранящую ссылку,
параметризуют происхождением — struct View[o: Origin], это прямой
аналог struct View<'a>.
Раздел пройден
Заголовок раздела «Раздел пройден»«Владение значениями» закончено. Теперь понятно, почему var b = a
требует уточнения, что означают mut, ref и out, когда именно
уничтожается значение и как компилятор доказывает, что ссылка безопасна.
Дальше — Метапрограммирование: параметры в квадратных
скобках, comptime, трейты и то, как компилятор генерирует
специализированный код под каждый набор параметров.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0