Выводим Vec на печать в Rust: от новичка до профи

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

«Как вывести Vec?» — вопрос, который задавал каждый Rust-разработчик

Если вы когда-нибудь писали на Rust и пытались просто вывести вектор на экран, то наверняка сталкивались с этим:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    println!("{}", numbers); // Ошибка!
}

Компилятор встречает вас красным сообщением: `Vec<{integer}>` doesn't implement `std::fmt::Display`. Знакомо? Этот вопрос на Stack Overflow собрал тысячи просмотров, и неспроста. В Rust нет «магии» — компилятор требует указать, как именно вы хотите отобразить коллекцию. И это прекрасно, потому что даёт полный контроль.

В этой статье мы разберём все основные способы вывода Vec на печать в Rust: от быстрого отладочного вывода до создания красивого форматирования для пользователя. Вы узнаете, какой метод когда использовать, и перестанете бояться ошибок компиляции.

Почему Vec не реализует Display?

Прежде чем прыгать в код, давайте поймём философию Rust. Трейт Display предназначен для отображения данных пользователю — так, как вы хотите их видеть в интерфейсе. А Debug — для отладки и логов.

Для вектора невозможно угадать, какой формат нужен вам:

  • Вывести элементы через запятую?
  • Каждый элемент на новой строке?
  • В квадратных скобках, как массив?
  • Или в виде таблицы?

Поэтому разработчики Rust решили не навязывать единый формат, а оставить выбор за программистом. Давайте посмотрим, какие варианты у нас есть.

Способ 1: Быстрый отладочный вывод (Debug)

Самый простой способ — использовать {:?} или {:#?} (pretty-print). Для этого тип элементов вектора должен реализовывать трейт Debug.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    println!("{:?}", numbers);    // [1, 2, 3, 4, 5]
    println!("{:#?}", numbers);   // Красивый вывод:
                                  // [
                                  //     1,
                                  //     2,
                                  //     3,
                                  //     4,
                                  //     5
                                  // ]
}

Когда использовать: отладка, логирование, быстрая проверка. Для продакшена обычно не подходит — формат негибкий.

Важно: если ваш тип не реализует Debug, компилятор подскажет. Для своих структур можно добавить #[derive(Debug)]:

#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let points = vec![Point { x: 1.0, y: 2.0 }, Point { x: 3.0, y: 4.0 }];
    println!("{:?}", points);
}

Способ 2: Ручной перебор с итератором

Самый гибкий способ — написать свой цикл. Вы получаете полный контроль над форматом.

fn main() {
    let words = vec!["hello", "world", "rust"];
    
    // Вариант 1: через запятую
    for (i, word) in words.iter().enumerate() {
        if i > 0 {
            print!(", ");
        }
        print!("{}", word);
    }
    println!(); // hello, world, rust

    // Вариант 2: каждый с новой строки
    for word in &words {
        println!("- {}", word);
    }
    // - hello
    // - world
    // - rust
}

Этот подход даёт максимальную гибкость, но требует больше кода. Отлично подходит, когда нужно уникальное форматирование.

Способ 3: Использование итератора с collect и join

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

fn main() {
    let words = vec!["hello", "world", "rust"];
    let joined = words.join(", ");
    println!("{}", joined); // hello, world, rust
}

Это работает только для строк и типов, которые можно преобразовать в строку. Для чисел придётся сначала преобразовать:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let joined: Vec<String> = numbers.iter().map(|n| n.to_string()).collect();
    println!("{}", joined.join(", ")); // 1, 2, 3, 4, 5
}

Способ 4: Кастомный Display для своего типа

Если вы часто выводите один и тот же тип вектора, можно реализовать Display для обёртки:

use std::fmt;

struct PrettyVec(Vec<i32>);

impl fmt::Display for PrettyVec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[")?;
        for (i, item) in self.0.iter().enumerate() {
            if i > 0 {
                write!(f, ", ")?;
            }
            write!(f, "{}", item)?;
        }
        write!(f, "]")
    }
}

fn main() {
    let v = PrettyVec(vec![10, 20, 30]);
    println!("{}", v); // [10, 20, 30]
}

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

Способ 5: Использование крейта itertools

Если вы работаете с Rust профессионально, вам пригодится крейт itertools. Он добавляет метод join для любого итератора:

use itertools::Itertools;

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let result = numbers.iter().join(" | ");
    println!("{}", result); // 1 | 2 | 3 | 4 | 5
}

Добавьте itertools = "0.12" в Cargo.toml. Это элегантный способ, который часто используется в реальных проектах.

Способ 6: Pretty-print для вложенных структур

Когда у вас вектор векторов или сложная структура, {:#?} может быть неидеален. Можно использовать крейт pretty или написать свой рекурсивный вывод:

fn print_nested(vec_of_vecs: &[Vec<i32>]) {
    for (i, inner) in vec_of_vecs.iter().enumerate() {
        print!("Row {}: ", i);
        for (j, val) in inner.iter().enumerate() {
            if j > 0 { print!(", "); }
            print!("{}", val);
        }
        println!();
    }
}

fn main() {
    let matrix = vec![
        vec![1, 2, 3],
        vec![4, 5, 6],
        vec![7, 8, 9],
    ];
    print_nested(&matrix);
    // Row 0: 1, 2, 3
    // Row 1: 4, 5, 6
    // Row 2: 7, 8, 9
}

                
                

Блоги

Рекомендаций по книгам