DEV Community

Subesh Yadav
Subesh Yadav

Posted on • Edited on

🦀 Day 13 of #100DaysOfRust — Mastering Strings in Rust

Today was all about deeply understanding String in Rust. While strings might look simple on the surface, they’re complex under the hood — especially with Rust's strict rules and memory safety model. In this post, I’ll walk through everything I learned about Strings, their underlying concepts, and how to use them effectively and safely.

📚 What is a String in Rust?

Rust has two main types for handling text:

  • String – a growable, owned, UTF-8 encoded text type provided by the standard library.
  • &str – a borrowed string slice, typically pointing to string literals or parts of other strings.
let s = String::from("Hello"); // Growable and owned
let s_literal: &str = "Hello"; // String slice, borrowed
Enter fullscreen mode Exit fullscreen mode

🔧 Creating Strings

There are multiple ways to create a new String:

let empty = String::new(); // Empty string
let from_literal = String::from("initial contents");
let from_to_string = "initial contents".to_string();
Enter fullscreen mode Exit fullscreen mode

These all give you a valid String. Both to_string() and String::from() are commonly used — choose whichever improves your code readability.

✅ UTF-8 Support

Rust strings support Unicode characters natively:

let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("Здравствуйте");
Enter fullscreen mode Exit fullscreen mode

✏️ Updating Strings

push_str() and push()

let mut s = String::from("foo");
s.push_str("bar");  // Now s is "foobar"

let mut s2 = String::from("lo");
s2.push('l');       // Now s2 is "lol"
push_str() adds a string slice.
Enter fullscreen mode Exit fullscreen mode

push() adds a single character.

➕ Concatenating Strings

Using the + Operator

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 is moved here
Enter fullscreen mode Exit fullscreen mode

You can only concatenate String + &str.

s1 gets moved, so you can’t use it afterwards.

Using format! (Preferred for Complex Cases)

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{s1}-{s2}-{s3}"); // tic-tac-toe
Enter fullscreen mode Exit fullscreen mode

✅ format! is clean, readable, and doesn’t take ownership.

❌ Indexing into Strings

Rust does not allow accessing string characters by index like s[0].

let s = String::from("hi");
let h = s[0]; // ❌ This won't compile!
Enter fullscreen mode Exit fullscreen mode

Why?

  • Strings are UTF-8 encoded.
  • Characters (Unicode scalar values) can take multiple bytes.
  • Indexing could land in the middle of a character, causing undefined behavior.

📏 Internal Representation

Rust’s String is just a wrapper over a Vec.

let hello = String::from("Здравствуйте"); // 24 bytes long, not 12
Enter fullscreen mode Exit fullscreen mode

UTF-8 encoding means each character can take 1–4 bytes, depending on the language/script.

🧩 Bytes vs Scalar Values vs Grapheme Clusters

Using the example: "नमस्ते"

🧱 Raw Bytes (u8):

[224, 164, 168, 224, 164, 174, ...] // 18 bytes total
Enter fullscreen mode Exit fullscreen mode

🔤 Unicode Scalar Values (char):

['न', 'म', 'स', '्', 'त', 'े'] // 6 characters
Enter fullscreen mode Exit fullscreen mode

💬 Grapheme Clusters (human-perceived letters):

["न", "म", "स्", "ते"] // 4 graphemes
Enter fullscreen mode Exit fullscreen mode

Rust only provides tools for bytes and chars via .bytes() and .chars(). For grapheme clusters, external crates like unicode-segmentation are needed.

✂️ Slicing Strings

let hello = "Здравствуйте";
let s = &hello[0..4]; // ✅ s = "Зд"
Enter fullscreen mode Exit fullscreen mode

⚠️ Be careful — slicing incorrectly (e.g., breaking in the middle of a UTF-8 char) causes a runtime panic.

let s = &hello[0..1]; // ❌ Will panic!
Enter fullscreen mode Exit fullscreen mode

🔁 Iterating Over Strings

.chars() — iterate by Unicode scalar values

for c in "Зд".chars() {
    println!("{c}");
}
Enter fullscreen mode Exit fullscreen mode

Output:

З
д
Enter fullscreen mode Exit fullscreen mode

.bytes() — iterate by raw bytes

for b in "Зд".bytes() {
    println!("{b}");
}
Enter fullscreen mode Exit fullscreen mode

Output:

208
151
208
180
Enter fullscreen mode Exit fullscreen mode

🧠 Strings Are Not Simple

  • Rust doesn’t hide string complexity.
  • Strings are stored as UTF-8, with variable-length encodings.
  • There’s a trade-off: safer and more reliable behavior in exchange for slightly more up-front effort.

The good news? Rust’s standard library offers many methods like:

  • contains()
  • replace()
  • split()
  • trim()

And much more to help work with strings effectively.

✅ Summary

Today’s takeaway:

  • Rust's String type is powerful, safe, and Unicode-aware.
  • You can:
    • Create & mutate strings.
    • Concatenate with + or format!.
    • Iterate over bytes or characters.
  • Rust prevents indexing pitfalls common in other languages.
  • UTF-8 makes strings efficient but complex — Rust lets you handle it right.

Understanding strings is key to building robust Rust apps. Tomorrow, I’ll explore something a bit less complex (maybe): hash maps!

Top comments (0)