Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
121: String Slicing and UTF-8 Boundaries
A few years ago, I was reviewing a PR for a teammate who was building a "preview" snippet for a blog engine. The logic was simple: take the first 100 characters of the post and add an ellipsis. It worked perfectly in local testing with English text. But the moment it hit production and a user posted a review in Japanese—or even just used a single emoji—the entire worker thread panicked and crashed. The error was a classic panic at 'byte index 100 is not a char boundary'. He was devastated because he'd tested it for hours; he just hadn't tested it with non-ASCII characters.
The Byte-Index Trap
In many languages, you can treat a string like an array of characters. In Rust, that's a dangerous assumption. A String or a &str is a sequence of UTF-8 encoded bytes. While an ASCII character like 'a' takes up exactly one byte, a character like '🦀' takes up four.
When you slice a string using the &s[start..end] syntax, you aren't telling Rust to give you the 1st through 10th characters; you're telling it to give you the 1st through 10th bytes. If your range ends in the middle of a multi-byte character, Rust refuses to give you an invalid UTF-8 sequence and panics immediately. I've always found this frustrating at first, but it's actually Rust saving you from creating "mojibake"—that garbled text you see when encoding goes wrong.
let heart_eye = "😍 Rust";
// This looks like it should work to get the emoji,
// but "😍" is 4 bytes long.
let slice = &heart_eye[0..1]; // 💥 PANIC: byte index 1 is not a char boundary
Navigating Characters Safely
If you need to slice a string based on the actual characters (what we call "Unicode Scalar Values"), you have to stop thinking in terms of indices and start thinking in terms of iterators. The .chars() method is your best friend here. It lets you step through the string one character at a time, regardless of how many bytes that character occupies.
However, if you actually need the byte indices to perform a slice—perhaps for performance reasons—you should use .char_indices(). This returns a tuple containing the byte position and the character itself. By iterating through this, you can find the exact byte boundary where a character ends.
Here is how I usually handle safe truncation. Instead of guessing the index, I use the iterator to find the boundary:
let text = "🦀 Rustacean";
let max_chars = 4;
let end_index = text
.char_indices()
.nth(max_chars)
.map(|(idx, _)| idx)
.unwrap_or(text.len());
let safe_slice = &text[..end_index];
println!("{}", safe_slice); // Prints "🦀 Ru"
Notice what happened there: I didn't just add 4 to the index. I asked the iterator to find the byte position of the 4th character. If the string is shorter than the limit, unwrap_or(text.len()) ensures we just take the whole thing without crashing. It's a bit more verbose than a simple array slice, but it's the only way to sleep soundly at night when your users are global.
📋 Practical Task
Building a UTF-8 Aware User Bio Truncator
You are building a social media profile page. Users can enter a bio of any length, but the UI only has room for a short preview. You need to write a function that truncates a string to a specific number of characters and appends "..." if it was truncated, without ever crashing on multi-byte characters.
Requirements:
- Create a function
truncate_bio(bio: &str, max_chars: usize) -> String. - The function must count characters, not bytes.
- If the bio is longer than
max_chars, truncate it and append "...". - If the bio is shorter than or equal to
max_chars, return it as-is. - Test your function with the following input:
"Rust is 🦀-tastic!"with amax_charsof 10. It should output"Rust is 🦀-..."(Note: the emoji counts as one character).
There are no comments for now.