Avatar ·

Как собрать элементы итератора в массив фиксированного размера в Rust?

Привет всем! Я недавно начал изучать Rust и столкнулся с проблемой, которая кажется очень простой, но я не могу найти правильного решения.

У меня есть итератор, который возвращает последовательность чисел. Я хочу собрать все эти числа в массив [i32; N], где N известно на этапе компиляции. Например, я пишу функцию, которая принимает срез чисел и возвращает массив из первых 5 элементов:

fn first_five(data: &[i32]) -> [i32; 5] {
    data.iter().take(5).collect() // не работает!
}

Компилятор выдаёт ошибку: the trait bound `[i32; 5]: FromIterator<&i32>` is not satisfied. Я понимаю, что collect() работает с коллекциями вроде Vec, но как заставить его работать с массивами?

Я пробовал явно указывать тип: let arr: [i32; 5] = data.iter().take(5).collect(); — но это тоже не помогает. В других языках (Python, JavaScript) это делается одной строчкой, а здесь приходится писать циклы. Неужели в Rust нет простого способа?

Подскажите, как правильно собрать элементы из итератора в массив фиксированного размера без написания ручного цикла? Или это принципиально невозможно из-за системы типов?

<<>>

Проблема: массивы не реализуют FromIterator

Действительно, в Rust массивы фиксированного размера (например, [i32; 5]) не реализуют трейт FromIterator, поэтому напрямую использовать .collect() для них нельзя. Это связано с тем, что размер массива должен быть известен на этапе компиляции, а итератор может содержать любое количество элементов. Однако есть несколько элегантных решений.

Решение 1: Использование std::array::from_fn (Rust 1.55+)

Самый современный и простой способ — использовать функцию from_fn из модуля std::array. Она создаёт массив, вызывая замыкание для каждого индекса:

fn first_five(data: &[i32]) -> [i32; 5] {
    let mut iter = data.iter().copied(); // или .cloned()
    std::array::from_fn(|_| iter.next().unwrap_or(0))
}

Здесь copied() преобразует &i32 в i32, чтобы не работать со ссылками. Если элементов в итераторе меньше 5, unwrap_or(0) вернёт значение по умолчанию. Можно также использовать unwrap(), если вы уверены, что элементов достаточно.

Решение 2: .collect::>().try_into().unwrap()

Классический подход — собрать в Vec, а затем преобразовать в массив:

fn first_five(data: &[i32]) -> [i32; 5] {
    data.iter()
        .copied()
        .take(5)
        .collect::>()
        .try_into()
        .unwrap() // безопасно, т.к. мы точно взяли 5 элементов
}

Метод try_into() (из трейта TryInto) возвращает Result<[T; N], Vec>. Если длина вектора совпадает с размером массива — всё хорошо, иначе возвращается ошибка. Этот способ чуть менее эффективен из-за дополнительного выделения памяти под вектор, но очень нагляден.

Решение 3: Ручной цикл (для старых версий Rust)

Если вам нужно поддерживать старые версии языка (до 1.55), можно написать цикл:

fn first_five(data: &[i32]) -> [i32; 5] {
    let mut result = [0i32; 5];
    for (i, &val) in data.iter().take(5).enumerate() {
        result[i] = val;
    }
    result
}

Этот код явный и понятный, хотя и более многословный. Компилятор часто оптимизирует его до той же эффективности, что и первые два варианта.

Решение 4: Крейт arrayvec

Если вы работаете с массивами постоянно, рассмотрите крейт arrayvec. Он предоставляет тип ArrayVec, который ведёт себя как вектор, но хранит данные на стеке:

use arrayvec::ArrayVec;

fn first_five(data: &[i32]) -> [i32; 5] {
    data.iter()
        .copied()
        .take(5)
        .collect::>()
        .into_inner()
        .unwrap()
}

Рекомендация

Для современного Rust (1.55+) используйте std::array::from_fn — это идиоматично и эффективно. Для простых случаев подойдёт вариант с try_into(). Главное — помните, что массивы в Rust — это не динамические коллекции, а фиксированные блоки данных на стеке, поэтому преобразование всегда требует явного указания размера.

<<>>

Альтернативный подход: макрос array![] и IntoIterator

Если вы не хотите писать замыкания или использовать try_into(), можно воспользоваться макросом array![] из крейта arrayvec или написать свой простой макрос для преобразования:

macro_rules! collect_array {
    ($iter:expr; $n:expr) => {{
        let mut iter = $iter.into_iter();
        [(); $n].map(|_| iter.next().unwrap())
    }};
}

fn first_five(data: &[i32]) -> [i32; 5] {
    collect_array!(data.iter().copied().take(5); 5)
}

Этот макрос создаёт массив из единичных значений () и затем заменяет их элементами из итератора. Он работает только если итератор гарантированно содержит нужное количество элементов, иначе произойдёт паника.

Ещё один вариант — использовать .collect::<[_; 5]>() из экспериментального API (требует nightly Rust и включения фичи array_from_iter):

#![feature(array_from_iter)]

fn first_five(data: &[i32]) -> [i32; 5] {
    data.iter().copied().take(5).collect()
}

Этот синтаксис пока нестабилен, но в будущем может стать стандартным способом. Для продакшена лучше придерживаться стабильных решений из первого ответа.