Конвертация integer в string в Rust: магия без unwrap и макросов

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

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

Начать курс

Введение: почему в Rust нельзя просто так взять и сложить число со строкой?

Если вы пришли из Python или JavaScript, то привыкли писать что-то вроде str(42) или 42 + "". В Rust такой фокус не пройдёт. Компилятор строго спросит: "Ты какой тип хочешь получить? Integer? String? Решайся!"

Вопрос "How do I convert from an integer to a string?" — один из самых популярных на Stack Overflow в разделе Rust. Тысячи разработчиков ломают голову, почему их код не компилируется, когда они пытаются написать:

let number = 42;
let text = number; // Ошибка: expected `&str`, found `i32`

Сегодня мы заглянем под капот этого преобразования. Вы узнаете не только как это сделать, но и как это работает на уровне трейтов, аллокаций и даже ассемблера. Поехали!

Самый простой способ: метод to_string()

В Rust, как и в большинстве современных языков, у целочисленных типов есть метод to_string(). Выглядит он обманчиво просто:

fn main() {
    let num: i32 = 255;
    let s: String = num.to_string();
    println!("{}", s); // Выведет: "255"
}

Это работает для всех целочисленных типов: i8, u32, i64, usize и т.д.

Что происходит под капотом?

Метод to_string() — это не магия компилятора. Он реализован через трейт Display. В стандартной библиотеке Rust есть реализация:

// Упрощённая версия из стандартной библиотеки
impl<T: fmt::Display> ToString for T {
    #[inline]
    fn to_string(&self) -> String {
        use fmt::Write;
        let mut buf = String::new();
        // Здесь вызывается метод fmt() трейта Display
        let _ = buf.write_fmt(format_args!("{}", self));
        buf.shrink_to_fit(); // Оптимизация памяти
        buf
    }
}

Ключевой момент: to_string() создаёт новую String на куче (heap). Каждый вызов — это аллокация памяти. Для сотен тысяч преобразований это может стать узким местом производительности.

Альтернатива: макрос format!

Второй популярный способ — макрос format!. Он работает как println!, но вместо вывода в консоль возвращает String:

fn main() {
    let num = 1024;
    let s = format!("{}", num);
    println!("Длина строки: {}", s.len()); // 4
}

Разница между to_string() и format! минимальна. to_string() внутри вызывает format_args! и write_fmt. По сути, это одно и то же. Выбор — вопрос стиля и читаемости.

Когда использовать format!?

  • Когда нужно вставить число в середину строки: format!("Число: {}", num)
  • Когда формат сложный (шестнадцатеричный, с разделителями): format!("{:#x}", 255) → "0xff"
  • Когда важна выразительность кода

Трейт Display vs ToString: что быстрее?

Если вы пишете библиотеку или высоконагруженный код, используйте напрямую трейт Display через write! в буфер. Это позволяет переиспользовать уже выделенную память:

use std::fmt::Write;

fn main() {
    let num = 42;
    let mut buffer = String::with_capacity(20); // Заранее выделяем память

    // Пишем число в буфер
    write!(buffer, "{}", num).unwrap();
    println!("{}", buffer); // "42"

    // Очищаем и используем снова
    buffer.clear();
    write!(buffer, "{}", 256).unwrap();
    println!("{}", buffer); // "256"
}

Этот подход даёт до 2-3x прирост производительности в циклах, где нужно многократно конвертировать числа в строки, потому что аллокация памяти происходит только один раз.

Разбор на уровне железа: что делает процессор?

Когда вы пишете 255.to_string(), Rust генерирует машинный код, который:

  1. Проверяет знак — если число отрицательное, добавляет '-' в начало.
  2. Делит число на 10 — извлекает последнюю цифру как остаток от деления.
  3. Конвертирует цифру в ASCII — добавляет 48 (код '0') к числу.
  4. Повторяет шаги 2-3, пока число не станет 0.
  5. Переворачивает строку — потому что цифры извлекаются с конца.
  6. Аллоцирует память на куче под итоговую строку.

Для числа 255 это выглядит так:

  • 255 % 10 = 5 → '5'
  • 25 % 10 = 5 → '5'
  • 2 % 10 = 2 → '2'
  • Результат: ['5', '5', '2'] → переворачиваем → "255"

В Rust этот код оптимизирован: используется деление на константу (компилятор заменяет деление на умножение и сдвиг), а переворот строки делается на месте, без дополнительной аллокации.

Специальные случаи: BigInt, отрицательные числа и no_std

Отрицательные числа

Метод to_string() корректно обрабатывает знак:

let neg = -128i8;
println!("{}", neg.to_string()); // "-128"

Под капотом: если число отрицательное, оно сначала превращается в положительное (через wrapping_neg()), в начало строки добавляется '-', а затем конвертируется модуль числа.

BigInt и огромные числа

Если вам нужно конвертировать числа больше u128 (например, для криптографии), используйте крейт num:

use num::BigInt;

fn main() {
    let big = BigInt::from(10u32).pow(100);
    let s = big.to_string();
    println!("{}", s); // 1 с сотней нулей
}

Реализация Display для BigInt использует алгоритм «разделяй и властвуй» для ускорения конвертации огромных чисел.

no_std окружение

В embedded-разработке (без стандартной библиотеки) to_string() недоступен. Используйте core::fmt::Write:

Блоги

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