Как вывести срез u8 в шестнадцатеричном виде в 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"
}
Это даёт и значение, и имя переменной, что очень удобно при отладке.
Ответов пока нет.
Войдите чтобы оставить ответ