Avatar ·

Как вывести срез u8 в шестнадцатеричном виде в Rust?

Привет, коллеги! Я новичок в Rust и пытаюсь разобраться с отображением байтовых данных. У меня есть срез `&[u8]`, например, массив байтов, полученный после чтения файла или хеширования. Мне нужно вывести его в консоль в виде привычной шестнадцатеричной строки, как это делается в других языках (например, `binascii.hexlify` в Python или `%02x` в C). Проблема в том, что простой `println!("{:?}", slice)` выводит что-то вроде `[72, 101, 108, 108, 111]` — это десятичные числа, а мне нужна строка типа `48656c6c6f`. Я пробовал использовать `{:X}` или `{:02x}` в форматной строке, но это не работает для срезов напрямую. Вот пример кода, который у меня есть: ```rust fn main() { let data: &[u8] = &[72, 101, 108, 108, 111]; // "Hello" в ASCII // Как вывести data как "48656c6c6f"? println!("{:?}", data); // Выводит [72, 101, 108, 108, 111] } ``` Я знаю, что можно написать цикл и вручную форматировать каждый байт, но это выглядит громоздко. Есть ли в стандартной библиотеке или популярных крейтах более элегантный способ? Мне нужно простое и идиоматичное решение для Rust. Заранее спасибо за помощь! <<>>

Использование крейта hex (рекомендуемый способ)

Самый простой и идиоматичный способ — добавить в проект популярный крейт hex. Он предоставляет функции encode и decode для работы с шестнадцатеричным представлением.

Добавьте зависимость в Cargo.toml:

[dependencies]
hex = "0.4"

Теперь ваш код станет очень лаконичным:

use hex;

fn main() {
    let data: &[u8] = &[72, 101, 108, 108, 111];
    let hex_string = hex::encode(data);
    println!("{}", hex_string); // Выведет: 48656c6c6f
}

Решение без внешних крейтов (только стандартная библиотека)

Если вы хотите избежать зависимостей, можно использовать итераторы и write! макрос. Современный Rust позволяет сделать это элегантно:

fn main() {
    let data: &[u8] = &[72, 101, 108, 108, 111];
    
    let hex_string: String = data.iter()
        .map(|b| format!("{:02x}", b))
        .collect();
    
    println!("{}", hex_string); // 48656c6c6f
}

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

Форматирование с разделителями

Часто требуется выводить hex с разделителями, например, для MAC-адресов или UUID:

fn format_hex_with_delimiter(data: &[u8], delimiter: &str) -> String {
    data.iter()
        .map(|b| format!("{:02x}", b))
        .collect::()
        .join(delimiter)
}

fn main() {
    let mac = &[0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E];
    println!("{}", format_hex_with_delimiter(mac, ":")); // 00:1a:2b:3c:4d:5e
}

Производительность

Для высокопроизводительных сценариев (например, логирование больших бинарных данных) крейт hex использует предвычисленные таблицы и SIMD-инструкции, что делает его в 2-5 раз быстрее, чем реализация через format!. Для большинства повседневных задач разница незаметна, но если вы работаете с сотнями мегабайт данных — используйте hex.

Вывод строки напрямую (без создания промежуточной строки)

Если вам не нужно сохранять hex-строку, а только вывести её в консоль, можно избежать аллокации:

fn print_hex(data: &[u8]) {
    for byte in data {
        print!("{:02x}", byte);
    }
    println!();
}

fn main() {
    let data = &[72, 101, 108, 108, 111];
    print_hex(data); // 48656c6c6f
}

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

Итоговые рекомендации

  • Для простоты и читаемости: используйте крейт hex.
  • Для минимализма (без зависимостей): используйте iter().map() + format!.
  • Для максимальной производительности: используйте крейт hex с буферизированным выводом.
  • Для вывода без аллокаций: используйте прямой цикл с print!.
<<>>

Альтернативный подход: использование трейта std::fmt::LowerHex

Можно реализовать собственный тип-обёртку, который реализует трейт std::fmt::LowerHex для среза &[u8]. Это позволит использовать стандартный синтаксис форматирования {:x}:

use std::fmt;

struct HexSlice<'a>(&'a [u8]);

impl<'a> fmt::LowerHex for HexSlice<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for byte in self.0 {
            write!(f, "{:02x}", byte)?;
        }
        Ok(())
    }
}

fn main() {
    let data = &[72, 101, 108, 108, 111];
    println!("{:x}", HexSlice(data)); // 48656c6c6f
}

Этот подход особенно удобен, если вам нужно много раз выводить hex-представление в разных местах кода. Вы можете добавить метод-хелпер или использовать типаж HexDisplay из крейта hex.

Для отладки: расширение для Debug

Если вы часто выводите байты при отладке, можно использовать крейт hex вместе с макросом dbg!:

use hex;

fn main() {
    let data = &[72, 101, 108, 108, 111];
    dbg!(hex::encode(data)); // [src/main.rs:4] hex::encode(data) = "48656c6c6f"
}

Это даёт и значение, и имя переменной, что очень удобно при отладке.