ленивые цепочки адаптеров без промежуточных аллокацийlazy adaptor chains with no intermediate allocations
Трейт Iterator требует реализации ровно одного метода — next(); всё остальное (map, filter, fold, zip, take и десятки других) — методы по умолчанию, реализованные через повторные вызовы next(). Цепочки адаптеров ленивы: ничего не выполняется, пока их не "запускает" потребляющий метод.The Iterator trait requires implementing exactly one method — next(); everything else (map, filter, fold, zip, take, and dozens more) is a default method implemented purely in terms of repeated calls to next(). Adaptor chains are lazy: nothing runs until a consuming method drives them.
Трейт Iterator (trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }, см. associated-types.html) — единственный метод, который обязан реализовать каждый итератор; всё остальное — методы ПО УМОЛЧАНИЮ, предоставленные самим трейтом, реализованные исключительно через повторные вызовы next(). Цепочки адаптеров (v.iter().map(f).filter(g).collect()) ЛЕНИВЫ — никакая работа не выполняется и никакая промежуточная коллекция не выделяется, пока "потребляющий" метод (.collect(), .sum(), цикл for, .count() и т.д.) действительно не запускает цепочку, многократно вызывая .next().The Iterator trait (trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }, see associated-types.html) is the single method every iterator must implement — everything else is a DEFAULT method provided by the trait itself, implemented purely in terms of repeated calls to next(). Adaptor chains (v.iter().map(f).filter(g).collect()) are LAZY — no work happens, and no intermediate collection is allocated, until a consuming method (.collect(), .sum(), a for loop, .count(), etc.) actually drives the chain by calling .next() repeatedly.
struct Counter { count: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
} // .map(), .filter(), .sum() и т.д. Counter получает бесплатно, реализовав только next()// .map(), .filter(), .sum() etc. Counter gets for free just by implementing next()
Ленивость плюс агрессивная мономорфизация делают функциональный стиль по-настоящему бесплатным:Laziness plus aggressive monomorphization make functional-style code genuinely free:
Компилятор агрессивно мономорфизирует и инлайнит цепочки адаптеров итераторов: каждый .map/.filter в цепочке становится отдельной, полностью конкретной generic-структурой, оборачивающей предыдущую, а не boxed/динамической обёрткой. Длинная цепочка адаптеров обычно компилируется в код, столь же плотный, как написанный вручную цикл, без единой промежуточной аллокации.The compiler aggressively monomorphizes and inlines iterator adaptor chains: each .map/.filter in a chain becomes a distinct, fully concrete generic struct wrapping the previous one, not a boxed/dynamic wrapper. A long chain of adaptors typically compiles down to code as tight as a hand-written loop, with no intermediate allocations at all.
В наивно реализованных пайплайн-абстракциях во многих других языках функциональный стиль (map/filter/reduce) нередко несёт реальные накладные расходы на каждый этап или промежуточные аллокации. У Rust этого нет — идея "ленивого, композируемого, не аллоцирующего пайплайна" здесь встроена в язык на уровне стандартной библиотеки, а не поверх неё.In naively-implemented pipeline abstractions in many other languages, functional-style code (map/filter/reduce) often carries real per-stage overhead or intermediate allocations. Rust has none of that — the idea of a "lazy, composable, zero-allocation pipeline" is built into the language at the standard-library level, not bolted on top.
Это напрямую параллельно, и заметно раньше, идее C++20 ranges/views (ranges-basics.html), принёсшей в C++ ту же самую идею "ленивого, композируемого, не аллоцирующего view-пайплайна" — примерно через пять лет после того, как Rust 1.0 вышел с ней как с фундаментальной частью стандартной библиотеки.This directly parallels — and substantially predates — C++20's ranges/views (ranges-basics.html), which brought the identical "lazy, composable, zero-allocation view pipeline" idea to C++ roughly five years after Rust 1.0 shipped with it as a foundational part of the standard library.
В цепочке ниже .filter и .map не создают промежуточный Vec — единственное, что реально запускает итерацию, это .sum() в конце.In the chain below, .filter and .map never build an intermediate Vec — the only thing that actually drives iteration is .sum() at the end.
let total: i32 = (1..=100)
.filter(|n| n % 3 == 0)
.map(|n| n * n)
.sum(); // только здесь запускается итерация — ни один Vec не аллоцируется по пути// only here does iteration actually run — no Vec is allocated along the way
Трейт IntoIterator — это то, что позволяет работать for x in collection: сами коллекции реализуют IntoIterator, а не Iterator напрямую, и цикл for раскрывается в вызов .into_iter() с последующими повторными вызовами .next().The IntoIterator trait is what makes for x in collection work — collections implement IntoIterator, not Iterator directly, and a for loop desugars to calling .into_iter() followed by repeated calls to .next().
Благодаря инлайнингу через всю цепочку адаптеров, этот код компилируется в машинный код, эквивалентный или лучший, чем цикл с ручным if-проверкой и аккумулятором.Thanks to inlining across the whole adaptor chain, this code compiles to machine code equivalent to, or better than, a hand-written loop with a manual if-check and accumulator.
let total: i32 = (1..=100).filter(|n| n % 3 == 0).map(|n| n * n).sum();
// эквивалент вручную — компилятор инлайнит первую форму примерно в это// the hand-written equivalent — the compiler inlines the first form into roughly this
let mut total_manual = 0;
for n in 1..=100 {
if n % 3 == 0 {
total_manual += n * n;
}
}
Одна и та же ленивая цепочка материализуется в разные конкретные коллекции исключительно на основании аннотации типа у .collect().The same lazy chain materializes into different concrete collections based purely on the type annotation given to .collect().
let v: Vec<i32> = (1..5).collect();
let s: std::collections::HashSet<i32> = (1..5).collect();
let text: String = vec!['r', 'u', 's', 't'].into_iter().collect();
Ленивость здесь необходима: жадный (eager) бесконечный итератор просто зависнет или уйдёт в OOM, а ленивый безопасно отдаёт ровно столько элементов, сколько запрошено через .take(n).Laziness is essential here: an eager infinite iterator would simply hang or OOM, while a lazy one safely yields exactly as many elements as requested via .take(n).
struct Fibonacci { curr: u64, next: u64 }
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let new_next = self.curr + self.next;
self.curr = self.next;
self.next = new_next;
Some(self.curr) // никогда не возвращает None — бесконечная последовательность// never returns None — an infinite sequence
}
}
let first_ten: Vec<u64> = Fibonacci { curr: 0, next: 1 }.take(10).collect();
.zip() сопоставляет два итератора поэлементно, .enumerate() добавляет индекс к каждому элементу — два из самых часто используемых адаптеров помимо map/filter..zip() pairs up two iterators element by element, .enumerate() attaches an index to each element — two of the most commonly reached-for adaptors beyond map/filter.
let names = vec!["alice", "bob", "carol"];
let scores = vec![92, 75, 88];
for (name, score) in names.iter().zip(scores.iter()) {
println!("{}: {}", name, score);
}
for (i, name) in names.iter().enumerate() {
println!("{}: {}", i, name);
}
Цепочка адаптеров ничего не делает, пока её не потребит завершающий метод — цепочка, построенная через .map()/.filter() и никогда не потреблённая (ни .collect(), ни цикл for, ни .sum()), просто молча ничего не выполняет. Это реальный, неочевидный класс багов "почему мой map с побочным эффектом ничего не делает", когда замыкание в .map() используется ради побочного эффекта, а не возвращаемого значения — идиоматичное решение — .for_each() или обычный цикл for вместо непотреблённого .map().Iterator adaptor chains do ABSOLUTELY NOTHING until a consuming method is called — a chain built with .map()/.filter() and never consumed (no .collect(), no for loop, no .sum()) simply does nothing at all, silently. This produces a real and non-obvious class of "why isn't my side-effecting map doing anything" bugs when a closure passed to .map() is used purely for its side effects rather than its return value — the fix/idiom is .for_each() or a plain for loop instead of an unconsumed .map().
Целевой тип .collect() выводится из контекста (аннотация типа, объявленный тип возврата функции или turbofish ::<Vec<_>>()) — без какого-либо контекста для вывода одиночный .collect() просто не компилируется ("type annotations needed"), что удивляет тех, кто ожидает от обычно превосходного вывода типов Rust способности "просто догадаться" по типу элемента итератора (это невозможно в общем случае, потому что множество разных типов коллекций могут одинаково валидно собрать тот же тип элементов)..collect()'s target type is inferred from context (a type annotation, a function's declared return type, or turbofish ::<Vec<_>>()) — with no context at all to infer from, .collect() alone doesn't compile ("type annotations needed"), which surprises people expecting Rust's usually-excellent type inference to "just figure it out" from the iterator's element type alone (it can't, because many different collection types could equally validly collect the same element type).
Итерация for x in &collection отдаёт ссылки (&T) на элементы (под капотом вызывается .iter()); for x in collection (без &) ПЕРЕМЕЩАЕТ коллекцию и отдаёт владеющие значения T (вызывается .into_iter()); for x in &mut collection отдаёт &mut T (вызывается .iter_mut()) — эти три формы выглядят почти одинаково синтаксически, но имеют совершенно разные последствия для владения, и выбор не той формы — частый источник путаницы "почему моя коллекция переместилась / почему я не могу использовать её после этого цикла" у новичков.Iterating with for x in &collection yields references (&T) to elements (calling .iter() under the hood); for x in collection (no &) MOVES the collection and yields owned T values (calling .into_iter()); for x in &mut collection yields &mut T (calling .iter_mut()) — these three forms look nearly identical syntactically but have entirely different ownership implications, and picking the wrong one is a common source of "why did my collection get moved/why can't I use it after this loop" confusion for newcomers.