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.
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 }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.
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-ASCIIIterating 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.
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.
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:
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