Введение: почему в 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 генерирует машинный код, который:
- Проверяет знак — если число отрицательное, добавляет '-' в начало.
- Делит число на 10 — извлекает последнюю цифру как остаток от деления.
- Конвертирует цифру в ASCII — добавляет 48 (код '0') к числу.
- Повторяет шаги 2-3, пока число не станет 0.
- Переворачивает строку — потому что цифры извлекаются с конца.
- Аллоцирует память на куче под итоговую строку.
Для числа 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:
Блоги
Рекомендаций по книгам
Читайте также
Работа с базами данных в JavaScript: PostgreSQL и MongoDB на Node.js