
Привет, Хабр!
Знакомая история: пишешь код, всё работает, ревью прошло, поехали дальше. А через какое-то время натыкаешься в документации на метод, который делал ровно то же самое — в одну строку, без твоего велосипеда на десять. И остаётся только вздохнуть: ну где ж ты был раньше!
Со стандартной библиотекой Rust так постоянно. Она большая, и половину её мы не используем не потому, что она плохая, а потому, что просто про неё не знаем — и пишем руками то, что давно лежит готовым. Во второй части уже разбирали такие штуки: never-тип !, matches!, black_box, прозрачные обёртки repr(transparent) и transmute_copy. Подготовил для вас ещё девять.
Вы клонируете поле только чтобы вытащить его из &mut self
Знакомо: метод с &mut self, и надо забрать поле целиком — отдать наружу или поменять на новое. А компилятор не даёт вынести значение из-под &mut. Логика такая: если заберёшь, в структуре останется дырка, а тип обязан всегда быть целым. Вот и приходится клонировать поле, хотя оригинал больше не нужен, или оборачивать всё в Option ради .take().
mem::take (с 1.40) все это решит без клонов. Забирает значение, а на его место кладёт Default. Перенос бесплатный, это обычный move:
use std::mem; struct Parser { tokens: Vec<String>, } impl Parser { fn drain_tokens(&mut self) -> Vec<String> { // отдаём Vec наружу, в self.tokens остаётся пустой вектор mem::take(&mut self.tokens) } }
Нет Default — бери mem::replace (он в языке с самого начала): кладёт то, что дал ты, и отдаёт старое. Шикарено заходит на машины состояний, где надо забрать текущее состояние и сразу положить следующее:
enum State { Idle, Running(Job), Done, } fn advance(state: &mut State) { // забираем текущее состояние, временно ставим заглушку let current = mem::replace(state, State::Idle); *state = match current { State::Running(job) => { job.finish(); State::Done } other => other, }; }
Кстати, Option::take, которой все пользуются, внутри — это mem::take.
Вы пишете if cond { Some(x) } else { None }
Мелочь, а попадается постоянно: из условия надо сделать Option. Особенно в filter_map, где None значит «пропусти». И почти всегда это пишут трёхстрочным if.
У bool для этого есть then (с 1.50) и then_some (с 1.62). Первый берёт замыкание, второй — готовое значение:
// было let role = if is_admin { Some(Role::Admin) } else { None }; // стало let role = is_admin.then(|| Role::Admin); // через замыкание let port = use_tls.then_some(443); // через значение // и где это правда красит код — внутри итераторов let evens: Vec<i32> = (0..10) .filter_map(|n| (n % 2 == 0).then_some(n)) .collect();
then ленивый: замыкание не сработает, если условие false. then_some считает значение всегда, ещё до проверки. Так что если справа что-то тяжёлое или с побочкой — берем then.
Вы режете строку через find и индексы
Разобрать key=value, снять Bearer у токена, отрезать расширение файла. Обычно это делают через find, а потом режут срезами &s[..i] и &s[i+1..]. И каждый раз думаешь, не уехал ли индекс, и рискуешь паникой на границе символа UTF-8.
split_once (с 1.52) режет по первому вхождению и отдаёт Option<(&str, &str)>:
let line = "timeout=30"; if let Some((key, value)) = line.split_once('=') { // key = "timeout", value = "30" } // "a=b=c".split_once('=') -> Some(("a", "b=c")) только по первому // "a=b=c".rsplit_once('=') -> Some(("a=b", "c")) rsplit_once — по последнему
А strip_prefix и strip_suffix (с 1.45) снимают префикс или суффикс и возвращают Option (None, если не совпало):
let header = "Bearer abc123"; let token = header.strip_prefix("Bearer "); // Some("abc123"), или None если префикса нет let name = "archive.tar.gz"; let base = name.strip_suffix(".gz"); // Some("archive.tar")
А вот про это знают не все: аргумент — не обязательно строка. Это любой Pattern: символ, строка, набор символов &['=', ':'] или замыкание |c: char| .... То есть разделителем может быть условие:
" hello".strip_prefix(char::is_whitespace); // Some(" hello") — снимает один пробел "timeout: 30".split_once(|c: char| c == '=' || c == ':'); // Some(("timeout", " 30"))
И ещё пара мелочей. split_once вернёт None, если разделителя нет вообще. И не путаем strip с trim: trim срезает все повторы и всегда отдаёт &str, а strip снимает один раз и даёт Option.
Вы держите Option рядом с итератором, чтобы заглянуть вперёд
В парсерах и токенайзерах постоянно нужно посмотреть на следующий элемент, но не съедать его — решить, что делать дальше. И народ таскает рядом с итератором Option со «следующим» и вручную его обновляет.
.peekable() (с 1.0) оборачивает итератор, а .peek() показывает следующий элемент по ссылке, не сдвигая сам итератор:
let mut it = "3 + 4".chars().peekable(); // пропускаем пробелы, подглядывая вперёд while let Some(&c) = it.peek() { if c.is_whitespace() { it.next(); // съели пробел } else { break; } }
А next_if (с 1.51) забирает следующий элемент только если он проходит проверку:
let mut nums = [1, 2, 3].into_iter().peekable(); nums.next_if(|&n| n == 1); // съест 1 nums.next_if(|&n| n == 99); // None, ничего не съест
Удобно тем, что не надо вручную возвращать подсмотренный элемент обратно в итератор.
Вы сортируете по убыванию через sort плюс reverse
По убыванию — и человек пишет .sort(), следом .reverse(), то есть два прохода. Или компаратор руками, |a, b| b.cmp(a), и через раз путает, где a, а где b. А с несколькими ключами компаратор превращается в лесенку из if.
cmp::Reverse (с 1.19) — обёртка, которая переворачивает сравнение. Завернул ключ — сортировка по нему идёт по убыванию, в один проход:
use std::cmp::Reverse; struct Task { priority: u32, name: String, } let mut tasks: Vec<Task> = load_tasks(); // по убыванию приоритета, без отдельного .reverse() tasks.sort_by_key(|t| Reverse(t.priority));
Для нескольких ключей — then и then_with (с 1.17): они склеивают сравнения и переходят к следующему только при равенстве на предыдущем:
// приоритет по убыванию, при равных приоритетах — имя по возрастанию tasks.sort_by(|a, b| { Reverse(a.priority).cmp(&Reverse(b.priority)) .then_with(|| a.name.cmp(&b.name)) });
then берёт готовый Ordering, then_with — замыкание (зовёт его только если первое сравнение дало Equal). Дорогой второй ключ — бери then_with.
И заодно про сами сортировки: sort стабильная, но выделяет буфер. sort_unstable быстрее и без аллокации, просто не сохраняет порядок равных элементов. Не нужна стабильность — бери sort_unstable_by_key. А Reverse ещё и превращает BinaryHeap в кучу-минимум, если класть в неё Reverse(value).
Вы фильтруете список дважды, чтобы разделить его надвое
Надо разложить элементы на «подходят» и «нет». Обычно гоняют .filter() два раза или пишут цикл с двумя Vec.
partition (с 1.0) делает это за один проход, раскидывая по двум коллекциям:
let nums = [1, 2, 3, 4, 5, 6]; // один проход: чётные в одну кучу, нечётные в другую let (even, odd): (Vec<i32>, Vec<i32>) = nums.into_iter().partition(|&n| n % 2 == 0); // even = [2, 4, 6], odd = [1, 3, 5]
Куда складывать — выводится из типа, не обязательно Vec, подойдёт любой FromIterator. true идёт в первую коллекцию, false — во вторую.
Вы пишете структуру и impl Iterator ради одноразового итератора
Нужна последовательность: числа по формуле, чтение до конца, что-то с состоянием. И ты заводишь отдельную структуру, реализуешь Iterator, пишешь next, держишь поля состояния. Гора кода ради одного места.
iter::successors (с 1.34) строит ряд, где следующий элемент считается из предыдущего:
use std::iter; // Фибоначчи: следующая пара получается из текущей let fibs: Vec<u64> = iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b))) .map(|(a, _)| a) .take(10) .collect(); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
iter::from_fn (тоже с 1.34) ещё проще: итератор из замыкания, которое отдаёт Option. Some — продолжаем, None — конец. Состояние замыкание держит само, занеси его внутрь через move:
let mut count = 0; let counter = iter::from_fn(move || { count += 1; (count <= 3).then_some(count) }); let v: Vec<i32> = counter.collect(); // [1, 2, 3]
successors — когда следующий выводится из предыдущего. from_fn — когда логика сложнее или нужен цикл внутри. Оба ленивые и после None больше не зовутся.
Вы собираете массив через Vec, потому что не знаете как иначе
Нужен массив фиксированной длины, скажем [Vec<u8>; 8]. Через [Vec::new(); 8] не выйдет — такое требует Copy. И народ либо берёт Vec вместо массива, либо лезет в MaybeUninit с unsafe.
array::from_fn (с 1.63) зовёт замыкание для каждого индекса и собирает массив. Без unsafe и без Default:
use std::array; // массив из 8 пустых буферов: ни Copy, ни Default не нужно let buffers: [Vec<u8>; 8] = array::from_fn(|_| Vec::new()); // индекс приходит в замыкание — можно заполнять по позиции let squares: [i32; 5] = array::from_fn(|i| (i * i) as i32); // [0, 1, 4, 9, 16]
Длина берётся из типа, а индекс прилетает в замыкание — удобно для небольших таблиц и буферов.
Вы складываете два числа и надеетесь, что не переполнится
Пишешь a + b, и поведение при переполнении зависит от того, как собрана программа. В debug — паника, в release — молча оборачивается по модулю. То есть в проде число тихо станет не тем, и узнаешь об этом по последствиям.
Это не UB. Знаковое переполнение в C неопределено, и компилятор там может сделать с программой что угодно. В Rust переполнение — определённое: либо паника в debug, либо обёртка в release, и память в порядке в любом случае. Хочешь ловить переполнение и в релизе — поставь overflow-checks = true в профиле Cargo.toml.
Но полагаться на дефолт по сборке не стоит, поведение разное. У каждого целого есть четыре явных метода:
let a: u8 = 200; let b: u8 = 100; // 200 + 100 = 300, в u8 не помещается a.checked_add(b); // None — переполнилось, решай сам что делать a.saturating_add(b); // 255 — упёрлись в потолок типа и встали a.wrapping_add(b); // 44 — явная обёртка по модулю 2^8 a.overflowing_add(b); // (44, true) — и результат, и флаг, что переполнилось
checked_* даёт Option, saturating_* упирается в границу типа, wrapping_* оборачивает осознанно, overflowing_* отдаёт результат и флаг. С любым из них поведение одинаковое.
А если весь расчёт идёт в одном режиме — есть обёртки Wrapping (давно) и Saturating (с 1.74). Они перегружают +, -, *, так что не надо звать методы на каждом шаге. Удобно в хешах, где обёртка — часть алгоритма:
use std::num::Wrapping; let mut hash = Wrapping(0u32); for &byte in data { hash = (hash << 5) ^ Wrapping(byte as u32); // +, *, ^ тут уже wrapping } let result = hash.0;
Люблю Rust
Девять инструментов, и ни одного крейта, макроса или unsafe. Всё лежит в std годами, просто про это редко вспоминают.
А теперь вопрос в зал: какой инструмент из std вы дольше всего обходили стороной, а наткнувшись — поняли, какое это золото? Накидайте в комментах.
Если зайдёт — будет и третья часть, у std есть ещё много чего.
Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.
Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.


