Converting Integer to String in Rust: Magic Without unwrap and Macros

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why can't you just add a number to a string in Rust?

\n\n

If you come from Python or JavaScript, you're used to writing something like str(42) or 42 + "". That trick won't work in Rust. The compiler will strictly ask: "What type do you want? Integer? String? Make up your mind!"

\n\n

The question "How do I convert from an integer to a string?" is one of the most popular on Stack Overflow in the Rust section. Thousands of developers scratch their heads wondering why their code doesn't compile when they try to write:

\n\n
let number = 42;\nlet text = number; // Error: expected `&str`, found `i32`
\n\n

Today we'll look under the hood of this conversion. You'll learn not only how to do it, but also how it works at the level of traits, allocations, and even assembly. Let's go!

\n\n

The Simplest Way: The to_string() Method

\n\n

In Rust, as in most modern languages, integer types have a to_string() method. It looks deceptively simple:

\n\n
fn main() {\n    let num: i32 = 255;\n    let s: String = num.to_string();\n    println!("{}", s); // Outputs: "255"\n}
\n\n

This works for all integer types: i8, u32, i64, usize, etc.

\n\n

What happens under the hood?

\n\n

The to_string() method is not compiler magic. It is implemented through the Display trait. In the Rust standard library, there is an implementation:

\n\n
// Simplified version from the standard library\nimpl<T: fmt::Display> ToString for T {\n    #[inline]\n    fn to_string(&self) -> String {\n        use fmt::Write;\n        let mut buf = String::new();\n        // Here the fmt() method of the Display trait is called\n        let _ = buf.write_fmt(format_args!("{}", self));\n        buf.shrink_to_fit(); // Memory optimization\n        buf\n    }\n}
\n\n

The key point: to_string() creates a new String on the heap. Each call is a memory allocation. For hundreds of thousands of conversions, this can become a performance bottleneck.

\n\n

Alternative: The format! Macro

\n\n

The second popular way is the format! macro. It works like println!, but instead of outputting to the console, it returns a String:

\n\n
fn main() {\n    let num = 1024;\n    let s = format!("{}", num);\n    println!("String length: {}", s.len()); // 4\n}
\n\n

The difference between to_string() and format! is minimal. to_string() internally calls format_args! and write_fmt. Essentially, it's the same thing. The choice is a matter of style and readability.

\n\n

When to use format!?

\n
    \n
  • When you need to insert a number in the middle of a string: format!("Number: {}", num)
  • \n
  • When the format is complex (hexadecimal, with separators): format!("{:#x}", 255) → "0xff"
  • \n
  • When code expressiveness is important
  • \n
\n\n

Display vs ToString Trait: Which is Faster?

\n\n

If you are writing a library or high-performance code, use the Display trait directly via write! into a buffer. This allows you to reuse already allocated memory:

\n\n
use std::fmt::Write;\n\nfn main() {\n    let num = 42;\n    let mut buffer = String::with_capacity(20); // Pre-allocate memory\n\n    // Write the number into the buffer\n    write!(buffer, "{}", num).unwrap();\n    println!("{}", buffer); // "42"\n\n    // Clear and reuse\n    buffer.clear();\n    write!(buffer, "{}", 256).unwrap();\n    println!("{}", buffer); // "256"\n}
\n\n

This approach provides up to 2-3x performance improvement in loops where you need to repeatedly convert numbers to strings, because memory allocation occurs

Blogs

Book Recommendations