Дженерики, стандартные трейты и ленивые итераторы

Дженерик без 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.»

Проверьте себя
1. Почему достаточно реализовать From, а Into писать не нужно?
AInto устарел и оставлен только для совместимости
BКомпилятор считает Into синонимом From на уровне синтаксиса
CВ std есть blanket impl: impl<T, U> Into<U> for T where U: From<T>
DInto генерируется derive-макросом автоматически
2. Что напечатает строка v.iter().map(|x| println!("{}", x)); без последующего потребителя?
AНичего: адаптер лишь создаёт структуру Map, next никто не вызывает
BВсе элементы вектора по одному в строке
CТолько первый элемент
DКод не скомпилируется из-за несовместимых типов
3. Сколько методов обязан реализовать тип, чтобы стать Iterator?
AОдин — next, плюс задать ассоциированный тип Item
BТри — next, size_hint и count
CНи одного, достаточно derive(Iterator)
DВсе методы трейта, реализаций по умолчанию у Iterator нет