Идиоматический Rust
Если вы привыкли к Java или C, рассмотрите это.
Идиома | Код |
---|---|
Думать о выражениях | y = if x { a } else { b }; |
y = loop { break 5 }; | |
fn f() -> u32 { 0 } | |
Думать о итераторах | (1..10).map(f).collect() |
names.iter().filter(| x| x.starts_with("A")) | |
Обрабатывать отсутствие с помощью ? | y = try_something()?; |
get_option()?.run()? | |
Использование строгих типов | enum E { Invalid, Valid { … } } в место ERROR_INVALID = -1 |
enum E { Visible, Hidden } в место visible: bool | |
struct Charge(f32) в место f32 | |
Неправильный стейт (state) невозможен | my_lock.write().unwrap().guaranteed_at_compile_time_to_be_locked = 10; 1 |
thread::scope(|s| { /_ Потоки не могут существовать дольше, чем scope() _/ }); | |
Предоставьте построителям | Car::new("Model T").hp(20).build(); |
Не паникуйте | Паника не исключение, она предполагает немедленный процесс прерывания! |
Паника только при ошибке программы в противном случае используйте Option<T> или Result<T,E> . | |
Если пользователь явно запросил, например, вызов get() против try_obtain(), паника тоже. | |
Дженерики в модерации | Простой <T: Bound> (например, AsRef<Path> ) может сделать API более удобными для использования. |
Сложные границы делают невозможным их соблюдение. Если сомневаетесь, не будьте креативными. | |
Разделенные реализации | Дженерики типа Point<T> могут иметь отдельный impl для Т для некоторой специализации. |
impl<T> Point<T> { /* Добавьте общие методы здесь */ } | |
impl Point<f32> { /* Добавление методов, относящихся только к Point<f32> */ } | |
Опасный | Избегайте unsafe {}, часто есть более безопасное и быстрое решение без него. |
Реализация трейтов | #[derive(Debug, Copy, …)] и пользовательский ‘impl’ там, где это необходимо. |
Инструменты | Регулярно запускайте clippy, чтобы значительно улучшить качество кода. |
Отформатируйте код с помощью rustfmt для обеспечения согласованности. | |
Добавьте модульные тесты (#[test] ) для проверки работоспособности кода. | |
Добавьте тесты документации ('''my_api::f()'''), чтобы убедиться, что документы соответствуют коду. | |
Документация | Аннотировать API-интерфейсы комментариями к документам, которые могут отображаться на docs.rs. |
Не забудьте включить краткое предложение и заголовок Примеры. | |
Если применимо: Паника, Ошибки, Безопасность, Прерывание и Неопределенное поведение. |
1
В большинстве случаев вы должны предпочесть ? над .unwrap(). Однако в случае блокировок возвращенный PoisonError означает панику в другом потоке, поэтому его развертывание (тем самым распространяя панику) часто является лучшей идеей.
Async-Await 101
Если вы знакомы с async/await в C# или TypeScript, вот некоторые вещи, которые следует иметь в виду:
Основные
Конструкция | Комментарий |
---|---|
async | Любой элемент, объявленный асинхронным, всегда возвращает impl Future<Output=_> . |
async fn f() {} | Функция f возвращает значение типа future<Output=()> . |
async fn f() -> S {} | Функция f возвращает значение типа future<Output=S> . |
async { x } | Преобразует {x} в impl Future<Output=X> . |
let sm = f(); | Вызов f(), который является async, не выполнит f, а создаст контейнер Future sm.(^1) (^2) |
sm = async { g() }; | Аналогично не выполняет блок { g() }, а производит контейнер Future . |
runtime.block_on(sm); | Вне async {} запланируется фактический запуск sm. Выполнит g().(^3) (^4) |
sm.await | Внутри sync {} запустите sm до завершения. Уступит время выполнения основной программе, если sm не готов. |
(^1) Технически асинхронно преобразует следующий код в анонимный, сгенерированный компилятором тип конечного автомата, f()
создает экземпляр этой машины.
(^2) Контейнер Future
всегда подразумевает Future
, в зависимости от типов, используемых внутри async.
(^3) Контейнер Future
, управляемый рабочим потоком, вызывающим Future::poll()
через среду выполнения напрямую или родительский .await
косвенно.
(^4) Rust не поставляется с веб сервером, для этого нужен внешний крейт, например, tokio.
Поток выполнения
В каждом x.await
контейнер Future
передает управление подчиненному контейнеру Future
x
. В какой-то момент низкоуровневый контейнер Future
, вызываемый через .await
, может быть ещё не готов завершиться. В этом случае рабочий поток возвращается вплоть до среды выполнения, чтобы он мог управлять другим Future
. Некоторое время спустя среда выполнения:
- может возобновить выполнение. Обычно это происходит, если только sm / Future не упал.
- может возобновить работу с предыдущим рабочим или другим рабочим потоком (зависит от времени выполнения).
Упрощенная диаграмма для кода, написанного внутри асинхронного блока:
consecutive_code(); consecutive_code(); consecutive_code();
СТАРТ --------------------> x.await --------------------> y.await --------------------> ГОТОВ
// ^ ^ ^ Future<Output=X> ready -^
// Вызывается через среду | |
// выполнения | |
// или внешний .await | Это может возобновиться в другом потоке (следующем наиболее доступном),
// | или НЕТ ВООБЩЕ, если Future было удалено.
// |
// Выполнить x. Если готово: просто продолжает исполнение, в противном случае возвращает
// этот поток в среду выполнения.
Предостережение
Учитывая поток выполнения, некоторые соображения при написании кода внутри асинхронной конструкции:
Конструкция(^1) | Комментарий |
---|---|
sleep_or_block(); | Однозначно плохо 🛑, никогда не останавливают текущий поток, засоряет исполнителя. |
set_TL(a); x.await; TL(); | Однозначно плохо 🛑, async может вернуться из другого потока, поток локальный недопустим. |
s.no(); x.await; s.go(); | Может быть, плохо 🛑, await не вернется, если Future упадет во время ожидания. |
Rc::new(); x.await; rc(); | Не отправляемые типы предотвращают отправку impl Future, менее совместимый. |
(^1) Здесь мы предполагаем, что s
— это любой нелокальный, который может быть временно переведен в недопустимое состояние; TL — это локальное хранилище любого потока, и async {}, содержит код, написанный без учета специфики исполнителя.
(^2) Поскольку Drop запускается в любом случае, когда Future отбрасывается, рассмотрите возможность использования drop guard, который очищает / исправляет состояние приложения, если его нужно оставить в плохом состоянии через точки .await.
Замыкания в API
Существует отношение субтрейта Fn: FnMut: FnOnce
. Это означает замыкание, которое реализует Fn
, также реализует FnMut
и FnOnce
. Аналогично замыкание, реализующее FnMut
, также реализует FnOnce
.
С точки зрения точки вызова это означает:
Сигнатура | Функция g может вызывать … | Функция g принимает … |
---|---|---|
g<F: FnOnce()>(f: F) | … f() как только. | Fn , FnMut , FnOnce |
g<F: FnMut()>(mut f: F) | … f() несколько раз. | Fn , FnMut |
g<F: Fn()>(f: F) | … f() несколько раз. | Fn |
Обратите внимание, что запрос закрытия Fn
в качестве функции является наиболее ограничительным для вызывающего объекта; но наличие закрытия Fn
в качестве вызывающего объекта наиболее совместимо с любой функцией.
С точки зрения того, кто определяет замыкание:
Замыкание | Реализация(^*) | Комментарий |
---|---|---|
|| { moved_s; } | FnOnce | Передающий должен отказаться от права собственности на moved_s . |
|| { &mut s; } | FnOnce , FnMut | Позволяет g() изменять локальное состояние переменной s. |
|| { &s; } | FnOnce , FnMut , Fn | Не может мутировать состояние, но может совместно использовать и повторно использовать s. |
Rust предпочитает получать параметр по ссылке (что приводит к наиболее «совместимым» замыканиям с точки зрения вызывающего функцию), но может быть принудительно забран параметр и средой путем копирования или перемещения с помощью синтаксиса move || {}
.
Это дает следующие преимущества и недостатки:
Требование | Преимущество | Недостаток |
---|---|---|
F: FnOnce | Легко удовлетворить замыкание. | Только для одноразового использования, g() может вызвать f() только один раз. |
F: FnMut | Позволяет g() изменять состояние замыкания. | Вызывающий код не может повторно использовать замыкание во время g(). |
F: Fn | Многие из них могут существовать одновременно. | Сложнее всего произвести для передающего. |
Небезопасный, необоснованный, неопределённый
Небезопасность приводит к необоснованности. Необоснованность приводит к неопределённости. Неопределенный ведет к темной стороне силы.
Безопасный код
- Безопастность имеет ограниченное значение в Rust, туманно «внутренняя профилактика неопределенного поведения (UB)».
- Обоснованность означает, что язык не позволяет использовать себя, чтобы вызвать UB.
- Крушение самолета или удаление базы данных не является UB, поэтому «безопасно» с точки зрения Rust.
- Запись в
/proc/[pid]/mem
для самостоятельного изменения кода также является «безопасной», в результате чего UB не вызывается по своей сути.
let y = x + x; // Безопастность Rust только гарантирует соответствие выполнения этого кода
print(y); // 'спецификация' (длинная история …). Это не гарантирует, что `y` равно `2x`
// (`X::add` может это реализовано плохо) и что же `y` не напечатано (`Y::fmt` может паниковать).
Небезопасный код
- Код, помеченный как небезопасный, имеет специальные разрешения, например, для удаления необработанных указателей или вызова других небезопасных функций.
- Вместе с этим приходят специальные обещания, которые автор должен выполнить перед компилятором, и компилятор будет доверять вам.
- Сам по себе небезопасный код неплохо, но опасный и необходим для FFI или экзотических структур данных.
// `x` всегда должен указывать на свободную от гонки, допустимую, выровненную, инициализированную память u8.
unsafe fn unsafe_f(x: *mut u8) {
my_native_lib(x);
}
Неопределенное поведение (UB)
- Как уже упоминалось, небезопасный код подразумевает особые обещания компилятору (в противном случае он не должен быть небезопасным).
- Невыполнение какого-либо обещания (промиса) заставляет компилятор производить ошибочный код, выполнение которого приводит к UB.
- После запуска неопределенного поведения может произойти что угодно. Коварно, эффекты могут быть 1) тонкими, 2) проявляться далеко от места нарушения или 3) быть видимыми только при определенных условиях.
- Казалось бы, работающая программа (включая любое количество модульных тестов) не является доказательством того, что код UB не может потерпеть неудачу из за прихоти.
- Код с UB объективно опасен, недействителен и никогда не должен существовать.
if maybe_true() {
let r: &u8 = unsafe { &*ptr::null() }; // После запуска приложения ВСЕ не определено. Даже если строка,
} else { // казалось бы, ничего не сделала, приложение теперь сможет запускать оба пути,
println!("the spanish inquisition"); // поврежденную базу данных или что-то еще.
}
Неподходящий код
- Любой безопасный Rust код, который может (даже только теоретически) производить UB для любого пользовательского ввода, всегда является необоснованным.
- Как и небезопасный код, который может вызывать UB по собственному желанию, нарушая вышеупомянутые обещания.
- Необоснованный код представляет собой угрозу стабильности и безопасности и нарушает основные предположения, которые имеют многие пользователи Rust.
fn unsound_ref<T>(x: &T) -> &u128 { // Сигнатура выглядит безопасно для пользователей.
unsafe { mem::transmute(x) } // Выполняется нормально, если вызывается с &u128.
}
Ответственное использование небезопасного кода
- Не используйте
unsafe
, если вам это абсолютно не нужно. - Следуйте рекомендациям Nomicon Unsafe, всегда соблюдайте все правила безопасности и никогда не ссылайтесь на UB.
- Сведите к минимуму использование
unsafe
и инкапсулируйте их в небольшие предупреждающие модули, которые легко просматривать. - Никогда не создавайте нездоровых абстракций, если вы не можете правильно инкапсулировать
unsafe
, не делайте этого. - Каждое небезопасное устройство должно сопровождаться текстовыми рассуждениями, описывающими его безопасность.
Гонка кода
Гонка код — это безопасный код 3-й стороны, который компилируется, но не соответствует ожиданиям API и может мешать вашим собственным гарантиям (безопасности).
Вы, автор | Пользовательский код может … |
---|---|
fn g<F: Fn()>(f: F) { … } | Неожиданная паника. |
struct S<X: T> { … } | Плохо реализовано Т , например неправильно использован Deref,… |
macro_rules! m { … } | Сделайте все вышеперечисленное, вызов может иметь странную область видимости. |
Патерн риска | Комментарий |
---|---|
#[repr(packed)] | Упакованное выравнивание может сделать ссылку &s.x недопустимой. |
impl std::… for S {} | Любой трейт impl, особенно std::ops может быть нарушен. В частности… |
- impl Deref for S {} | Может случайным образом Deref, например, s.x != s.x, или паника. |
- impl PartialEq for S {} | Может нарушать правила равенства, паника. |
- impl Eq for S {} | Может вызвать s != s, паника, не должен использовать s в HashMap и других. |
- impl Hash for S {} | Может нарушать правила хеширования, паника, не должен использовать s в HashMap и других. |
- impl Ord for S {} | Может нарушать правила назначения, паника, не должны использовать s в BTreeMap и других. |
- impl Index for S {} | Может случайным образом индексировать, например, s[x] != s[x], паника. |
- impl Drop for S {} | Может выполнять код или паниковать, конец области {}, во время назначения s = new_s. |
panic!() | Пользовательский код может паниковать в любое время, что приводит к прерыванию или раскручиванию стека. |
catch_unwind(|| s.f(panicky)) | Кроме того, вызывающий код может принудительно наблюдать за нарушенным состоянием в s. |
let … = f(); | Имя переменной может повлиять на порядок выполнения Drop.(^1) |
(^1) Примечательно, что при переименовании переменной из _x
в _ваше
также измените поведение Drop, поскольку вы измените семантику. Переменная с именем _x
будет иметь Drop::drop()
, выполненную в конце ее области, переменная с именем _
может выполнить ее немедленно при «очевидном» назначении («очевидно», потому что привязка с именем _
означает, что подстановочный знак REF отбрасывает это, это произойдет как можно скорее, часто сразу)!
Последствия
- Общий код не может быть безопасным, если безопасность зависит от сотрудничества типов w.r.t. большинство (
std::
) трейтов. - Если требуется типовая кооперация, вы должны использовать небезопасные трейты (вероятно, реализовать свои собственные).
- Необходимо учитывать случайное выполнение кода в неожиданных местах (например, переназначение, конец области).
- Вы все еще можете наблюдать после паники.
Как следствие, безопасный, но падающи код (например,
airplane_speed<T>()
), вероятно, также должен следовать этому руководству.
Стабильность API
При обновлении API эти изменения могут нарушить работу клиентского кода. RFC Основные изменения (🔴) определенно нарушаются, в то время как незначительные изменения (🟡) могут нарушать:
Крейты:
🔴 компиляция крейта, который ранее компилировался для стабильной работы, требует ночную версию.
🟡 Изменение использования Cargo (например, добавление или удаление функций).
Модули:
🔴 Переименование/перемещение/удаление любых общедоступных элементов.
🟡 Добавление новых общедоступных элементов, так как это может нарушить код, использующий ваш_crate::*
.
Структуры:
🔴 Добавление закрытого поля, когда все текущие поля открыты.
🔴 Добавление открытого поля, если закрытого поля не существует.
🟡 Добавление или удаление закрытых полей, если хотя бы одно из них уже существует (до и после изменения).
🟡 Переход от кортежной структуры со всеми закрытыми полями (по крайней мере, с одним полем) к нормальной структуре или наоборот.
Перечисления:
🔴 Добавление новых вариантов, может быть смягчен с помощью раннего #[non_exhaustive]
.
🔴 Добавление новых полей в вариант.
Трейты:
🔴 Добавление элемента, не являющегося элементом по умолчанию, прерывает все существующие impl T for S {}
.
🔴 Любое нетривиальное изменение подписей элементов затронет либо потребителей, либо исполнителей.
🟡 Добавление элемента по умолчанию, может привести к неоднозначности диспетчеризации с другими существующими трейтами.
🟡 Добавление параметра типа по умолчанию.
🔴 Реализация любого «фундаментального» трейта, так как не реализованный фундаментальный трейт уже была обещанием.
🟡 Реализация любого несущественного трейта, может также вызвать неоднозначность рассылки.
Встроенные реализации:
🟡 Добавление любых встроенных элементов, может привести к тому, что клиенты предпочтут это трейту функцию и приведут к ошибке компиляции.
Сигнатуры в определениях типов:
🔴 Ужесточение границ (например, <T>
до <T: Clone>
).
🟡 Ослабление границ.
🟡 Добавление параметров типа по умолчанию.
🟡 Обобщение на дженерики.
Сигнатуры в функциях:
🔴 Добавление/удаление аргументов.
🟡 Ввод нового параметра типа.
🟡 Обобщение на дженерики.
Поведенческие изменения:
🔴 / 🟡 Изменение семантики может не привести к ошибкам компилятора, но может заставить клиентов делать неправильные вещи.