← thecodex.expert · The Codex Family of Knowledge
Rust

String Handling

Rust does not let you write my_string[0] to get a character — not as an oversight, but because a byte index cannot always identify a full character in UTF-8.

Rust 1.95+ Guaranteed valid UTF-8 Last verified:
Where this bites you

split(',') splits at every comma, so a string that ends with the separator ends with an empty piece — which is easy to forget, because data with a trailing delimiter looks completely normal. The empty string flows on as a perfectly valid element until something tries to use it, and then the failure shows up far from the cause.

Rustsplit_bug.rs
let line = "10,20,30,";
let parts: Vec<&str> = line.split(',').collect();
println!("{:?} ({} parts)", parts, parts.len());   // ["10", "20", "30", ""] (4 parts)

let total: i32 = parts.iter().map(|p| p.parse::<i32>().unwrap()).sum();
// panicked: called `Result::unwrap()` on an `Err` value: ParseIntError { kind: Empty }
Canonical Definition

A Rust String is guaranteed to be valid UTF-8 at every point in the type system — there is no way to construct an invalid one without deliberately bypassing the safety checks. Because a UTF-8 character can occupy 1 to 4 bytes, direct integer indexing (s[0]) is intentionally not supported, since it could split a multi-byte character and silently return nonsense.

Why you can't index a String by position

This is intentional, not a missing feature — the compiler refuses to compile code that could silently misbehave on non-ASCII text.

Rustno_indexing.rs
let s = String::from("héllo");
// let c = s[0];   // COMPILE ERROR: String cannot be indexed by integer

// use slicing on BYTE ranges instead, at your own risk of splitting a character:
let slice = &s[0..1];   // "h" — fine here, but risky in general on non-ASCII

Iterating characters correctly with .chars()

.chars() gives an iterator over Unicode scalar values (essentially, characters), correctly decoding multi-byte UTF-8 — this is the right way to work character-by-character, mirroring the range-over-string behavior seen in Go's strings-and-runes topic.

Rustchars.rs
let s = String::from("héllo");

for c in s.chars() {
    print!("{} ", c);   // h é l l o — each printed once, correctly decoded
}

println!("{}", s.chars().count());   // 5, the real character count
println!("{}", s.len());              // 6, the BYTE length (é is 2 bytes)

Common string methods

String supports concatenation, splitting, trimming, and case conversion, mostly working on either String or &str interchangeably thanks to deref coercion.

Ruststring_methods.rs
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;   // + takes ownership of s1, borrows s2

let trimmed = "  padded  ".trim();          // "padded"
let upper = "hello".to_uppercase();          // "HELLO"
let parts: Vec<&str> = "a,b,c".split(',').collect();   // ["a", "b", "c"]

Both are working as designed: split reports the empty field after the last comma. When the separator terminates each item rather than separating them, split_terminator is the method that matches that shape — it drops the final empty piece:

Rustsplit_fix.rs
let line = "10,20,30,";
let parts: Vec<&str> = line.split_terminator(',').collect();
println!("{:?} ({} parts)", parts, parts.len());   // ["10", "20", "30"] (3 parts)

let total: i32 = parts.iter().map(|p| p.parse::<i32>().unwrap()).sum();
println!("{}", total);   // 60

Sources

1
The Rust Project. The Rust Programming Language, "Storing UTF-8 Encoded Text with Strings", doc.rust-lang.org/book/ch08-02.