Давно начал следить за языком Rust, кажется, ещё до выхода версии 1.0. За это время язык сильно изменился, оформился и стал совсем взрослым, можно в производство. При этом из коробки программисту предлагается довольно много интересных концепций для разработки надёжного ПО с длительным жизненным циклом. Однако сфера промышленной автоматизации не так динамична (как нам иногда бы хотелось), поэтому пока приходится только присматриваться к Rust. Тем не менее надо знакомиться поближе. Просто так читать книжки или заметки не продуктивно, надо что-то пробовать делать. Например, можно начать с решения задачек на LeetCode (что я и решил попробовать). А при решении таких задачек иногда натыкаешься на что-то такое, с чем и Stack Overflow может не помочь, не только книжки. В результате поисков дополнительной информации наткнулся на серию заметок, которой хотелось бы поделиться с общественностью (да-да, он воровал тексты у богатых и переводил их бедным). Под катом перевод первой маленькой заметки про (отсутствие) NULL в Rust.
Большинство ключевых функциональных возможностей Rust не являются новыми или уникальными — функциональное программирование, объектно-ориентированное программирование, метапрограммирование, отсутствие NULL, управление памятью без сборщика мусора — всё это существовало и раньше. Но Rust выгодно отличается в той области, для которой он изначально задумывался — безопасная работа с памятью и производительность во время выполнения. В этой серии заметок поговорим о некоторых из перечисленных функциональных возможностях.
Как и обещал, начнем с NULL. Что на самом деле означает NULL? В большинстве языков он обозначает ничто — пустое, недействительное, нулевое или условное значение, которое может быть заменой для любого типа. NULL был придуман сэром Тони Хоаром в 1965 году и с тех пор пользуется популярностью. Rust является одним из немногих языков программирования, которые не поддерживают NULL, и небезосновательно.
Начнём с того, что сам сэр Хоар назвал NULL ошибкой в миллиард долларов. Мне, как разработчику преимущественно на C (или на C++, как автору оригинальной заметки) хорошо знакома боль от необходимости повсеместно добавлять проверки на NULL. Отсутствие хотя бы одной проверки может стать фатальным. Идея избегать ошибок, сбоев и уязвимостей звучит довольно привлекательно.
Вместо NULL язык Rust предоставляет перечисление Option, которое является общим для любого типа и имеет два варианта Some и None. Если есть вероятность, что объект типа T не имеет значения, то есть может быть None, его тип становится Option вместо T. Если попытаться использовать такой объект как T напрямую, компилятор будет люто (но понятно) ругаться. Поэтому не получится использовать объекты или ссылки, которые не определены, что устраняет целый класс ошибок.
Рассмотрим следующую программу на C++ в качестве примера:
#include <iostream>
using namespace std;
struct Wrapper
{
int value;
void print()
{
cout << value << endl;
}
};
int main()
{
cout << "Entrance" << endl;
Wrapper *w = nullptr;
w->print();
cout << "Exit" << endl;
}
Программа замечательно компилируется, но её запуск приводит к сбою. Ошибка сегментации памяти возникает, когда разыменовывается нулевой указатель. Программу легко исправить, потому что она короткая и простая. Но в реальности это часто не так. С увеличением сложности, при передаче указателя в другие функции, ошибиться становится проще простого. Это позволит ошибке проявиться у заказчика, и может потребоваться несколько дней (хорошо если так) для отладки.
Буквальный перевод программы на Rust будет выглядеть следующим образом, но, конечно, он не будет компилироваться:
fn main()
{
println!("Entrance");
let w: Wrapper = None;
w.print();
println!("Exit");
}
struct Wrapper
{
value: i32
}
impl Wrapper
{
fn print(&self)
{
println!("{}", self.value);
}
}
Объект w не может иметь тип Wrapper и иметь значение None. Если не получается присвоить ему значение, он должен иметь тип Option, который затем необходимо будет развернуть, чтобы получить объект Wrapper. Простой и безопасный способ — оператор if (в данном случае if let), который использует w, если это какое-то значение, и игнорирует его в противном случае. Следующая версия программы делает именно это и работает корректно:
fn main()
{
println!("Entrance");
let w: Option<Wrapper> = None;
if let Some(v) = w
{
v.print();
}
println!("Exit");
}
И хотя в C++ есть аналогичный необязательный тип Optional со значением NULL, он настолько же полезный, насколько обязательный. Компилятор Rust, наоборот, не будет компилировать программу без всех необходимых проверок, если используется Option (а без него нельзя использовать None, т.к. NULL же нет).
Итак, какое впечатление от Rust на данный момент? Неудобно для идеального результата? Больше чем просто разница в синтаксисе? Чрезмерно усердный компилятор? Всё вышеперечисленное? Верно. Следите за обновлениями и мы вместе узнаем, что делает Rust таким, какой он есть.
Другие заметки цикла:
С лёгким налётом ржавчины или куда делся NULL
amarao
Мне кажется, вы не очень далеко прочитали про Rust.
ob1 Автор
Замечание в целом верное, пока ещё читаю. :) Полагаю, что для связи с кодом на Си (в котором активно используется NULL) приходится выкручиваться через сырые указатели std::ptr. Для самого Rust такой концепции как NULL нет. А вот возможность есть. И это хорошо.
amarao
Примитивный (базовый) тип pointer может иметь и null, и прочие гадости.
https://doc.rust-lang.org/std/primitive.pointer.html
Все остальные благородные типы (вроде slice или box) внутри имеют эти самые pointer'ы.
morijndael
Только этот код не упадет, потому что разыменования не происходит, и указатель мало отличается от обычного числа. А разыменование придется оборачивать в unsafe-блок, то есть вручную дать гарантии отсутствия нулевого указателя.
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6e21d9c0d908030bc1a9440f5a5146aa
amarao
Безусловно, это так. Разименование ptr — это unsafe, а часто и прямое UB (в зависимости от степени ужаса в указателе). Я привёл этот код для того, чтобы показать, что в Rust есть и указатели, и NULL.
Дальше нужно обсуждать, что такого волшебного было сделано в стандартной библиотеке, языке и системе типов, чтобы можно было мирно порхать по result'ам и box'ам ни разу не наткнувшись на луркающее снизу UB.
struvv
в unsafe по очевидным причинам есть всё, что есть в си, но когда говорят про rust и безопасность, то таком контексте речь всегда идёт о safe подмножестве и для удобства можно автоматом вставлять копипасту про safe подмножество, чтобы не рождать бесполезные споры
amarao
У меня как раз появлось ощущение, что в интернетах образовался особый пласт Rust-о-фанов, которые Rust толком не знают, зато несут особый свет особого неофитства о том, как Rust, волшебным взмахом safe делает всё safe и какой он safe, и что в нём нет указателей и NULL, и не может быть ub.
Хотя на самом деле, Rust — это такая специальная дисциплина ума, плюс разумные дефолты, выписанные в виде языка, которые дают возможность меньше думать про тлен и больше думать про сложное. В отсутствие дисциплины код на Rust'е превращается в такую же кашу, как и на любом другом языке.
struvv
rust подмножество safe просто даёт хорошую абстракцию, которая не течёт
это позволяет «забыть» о том, что внутри инкапсуляции, так как это правильно — «не знать» как там устроены внутренности. А знать приходится начинать тогда, когда они начинают протекать и является некачественным поделием. Всегда, когда разработчик должен знать что внутри X — это кривые руки архитектора интерфейсов X. Под словом «знать» я имею в виду принимать решения учитывая внутреннее устройство, а не настоящее знание деталей устройства
Потому если какой-то язык или инженерный механизм и так далее требуют знания всё более мелких деталей внутри это говорит не о мощности этого инструмента и крутости его эксплуатантов, а лишь о дырявости абстракций, которые применяются в этом добре
rust разделяет уровни абстракции на safe и unsafe делая так, что в safe протечки не происходят, в отличие от тонущего в протечках как дуршлаг cpp и прочих подобных языков
Именно это и делает rust тем, чем он является, остальное это просто следствие разделения абстракций на правильные уровни
amarao
Я не чуть не собираюсь оправдывать С++; более того, я очень люблю Rust. Но!
safe в rust даёт достаточно ограниченные гарантии. Я сейчас попытался найти тот пример, которым мне ткнули недавно, где сделали реальный wtf UB в совершенно safe-коде всего лишь с комбинацией двух lifetime'ов и одного static'а (на чтение). Увы, не нашёл.
Более того, многие не понимают, что от чего именно safe Rust. Там всего лишь хотят сделать так, чтобы не было UB. В ходе этого "всего лишь" оказывается, что очень много "edge case'ов" других языков — нифига не 'safe' (вставление в set элемента при итерации по нему, например).
А вот вторая часть Rust, которую почему-то не замечают, на самом деле, значит куда больше, чем война с UB (под словом safe). Например, очень, очень, разумные дефолты. Copy для непримитивных типов только в явном виде (move по-умолчанию), ничего не public пока не сказано обратное, всё readonly пока не попросили mut, никаких type elision в неожиданных местах (
foo as usize
100500 раз), запрет на модификацию чужих трейтов для чужих типов.Я бы сказал, что вот эта часть (разумные строгие дефолты) значит куда больше, чем культ вокруг safe. Во многих языках unsafe (в контексте raw pointers) просто нет, и они себя отлично чувствуют, т.е. само по себе "скрытие" ptr — это не особое достижение.
freecoder_xx
Вот пример, воспроизводящий упомянутую вами проблему (использование после освобождения):
Запустить
И это — баг в компиляторе: https://github.com/rust-lang/rust/issues/25860
freecoder_xx
Все-таки данная заметка — не про UB, а про неудобства с использованием null и "падения" программ из-за него. Думаю здесь уместнее было бы сравнение с Java (или с другим языком, имеющим null), а не с указателями C/C++.
amarao
Да, в контексте сравнения null джавы (который не ведёт к UB, а просто к runtime ошибкам) — у раста есть некоторые преимущества, но..
или экивалент jav'овый без проверки на null на самом деле одно и то же. Вернули None/Null — упади. Т.е. компилятор подразумевает обработку ошибок, но вовсе её не требует. В этом смысле, кстати,
[]
в rust ведёт себя так же, как и java-код. Вылетел за границы — вот тебе паника.DarkEld3r
Ну как же? Очень даже требует, иначе не пришлось бы явно писать
unwrap
. Не требует "обрабатывать правильно" — да, но в общем случае это невозможно (где-то и падать на панике — вполне нормальное поведение).С нулабельными типами проблема в том, что нельзя статически гарантировать наличие значения, вот и вылетают NullPointerException периодически.
amarao
а, почитал, что в java. У них всё может быть null, и невозможно сконструировать non-nullable тип. Бедные, бедные джависты.