Дженерики, стандартные трейты и ленивые итераторы
Дженерик без trait bound бесполезен, а итератор без потребителя вообще ничего не делает — на этих двух фактах ломается больше кандидатов, чем на лайфтаймах.
Trait bound — требование вида
T: Display, ограничивающее параметр типа набором доступных операций.
Вопрос: «Как ограничить дженерик и что такое where?»
В теле дженерик-функции компилятор знает о типе ровно то, что записано в bound, — и ни байтом больше. Это принципиальное отличие от шаблонов C++, где ошибка всплывает только при инстанцировании.
Что на самом деле проверяет интервьюер
Понимаете ли вы, что проверка идёт по объявлению функции, а не по месту вызова, и умеете ли читать E0277.
fn print_pair<T>(a: T, b: T) {
println!("{} и {}", a, b);
}
Ошибка компиляции:
error[E0277]: `T` doesn't implement `std::fmt::Display`
--> src/main.rs:2:25
|
2 | println!("{} и {}", a, b);
| ^ `T` cannot be formatted with the default formatter
|
= note: in format strings you may be able to use `{:?}` (or {:#?}) instead
help: consider restricting type parameter `T`
|
1 | fn print_pair<T: std::fmt::Display>(a: T, b: T) {
| +++++++++++++++++++
Исправление ровно такое, как предлагает компилятор. Когда ограничений становится больше двух, их выносят в блок where — сигнатура остаётся читаемой.
use std::fmt::Display;
fn report<T, K>(items: &[T], key: K) -> String
where
T: Display + Clone,
K: Display,
{
format!("{}: {} шт.", key, items.len())
}
Пара напоминаний из предыдущих разделов: Clone — явное копирование через .clone(), возможно дорогое; Copy — побитовое копирование при присваивании, доступно только типам без владения кучей. Debug печатается через {:?} и обычно берётся из derive; Display печатается через {}, пишется руками и означает «представление для пользователя».
Вопрос: «Зачем нужны From и Into и почему хватает реализовать только From?»
From описывает непадающее преобразование одного типа в другой.
struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let f: Fahrenheit = Celsius(100.0).into();
println!("{:.1}", f.0);
}
Вывод:
212.0
Что на самом деле проверяет интервьюер
Знаете ли вы про blanket impl — реализацию трейта сразу для всех типов, удовлетворяющих условию. В стандартной библиотеке записано примерно так:
impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
}
Отсюда правило: реализуем только From, а Into приезжает бесплатно. Обратное неверно — написать свой Into вы не сможете, он конфликтует с blanket impl. В сигнатурах при этом удобнее impl Into<String>, потому что такой аргумент примет и &str, и String.
Для преобразований, которые могут провалиться, есть TryFrom с ассоциированным типом ошибки и парный TryInto.
use std::convert::TryFrom;
fn main() {
println!("{:?}", u8::try_from(200i32));
println!("{:?}", u8::try_from(300i32).is_err());
}
Вывод:
Ok(200) true
Ещё одно место, где From окупается: оператор ? сам вызывает From::from для типа ошибки, поэтому одна реализация impl From<io::Error> for MyError убирает десятки ручных map_err.
Вопрос: «Почему этот код с map ничего не печатает?»
fn main() {
let v = vec![1, 2, 3];
v.iter().map(|x| println!("вижу {}", x));
println!("готово");
}
Вывод:
готово
И предупреждение компилятора, которое и есть ответ на вопрос:
warning: unused `std::iter::Map` that must be used
--> src/main.rs:3:5
|
3 | v.iter().map(|x| println!("вижу {}", x));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
Что на самом деле проверяет интервьюер
Понимаете ли вы устройство трейта Iterator. У него один обязательный метод и один ассоциированный тип, а десятки остальных методов — методы по умолчанию, выраженные через next.
struct Countdown(u32);
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.0 == 0 {
None
} else {
let value = self.0;
self.0 -= 1;
Some(value)
}
}
}
fn main() {
let all: Vec<u32> = Countdown(3).collect();
let odd_sum: u32 = Countdown(3).filter(|n| n % 2 == 1).sum();
println!("{:?} {}", all, odd_sum);
}
Вывод:
[3, 2, 1] 4
Адаптеры map, filter, take, skip не вычисляют ничего: они возвращают новую структуру (Map, Filter, Take), которая помнит исходный итератор и замыкание. Работа начинается только когда кто-то вызовет next, — а это делают потребители: collect, sum, count, fold, for_each и цикл for. Побочный эффект внутри map без потребителя просто не случится; для эффектов есть for_each или обычный for.
Turbofish и вывод типа
collect умеет собрать во что угодно, реализующее FromIterator, поэтому целевой тип надо назвать — либо аннотацией переменной, либо turbofish.
let squares = (1..=4).map(|n| n * n).collect::<Vec<_>>();
println!("{:?}", squares);
Вывод:
[1, 4, 9, 16]
Подчёркивание в Vec<_> означает «тип элемента выведи сам». Тот же collect умеет собирать в String, HashMap и даже в Result<Vec<_>, E>, останавливаясь на первой ошибке.
Почему это zero-cost
Цепочка адаптеров — вложенные обобщённые структуры, известные на этапе компиляции. После мономорфизации и инлайнинга v.iter().filter(...).map(...).sum() превращается в один цикл без промежуточных коллекций и без косвенных вызовов — ровно в то, что вы написали бы руками.
Типичные ошибки кандидатов
- Писать
mapради побочного эффекта и не понимать, почему ничего не произошло. - Игнорировать предупреждение
unused ... that must be used— оно прямо называет причину. - Реализовывать
IntoвместоFromи упираться в конфликт с blanket impl. - Использовать
Fromдля преобразований, которые могут провалиться, вместоTryFrom. - Считать, что bound проверяется в месте вызова: в Rust он проверяется по сигнатуре, поэтому ошибка приходит в объявление функции.
- Верить, что цепочка из пяти адаптеров создаёт пять промежуточных векторов.
Как ответить кратко
«Дженерик ограничивается trait bound: <T: Display + Clone> или блок where, если условий много; без bound компилятор ничего о типе не знает и выдаёт E0277. From — непадающее преобразование; Into реализовывать не нужно, потому что в std есть blanket impl impl<T, U> Into<U> for T where U: From<T>; для падающих есть TryFrom, и оператор ? опирается на From для ошибок. У трейта Iterator обязателен только next, всё остальное — методы по умолчанию. Итераторы ленивы: map и filter лишь строят структуру, работа начинается на потребителе — collect, sum, for. Поэтому map без потребителя ничего не печатает, а компилятор предупреждает про lazy iterators.»