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
🔧 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();
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("Здравствуйте");
✏️ 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.
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
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
✅ 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!
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
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
🔤 Unicode Scalar Values (char):
['न', 'म', 'स', '्', 'त', 'े'] // 6 characters
💬 Grapheme Clusters (human-perceived letters):
["न", "म", "स्", "ते"] // 4 graphemes
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 = "Зд"
⚠️ 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!
🔁 Iterating Over Strings
.chars() — iterate by Unicode scalar values
for c in "Зд".chars() {
println!("{c}");
}
Output:
З
д
.bytes() — iterate by raw bytes
for b in "Зд".bytes() {
println!("{b}");
}
Output:
208
151
208
180
🧠 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)