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

Руководство по кодированию

Posted on:3 мая 2023 г. at 09:05

Example Dynamic OG Image link

Идиоматический 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. Некоторое время спустя среда выполнения:

Упрощенная диаграмма для кода, написанного внутри асинхронного блока:

       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Многие из них могут существовать одновременно.Сложнее всего произвести для передающего.

Небезопасный, необоснованный, неопределённый

Небезопасность приводит к необоснованности. Необоснованность приводит к неопределённости. Неопределенный ведет к темной стороне силы.

Безопасный код

let y = x + x;  // Безопастность Rust только гарантирует соответствие выполнения этого кода
print(y);       // 'спецификация' (длинная история …). Это не гарантирует, что `y` равно `2x`
                // (`X::add` может это реализовано плохо) и что же `y` не напечатано (`Y::fmt` может паниковать).

Небезопасный код

// `x` всегда должен указывать на свободную от гонки, допустимую, выровненную, инициализированную память u8.
unsafe fn unsafe_f(x: *mut u8) {
    my_native_lib(x);
}

Неопределенное поведение (UB)

if maybe_true() {
    let r: &u8 = unsafe { &*ptr::null() };   // После запуска приложения ВСЕ не определено. Даже если строка,
} else {                                     // казалось бы, ничего не сделала, приложение теперь сможет запускать оба пути,
    println!("the spanish inquisition");     // поврежденную базу данных или что-то еще.
}

Неподходящий код

fn unsound_ref<T>(x: &T) -> &u128 {      // Сигнатура выглядит безопасно для пользователей.
    unsafe { mem::transmute(x) }         // Выполняется нормально, если вызывается с &u128.
}

Ответственное использование небезопасного кода

Гонка кода

Гонка код — это безопасный код 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 отбрасывает это, это произойдет как можно скорее, часто сразу)!

Последствия

Стабильность API

При обновлении API эти изменения могут нарушить работу клиентского кода. RFC Основные изменения (🔴) определенно нарушаются, в то время как незначительные изменения (🟡) могут нарушать:

Крейты:

🔴 компиляция крейта, который ранее компилировался для стабильной работы, требует ночную версию.

🟡 Изменение использования Cargo (например, добавление или удаление функций).

Модули:

🔴 Переименование/перемещение/удаление любых общедоступных элементов.

🟡 Добавление новых общедоступных элементов, так как это может нарушить код, использующий ваш_crate::*.

Структуры:

🔴 Добавление закрытого поля, когда все текущие поля открыты.

🔴 Добавление открытого поля, если закрытого поля не существует.

🟡 Добавление или удаление закрытых полей, если хотя бы одно из них уже существует (до и после изменения).

🟡 Переход от кортежной структуры со всеми закрытыми полями (по крайней мере, с одним полем) к нормальной структуре или наоборот.

Перечисления:

🔴 Добавление новых вариантов, может быть смягчен с помощью раннего #[non_exhaustive].

🔴 Добавление новых полей в вариант.

Трейты:

🔴 Добавление элемента, не являющегося элементом по умолчанию, прерывает все существующие impl T for S {}.

🔴 Любое нетривиальное изменение подписей элементов затронет либо потребителей, либо исполнителей.

🟡 Добавление элемента по умолчанию, может привести к неоднозначности диспетчеризации с другими существующими трейтами.

🟡 Добавление параметра типа по умолчанию.

🔴 Реализация любого «фундаментального» трейта, так как не реализованный фундаментальный трейт уже была обещанием.

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

Встроенные реализации:

🟡 Добавление любых встроенных элементов, может привести к тому, что клиенты предпочтут это трейту функцию и приведут к ошибке компиляции.

Сигнатуры в определениях типов:

🔴 Ужесточение границ (например, <T> до <T: Clone>).

🟡 Ослабление границ.

🟡 Добавление параметров типа по умолчанию.

🟡 Обобщение на дженерики.

Сигнатуры в функциях:

🔴 Добавление/удаление аргументов.

🟡 Ввод нового параметра типа.

🟡 Обобщение на дженерики.

Поведенческие изменения:

🔴 / 🟡 Изменение семантики может не привести к ошибкам компилятора, но может заставить клиентов делать неправильные вещи.