Идиомы стандартной библиотеки Rust: трюки на каждый день
На каждой кухне есть ящик с мелкими инструментами: овощечистка, открывалка, чеснокодавка. Любую работу можно сделать одним большим ножом, но мелкие инструменты делают её быстрее и безопаснее. В стандартной библиотеке Rust тоже есть такой ящик.
Эта страница — экскурсия по нему. Каждый трюк короткий: картинка из жизни, запускаемый пример и строка «используй, когда…», чтобы было понятно, когда за ним тянуться. Учить всё сразу не нужно. Пролистай страницу сейчас и возвращайся, когда заметишь в своём коде неуклюжее место.
Забираем значение из &mut: mem::take, replace, swap
Заголовок раздела «Забираем значение из &mut: mem::take, replace, swap»Представь мусорное ведро с пакетом. Полный пакет вынуть можно, но только если сразу вставить новый. Ведро никогда не остаётся без пакета.
В Rust то же правило. Если у тебя есть только &mut self, просто переместить поле наружу нельзя: в структуре осталась бы дыра. Компилятор остановит тебя ошибкой E0507. Функции из std::mem решают это тем, что всегда что-то оставляют на месте:
mem::take(&mut x)забирает значение и оставляет значение по умолчанию для этого типа (пустойVec, пустуюString,0,None).mem::replace(&mut x, new)кладётnewи отдаёт тебе старое значение.Option::take()забирает то, что лежит внутриOption, и оставляетNone.
use std::mem;
struct Inbox { unread: Vec<String>, status: String, draft: Option<String>,}
impl Inbox { // Отдаём все непрочитанные сообщения и оставляем пустой Vec. fn read_all(&mut self) -> Vec<String> { mem::take(&mut self.unread) }
// Кладём новый статус и получаем старый обратно. fn set_status(&mut self, new: &str) -> String { mem::replace(&mut self.status, new.to_string()) }
// Забираем черновик (если он есть) и оставляем None. fn send_draft(&mut self) -> Option<String> { self.draft.take() }}
fn main() { let mut inbox = Inbox { unread: vec!["Обед?".to_string(), "Счёт #42".to_string()], status: "в сети".to_string(), draft: Some("Увидимся в 6".to_string()), };
let messages = inbox.read_all(); println!("прочитано {:?}, ещё не прочитано: {:?}", messages, inbox.unread);
let old = inbox.set_status("на встрече"); println!("статус был '{old}', теперь '{}'", inbox.status);
println!("отправлено: {:?}", inbox.send_draft()); println!("отправлено снова: {:?}", inbox.send_draft());}Вывод:
прочитано ["Обед?", "Счёт #42"], ещё не прочитано: []статус был 'в сети', теперь 'на встрече'отправлено: Some("Увидимся в 6")отправлено снова: NoneНигде нет clone(): сообщения и старый статус перемещаются, а не копируются. Поэтому эти функции одновременно безопасные и дешёвые.
Набор дополняют два родственника. mem::swap(&mut a, &mut b) меняет два значения местами, как две тарелки на столе. Option::replace(new) кладёт в Option новое значение и отдаёт старое.
use std::mem;
fn main() { // Одна очередь копится, пока другая в работе. let mut filling = vec!["заказ 1", "заказ 2"]; let mut processing: Vec<&str> = Vec::new();
mem::swap(&mut filling, &mut processing); println!("в работе {processing:?}, копится {filling:?}");
// Option::replace: кладём новое значение, получаем старое. let mut now_playing = Some("Песня A"); let previous = now_playing.replace("Песня B"); println!("было {previous:?}, теперь {now_playing:?}");}Вывод:
в работе ["заказ 1", "заказ 2"], копится []было Some("Песня A"), теперь Some("Песня B")Используй, когда у тебя есть только доступ через &mut, а значением внутри нужно завладеть.
Срезы: windows, chunks и компания
Заголовок раздела «Срезы: windows, chunks и компания»Представь windows(2) как лупу, которая скользит вдоль линейки: она показывает две отметки сразу, потом сдвигается на шаг. chunks(3) — как раскладывать яйца по коробкам на три штуки: последняя коробка может оказаться неполной. chunks_exact(3) даёт только полные коробки, а оставшиеся яйца откладывает в .remainder().
Эти методы работают с любым срезом, а значит, и с массивами, и с векторами.
fn main() { let temps = [12, 15, 14, 19, 21];
// windows(2): каждая пара соседей, с перекрытием for pair in temps.windows(2) { println!("{} -> {}: изменение {}", pair[0], pair[1], pair[1] - pair[0]); }
// chunks(3): пачки по 3, последняя может быть меньше let emails = ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"]; for batch in emails.chunks(3) { println!("отправляем: {batch:?}"); }
// chunks_exact(2): только полные пачки; остаток ждёт в remainder() let seats = [1, 2, 3, 4, 5]; let pairs = seats.chunks_exact(2); let leftover = pairs.remainder(); for pair in pairs { println!("пара {pair:?}"); } println!("остаток: {leftover:?}");}Вывод:
12 -> 15: изменение 315 -> 14: изменение -114 -> 19: изменение 519 -> 21: изменение 2отправляем: ["[email protected]", "[email protected]", "[email protected]"]отправляем: ["[email protected]", "[email protected]"]пара [1, 2]пара [3, 4]остаток: [5]Ещё несколько помощников для срезов отвечают на частые вопросы без цикла. split_first() даёт первый элемент и всё остальное — как учитель, который читает первое имя в списке, а остаток списка передаёт помощнику. split_last() делает то же с конца.
fn main() { let command = ["git", "commit", "-m", "исправить опечатку"];
// split_first: первый элемент и срез с остальными if let Some((program, args)) = command.split_first() { println!("программа: {program}, аргументы: {args:?}"); }
// split_last: последний элемент и всё, что перед ним if let Some((message, _rest)) = command.split_last() { println!("сообщение: {message}"); }
println!("это git? {}", command.starts_with(&["git"])); println!("есть -m? {}", command.contains(&"-m"));
// iter().rev(): идём с конца, не меняя срез let history = ["открыть", "изменить", "сохранить"]; let undo_order: Vec<_> = history.iter().rev().collect(); println!("порядок отмены: {undo_order:?}");}Вывод:
программа: git, аргументы: ["commit", "-m", "исправить опечатку"]сообщение: исправить опечаткуэто git? trueесть -m? trueпорядок отмены: ["сохранить", "изменить", "открыть"]split_first и split_last возвращают Option, потому что у пустого среза нет первого элемента. Поэтому мы используем if let.
Используй, когда нужны соседи (windows), пачки (chunks) или «голова и всё остальное» (split_first).
Помощники итераторов, которые нужны постоянно
Заголовок раздела «Помощники итераторов, которые нужны постоянно»На странице Итераторы разобраны map, filter, sum и collect. Вот следующие инструменты из ящика.
peekable: посмотри, прежде чем взять
Заголовок раздела «peekable: посмотри, прежде чем взять»Представь, что ты раздаёшь карты. Иногда хочется взглянуть на следующую карту, прежде чем решить, брать ли её. .peekable() даёт итератор, который так умеет: .peek() показывает следующий элемент, не забирая его, а .next_if(...) забирает его, только если он проходит проверку. Это ровно то, что нужно небольшим парсерам.
fn main() { let input = "12+345-6"; let mut chars = input.chars().peekable(); let mut tokens: Vec<String> = Vec::new();
while let Some(c) = chars.next() { if c.is_ascii_digit() { let mut number = c.to_string(); // Берём следующий символ, только пока это тоже цифра. while let Some(d) = chars.next_if(|next| next.is_ascii_digit()) { number.push(d); } tokens.push(number); } else { tokens.push(c.to_string()); } }
println!("{tokens:?}");}Вывод:
["12", "+", "345", "-", "6"]Без peekable ты прочитал бы +, пока искал следующие цифры, и он бы пропал.
Используй, когда от следующего элемента зависит, что делать с текущим.
Обход списков: zip, enumerate, step_by, take_while, skip_while, chain
Заголовок раздела «Обход списков: zip, enumerate, step_by, take_while, skip_while, chain»| Метод | Представь это как | Используй, когда… |
|---|---|---|
zip |
молнию, которая соединяет две стороны зубчик за зубчиком | у тебя два списка, которые связаны друг с другом |
enumerate |
нумерацию строк в списке | кроме элемента нужен его индекс |
step_by(n) |
подъём по лестнице через две (или n) ступеньки | нужен каждый n-й элемент |
take_while |
ешь конфеты, пока не попадётся невкусная | нужно начало списка до первого «нет» |
skip_while |
проматываешь рекламу, пока не начнётся фильм | нужен остаток списка начиная с первого «нет» |
chain |
сцепку двух вагонов | нужен один список, а за ним другой |
fn main() { let names = ["Аня", "Бен", "Чэнь"]; let scores = [88, 92, 75];
// zip соединяет два списка в пары; enumerate добавляет счётчик с 0 for (i, (name, score)) in names.iter().zip(scores.iter()).enumerate() { println!("{}. {name}: {score}", i + 1); }
// step_by: каждая 15-я минута часа let quarters: Vec<u32> = (0..60).step_by(15).collect(); println!("четверти часа: {quarters:?}");
// into_iter() у массива даёт сами числа let queue = [3, 5, 8, 12, 4]; let small: Vec<i32> = queue.into_iter().take_while(|n| *n < 10).collect(); let rest: Vec<i32> = queue.into_iter().skip_while(|n| *n < 10).collect(); println!("до первого большого: {small:?}, начиная с него: {rest:?}");
// chain: один список за другим let morning = ["почта", "планёрка"]; let afternoon = ["ревью", "деплой"]; let day: Vec<_> = morning.iter().chain(afternoon.iter()).collect(); println!("{day:?}");}Вывод:
1. Аня: 882. Бен: 923. Чэнь: 75четверти часа: [0, 15, 30, 45]до первого большого: [3, 5, 8], начиная с него: [12, 4]["почта", "планёрка", "ревью", "деплой"]Заметь, что take_while останавливается на 12 навсегда. 4 после него маленькое, но его уже никто не проверяет. В этом и отличие от filter, который смотрит на каждый элемент.
Отвечаем на вопросы: fold, position, find_map, partition, flat_map
Заголовок раздела «Отвечаем на вопросы: fold, position, find_map, partition, flat_map»| Метод | Представь это как | Используй, когда… |
|---|---|---|
sum |
клавишу «плюс» на калькуляторе | нужна просто сумма |
fold |
снежный ком, который катится с горы и растёт на каждом шаге | ты собираешь один результат по своему правилу |
position |
«на каком месте сидит Бен?» | нужен индекс первого совпадения |
find_map |
перебор ключей, пока какой-то не откроет дверь | ты пробуешь что-то с каждым элементом и оставляешь первый успех |
partition |
раскладку белья по двум корзинам | нужно разделить список на два по вопросу «да/нет» |
flat_map |
высыпание нескольких пакетов на один стол | каждый элемент превращается в несколько |
fn main() { let orders = [12, 40, 7, 55, 30];
// sum: просто сложить let total: i32 = orders.iter().sum();
// fold: сложить по-своему (здесь за каждый заказ ещё и комиссия 2 евро) let with_fees = orders.iter().fold(0, |acc, price| acc + price + 2); println!("всего {total}, с комиссией {with_fees}");
// position: индекс первого совпадения println!("первый заказ дороже 50 — под индексом {:?}", orders.iter().position(|p| *p > 50));
// partition: два Vec, один для «да», другой для «нет» let (big, small): (Vec<i32>, Vec<i32>) = orders.into_iter().partition(|p| *p >= 30); println!("крупные {big:?}, мелкие {small:?}");
// find_map: пробуем разобрать каждую строку, оставляем первую удачную let inputs = ["abc", "", "42", "7"]; let first_number = inputs.iter().find_map(|s| s.parse::<i32>().ok()); println!("первое число: {first_number:?}");
// flat_map: каждая строка становится несколькими словами, все в одном списке let lines = ["купить молоко", "позвонить маме сейчас"]; let words: Vec<&str> = lines.iter().flat_map(|line| line.split(' ')).collect(); println!("{words:?}");}Вывод:
всего 144, с комиссией 154первый заказ дороже 50 — под индексом Some(3)крупные [40, 55, 30], мелкие [12, 7]первое число: Some(42)["купить", "молоко", "позвонить", "маме", "сейчас"]fold принимает начальное значение (0) и замыкание, которое получает «результат на данный момент» и следующий элемент. Если нужна просто сумма, sum скажет это понятнее. Бери fold, когда правило твоё собственное.
Наводим порядок в Vec на месте: dedup и retain
Заголовок раздела «Наводим порядок в Vec на месте: dedup и retain»Это методы Vec, а не итератора, но лежат они в том же ящике. dedup — как диджей, который никогда не ставит одну и ту же песню два раза подряд (позже песня может вернуться). retain — как охранник на входе: внутри остаются только те, кто прошёл проверку.
fn main() { // dedup убирает повторы, которые стоят рядом let mut clicks = vec!["главная", "главная", "магазин", "магазин", "магазин", "корзина", "главная"]; clicks.dedup(); println!("{clicks:?}");
// retain оставляет только элементы, прошедшие проверку let mut cart = vec![("яблоки", 3), ("хлеб", 0), ("молоко", 1), ("яйца", 0)]; cart.retain(|(_, qty)| *qty > 0); println!("{cart:?}");}Вывод:
["главная", "магазин", "корзина", "главная"][("яблоки", 3), ("молоко", 1)]Последняя "главная" уцелела, потому что рядом с ней не было другой "главная". Чтобы убрать все повторы, сначала отсортируй список, как показано на странице Сортировка.
Используй, когда нужно изменить Vec, не собирая новый.
Удобства Option и Result
Заголовок раздела «Удобства Option и Result»Option — как посылка, которая то ли пришла, то ли нет. Можно открывать каждую посылку полным match, но чаще всего то же самое говорит помощник из одного слова. На странице Option разобраны unwrap_or и match; вот следующие помощники.
fn main() { let nickname: Option<String> = None; let age: Option<u32> = Some(17); let width: Option<u32> = Some(3); let height: Option<u32> = Some(4);
// unwrap_or_default: значение или значение по умолчанию для типа ("", 0, пустой Vec...) println!("прозвище: '{}'", nickname.unwrap_or_default());
// map_or: запасной вариант для None, иначе преобразуем значение println!("{}", age.map_or("возраст неизвестен".to_string(), |a| format!("{a} лет")));
// filter: оставляем значение, только если оно проходит проверку println!("совершеннолетний возраст: {:?}", age.filter(|a| *a >= 18));
// zip: Some, только если ОБА — Some println!("площадь: {:?}", width.zip(height).map(|(w, h)| w * h));
// and_then: следующий шаг, который сам может дать None let good = Some("42").and_then(|s| s.parse::<u32>().ok()); let bad = Some("forty").and_then(|s| s.parse::<u32>().ok()); println!("разобрано: {good:?} и {bad:?}");}Вывод:
прозвище: ''17 летсовершеннолетний возраст: Noneплощадь: Some(12)разобрано: Some(42) и NoneВнутри функции ? работает и с Option: если значение — None, функция сразу возвращает None. В функции, которая возвращает Result, сначала преврати Option в Result через ok_or(error) или ok_or_else(|| error). Бери ok_or_else, когда создать ошибку стоит труда: замыкание выполняется только тогда, когда оно действительно нужно.
struct Employee { name: String, manager: Option<String>,}
// `?` у Option: если там None, сразу возвращаем None.fn manager_initial(e: &Employee) -> Option<char> { let manager = e.manager.as_ref()?; manager.chars().next()}
// ok_or_else превращает None в ошибку, и `?` работает в функции с Result.fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting.ok_or_else(|| "PORT не задан".to_string())?; raw.parse::<u16>().map_err(|e| e.to_string())}
fn main() { let ana = Employee { name: "Аня".to_string(), manager: Some("Грейс".to_string()) }; let grace = Employee { name: "Грейс".to_string(), manager: None }; println!("{}: {:?}", ana.name, manager_initial(&ana)); println!("{}: {:?}", grace.name, manager_initial(&grace));
println!("{:?}", port_from(Some("8080"))); println!("{:?}", port_from(None)); println!("{:?}", port_from(Some("eighty")));
// transpose: Option<Result<..>> становится Result<Option<..>> let retries_setting: Option<&str> = Some("3"); let retries: Result<Option<u8>, _> = retries_setting.map(|s| s.parse::<u8>()).transpose(); println!("{retries:?}");}Вывод:
Аня: Some('Г')Грейс: NoneOk(8080)Err("PORT не задан")Err("invalid digit found in string")Ok(Some(3))Строка invalid digit found in string — это сообщение самой стандартной библиотеки: «в строке найдена недопустимая цифра». transpose нужен редко, но это аккуратный ответ на ситуацию «у меня необязательная настройка, и её разбор может не удаться». С ним можно поставить ? на результат и сохранить Option.
Используй, когда у match по Option одна ветка интересная, а другая скучная.
Entry API у HashMap
Заголовок раздела «Entry API у HashMap»Представь гардероб. Ты называешь имя, а гардеробщик либо находит этот крючок, либо вешает новый, и отдаёт его тебе. Один поход, а не два. Это и есть .entry(key). На странице Хеш-таблицы показан entry(key).or_insert(value) — классический счётчик. Стоит знать ещё два варианта:
or_insert_with(f)создаёт значение по умолчанию, только если ключа нет.or_default()делает то же со значением по умолчанию для типа.and_modify(f)меняет значение, если ключ есть; добавь после негоor_insertдля случая «новый ключ».
Названия фруктов здесь английские: в конце мы сортируем ключи по алфавиту, и так вывод совпадает с английской версией урока.
use std::collections::HashMap;
fn main() { let sales = [("apples", 3), ("pears", 2), ("apples", 5), ("plums", 1), ("pears", 4)];
// and_modify + or_insert: обновить, если есть, вставить, если нет let mut totals: HashMap<&str, i32> = HashMap::new(); for (fruit, qty) in sales { totals.entry(fruit).and_modify(|t| *t += qty).or_insert(qty); }
// or_insert_with: создаём пустой Vec, только когда он нужен let mut orders: HashMap<&str, Vec<i32>> = HashMap::new(); for (fruit, qty) in sales { orders.entry(fruit).or_insert_with(Vec::new).push(qty); }
// Порядок в HashMap случайный, поэтому перед выводом сортируем ключи let mut fruits: Vec<&str> = totals.keys().copied().collect(); fruits.sort(); for fruit in fruits { println!("{fruit}: всего {}, заказы {:?}", totals[fruit], orders[fruit]); }}Вывод:
apples: всего 8, заказы [3, 5]pears: всего 6, заказы [2, 4]plums: всего 1, заказы [1]Используй, когда иначе пришлось бы писать «если ключ есть — изменить, иначе — вставить», то есть искать ключ дважды.
Тип never !: код, который не возвращается
Заголовок раздела «Тип never !: код, который не возвращается»Некоторые дороги — с односторонним движением: заехал, и обратно уже не выехать. В Rust у выражения, которое никогда не завершается, тип never («никогда»), он пишется как !. Такой тип у panic!, return, break, continue, бесконечного loop и std::process::exit.
Значение типа ! никогда не может существовать на самом деле, поэтому компилятор разрешает ему стоять на месте любого типа. Вот почему у match одна ветка может давать i32, а другая — делать return: вторая ветка никогда не выдаёт значение, так что сломать типы она не может.
use std::process;
// `-> !` значит «эта функция никогда не возвращается»fn quit(message: &str) -> ! { println!("{message}"); process::exit(0)}
fn level_points(level: &str) -> u32 { let points: u32 = match level { "easy" => 10, "hard" => 30, _ => return 0, // у `return` тип `!`, поэтому он подходит туда, где ждут u32 }; points * 2}
fn main() { println!("лёгкий: {}, сложный: {}, странный: {}", level_points("easy"), level_points("hard"), level_points("weird"));
let mut total = 0; for input in ["4", "oops", "6"] { let n: i32 = match input.parse() { Ok(n) => n, Err(_) => continue, // `continue` — тоже `!` }; total += n; } println!("всего: {total}");
let config_file: Option<&str> = None; let config = match config_file { Some(path) => path, None => quit("файл настроек не найден, останавливаемся"), }; println!("загружен {config}"); // никогда не выводится}Вывод:
лёгкий: 20, сложный: 60, странный: 0всего: 10файл настроек не найден, останавливаемсяОдно предупреждение: process::exit останавливает программу немедленно, не запуская код очистки (деструкторы) для значений, которые ещё живы. В настоящих программах вернуться из main обычно бережнее.
Два макроса используют ! как заглушки, пока ты набрасываешь код. todo!() значит «напишу потом». unimplemented!() значит «здесь это не поддерживается». Оба вызывают панику, если до них дойдёт выполнение, но позволяют остальной программе скомпилироваться:
fn export_pdf(report: &str) -> Vec<u8> { todo!("экспорт в PDF появится в v2")}Если вызвать эту функцию, получишь not yet implemented: экспорт в PDF появится в v2 («ещё не реализовано»). С unimplemented!("экспорт в PDF") сообщение будет not implemented: экспорт в PDF («не реализовано»).
Используй, когда функция должна остановить программу или ветка должна выйти раньше, а не выдать значение.
Маленькие удобства
Заголовок раздела «Маленькие удобства»Это карманные инструменты: крошечные, но пригождаются каждую неделю.
matches!(value, pattern)отвечает на вопрос «подходит ли значение под образец?» значениемtrueилиfalse(подробнее на странице Сопоставление с образцом).std::cmp::min/maxвыбирают меньшее или большее из двух значений, а.clamp(low, high)удерживает значение в диапазоне, как ручка громкости, которая упирается в 100..abs_diff(other)даёт расстояние между двумя числами. Оно всегда положительное и никогда не переполняется, даже у беззнаковых типов.dbg!(expr)выводит файл, строку, выражение и значение, а потом возвращает значение обратно, так что им можно обернуть что угодно.assert!(condition)паникует, если условие ложно.debug_assert!делает то же самое, но только в отладочных сборках, поэтому в релизной сборке ничего не стоит.
use std::cmp::{max, min};
enum Order { Pending, Shipped, Delivered, Cancelled,}
fn main() { let orders = [Order::Pending, Order::Delivered, Order::Shipped, Order::Cancelled, Order::Pending];
// matches!: проверка образца «да/нет», | означает «или» let open = orders.iter().filter(|o| matches!(o, Order::Pending | Order::Shipped)).count(); println!("открытых заказов: {open}");
// min, max и clamp println!("min {}, max {}", min(7, 3), max(7, 3)); let volume = 140; let safe = volume.clamp(0, 100); println!("громкость {volume} ограничена до {safe}");
// abs_diff: насколько далеко, никогда не отрицательно let (guess, secret): (u32, u32) = (30, 42); println!("до ответа осталось {}", guess.abs_diff(secret));
// dbg! пишет в stderr и возвращает значение let doubled = dbg!(safe * 2);
// assert! останавливает программу, если это когда-нибудь станет ложью assert!(doubled <= 200, "удвоенная громкость слишком большая: {doubled}"); debug_assert!(safe <= 100); // проверяется только в отладочных сборках println!("все проверки пройдены");}Вывод:
открытых заказов: 3min 3, max 7громкость 140 ограничена до 100до ответа осталось 12все проверки пройденыСтроки от dbg! в выводе выше нет, потому что dbg! пишет в stderr, а не в stdout. В Playground она появляется на панели «Standard Error»:
[src/main.rs:28:19] safe * 2 = 200Поэтому dbg! и можно спокойно расставлять при отладке: он не смешивается с настоящим выводом программы. Только не забудь убрать его перед релизом.
У сортировки есть свои маленькие инструменты. Подробно они разобраны на странице Сортировка, а здесь — короткая версия. sort_unstable чуть быстрее sort и отлично подходит для простых чисел. sort_by_key сортирует по одному полю. binary_search очень быстро находит элемент в отсортированных данных — как будто открываешь словарь посередине, а не читаешь с первой страницы.
struct Player { name: &'static str, score: u32,}
fn main() { // sort_unstable: чуть быстрее; равные элементы могут поменяться местами let mut ids = vec![42, 7, 19, 3, 25]; ids.sort_unstable(); println!("{ids:?}");
// binary_search: Ok(индекс), если найдено, Err(индекс) — куда бы встал println!("ищем 19: {:?}", ids.binary_search(&19)); println!("ищем 20: {:?}", ids.binary_search(&20));
// sort_by_key: сортировка по одному полю let mut players = vec![ Player { name: "Аня", score: 120 }, Player { name: "Бен", score: 95 }, Player { name: "Чэнь", score: 150 }, ]; players.sort_by_key(|p| p.score); let names: Vec<&str> = players.iter().map(|p| p.name).collect(); println!("от меньшего к большему: {names:?}");}Вывод:
[3, 7, 19, 25, 42]ищем 19: Ok(2)ищем 20: Err(3)от меньшего к большему: ["Бен", "Аня", "Чэнь"]Err(3) — не повод для беспокойства. Он говорит, что 20 в списке нет, а индекс 3 — то место, куда его можно вставить, чтобы список остался отсортированным.
Используй, когда ловишь себя на том, что пишешь маленький if или цикл, который один из этих методов уже умеет.
Частые ошибки
Заголовок раздела «Частые ошибки»mem::take для типа без Default. mem::take должен что-то оставить на месте, поэтому ему нужно знать, что значит «пусто» для этого типа:
use std::mem;
struct Connection { host: String,}
struct Pool { active: Connection,}
impl Pool { fn reset(&mut self) -> Connection { mem::take(&mut self.active) // ❌ каким был бы «пустой» Connection? }}error[E0277]: the trait bound `Connection: Default` is not satisfied --> src/main.rs:13:19 |13 | mem::take(&mut self.active) // ❌ каким был бы «пустой» Connection? | --------- ^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Connection` | | | required by a bound introduced by this call |help: consider annotating `Connection` with `#[derive(Default)]`Компилятор говорит, что Connection не реализует Default. Исправь это, выбрав замену сам: mem::replace(&mut self.active, Connection { host: "db2".to_string() }), или храни поле как Option<Connection> и используй self.active.take(). Добавляй #[derive(Default)], только если «пустое» значение для этого типа действительно имеет смысл.
? у Option в функции, которая возвращает Result. Сами по себе они не сочетаются:
fn port_from(setting: Option<&str>) -> Result<u16, String> { let raw = setting?; // ❌ у None нет сообщения об ошибке raw.parse::<u16>().map_err(|e| e.to_string())}error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result` --> src/main.rs:2:22 |1 | fn port_from(setting: Option<&str>) -> Result<u16, String> { | ---------------------------------------------------------- this function returns a `Result`2 | let raw = setting?; // ❌ у None нет сообщения об ошибке | ^ use `.ok_or(...)?` to provide an error compatible with `Result<u16, String>`Компилятор сам подсказывает исправление: setting.ok_or("PORT не задан".to_string())? или ok_or_else, как в примере выше. Другие формы этой ошибки — на странице E0277.
dedup или binary_search на неотсортированных данных. dedup убирает только повторы, которые стоят рядом, а binary_search на неотсортированных данных даёт бессмысленный ответ. Ни то ни другое не ошибка компиляции, поэтому это легко пропустить. Сначала сортируй.
Забытый остаток у chunks_exact. Цикл for по chunks_exact(3) молча пропускает последние 1–2 элемента. Если они важны, обработай .remainder() или используй chunks.
Практика
Заголовок раздела «Практика»1. Изменения цены по дням
Заголовок раздела «1. Изменения цены по дням»У тебя есть цены [100, 104, 98, 110, 107]. Собери Vec с изменением от каждого дня к следующему, выведи его, а затем выведи самый большой рост. Подсказка: windows(2) даёт каждую пару как маленький срез w, так что изменение — это w[1] - w[0]. .iter().max() находит наибольшее.
Показать решение
fn main() { let prices = [100, 104, 98, 110, 107];
let changes: Vec<i32> = prices.windows(2).map(|w| w[1] - w[0]).collect(); println!("изменения: {changes:?}");
let biggest_rise = changes.iter().max(); println!("самый большой рост: {biggest_rise:?}");}Вывод:
изменения: [4, -6, 12, -3]самый большой рост: Some(12)Пять цен дают четыре изменения, потому что windows(2) делает одну пару на каждый шаг. max() возвращает Option, ведь у пустого списка нет наибольшего значения.
2. Очищаем очередь печати
Заголовок раздела «2. Очищаем очередь печати»Напиши структуру Printer с полем queue: Vec<String>. Дай ей метод add(&mut self, file: &str) и метод flush(&mut self) -> Vec<String>, который отдаёт все файлы из очереди и оставляет её пустой. Добавь два файла, вызови flush и выведи, что было отдано и сколько файлов осталось. Подсказка: у flush есть только &mut self, поэтому используй std::mem::take.
Показать решение
use std::mem;
struct Printer { queue: Vec<String>,}
impl Printer { fn add(&mut self, file: &str) { self.queue.push(file.to_string()); }
fn flush(&mut self) -> Vec<String> { mem::take(&mut self.queue) }}
fn main() { let mut printer = Printer { queue: Vec::new() }; printer.add("report.pdf"); printer.add("photo.png");
let jobs = printer.flush(); println!("отправлено на печать ({}): {jobs:?}", jobs.len()); println!("осталось в очереди: {}", printer.queue.len());}Вывод:
отправлено на печать (2): ["report.pdf", "photo.png"]осталось в очереди: 0Если написать в flush просто self.queue, получится ошибка E0507. mem::take перемещает Vec наружу и оставляет на его месте пустой, без всякого копирования.
3. Первый подходящий порт
Заголовок раздела «3. Первый подходящий порт»Напиши функцию first_port(candidates: &[&str]) -> Option<u16>, которая возвращает первую строку, которая разбирается как u16. Вызови её с ["", "abc", "70000", "8080", "9090"] и с ["none", "-1"] и выведи порт или 3000, если подходящего нет. Подсказка: find_map с s.parse::<u16>().ok(), а затем unwrap_or(3000).
Показать решение
fn first_port(candidates: &[&str]) -> Option<u16> { candidates.iter().find_map(|s| s.parse::<u16>().ok())}
fn main() { let from_config = ["", "abc", "70000", "8080", "9090"]; let all_bad = ["none", "-1"];
println!("используем порт {}", first_port(&from_config).unwrap_or(3000)); println!("используем порт {}", first_port(&all_bad).unwrap_or(3000));}Вывод:
используем порт 8080используем порт 3000"70000" похоже на число, но в u16 не помещается (максимум — 65535), поэтому разбор не удаётся, и find_map идёт дальше. Он останавливается на "8080" и до "9090" даже не доходит.
mem::take,mem::replaceиOption::takeзабирают значение из&mut, оставляя что-то на его месте;mem::swapменяет два значения местами.- У срезов
windowsдаёт перекрывающихся соседей,chunks— пачки, аsplit_first/split_last— «один элемент и всё остальное». peekableпозволяет заглянуть вперёд;zip,enumerate,chain,fold,find_mapиpartitionзаменяют множество ручных циклов.- Помощники
Optionвродеunwrap_or_default,map_or,filterиok_orизбавляют от маленькихmatch, а?работает и сOption. entry()вместе сor_insert_withилиand_modifyобновляетHashMapза один поиск.- Тип never
!принадлежит коду, который никогда не завершается (panic!,return,continue,process::exit), поэтому он подходит везде, где ожидается значение.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что оставляет mem::take(&mut self.items), если items — это Vec?
2. Что даёт [1, 2, 3, 4].windows(2)?
3. Почему компилируется let x: i32 = match s { "a" => 1, _ => return };?
4. Функция возвращает Result, а у тебя Option. Что добавить перед ?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»std::mem— документация стандартной библиотеки (на английском):take,replace,swapи другие помощники для работы с памятью, с примерами.- Методы срезов — документация стандартной библиотеки (на английском): все методы срезов, включая
windows,chunks,split_firstиbinary_search. - Трейт
Iterator— документация стандартной библиотеки (на английском): полный список методов итератора, отzipиpeekableдоpartitionиfind_map. Option— документация стандартной библиотеки (на английском): все помощникиOption, сгруппированные по тому, что они делают.- Расходящиеся функции — Rust на примерах (перевод на русский): короткие примеры функций, которые возвращают
!, и объяснение, чем это полезно.