8. Slices and strings: fat pointers and UTF-8
8.1 Slices: a pointer and a length
A slice &[T] is a borrowed view into contiguous elements. It is a fat
pointer: two words, (pointer, length). Unlike a Go slice it has no
capacity, so it cannot grow and cannot write past its end.
fn sum(xs: &[i64]) -> i64 { xs.iter().sum() } // accepts arrays, Vecs, and sub-slices
fn main() {
let arr = [1i64, 2, 3, 4, 5];
let v = vec![10i64, 20, 30];
println!("{} {} {}", sum(&arr), sum(&v), sum(&arr[1..3])); // 15 60 5
let mut buf = [0u8; 8];
let (head, tail) = buf.split_at_mut(4); // two non-overlapping &mut slices: allowed
head[0] = 0xAA;
tail[0] = 0xBB;
println!("{buf:02x?}");
println!("{}", std::mem::size_of::<&[u8]>()); // 16: pointer + length
}
Write functions that take &[T] instead of &Vec<T>, and &str instead
of &String. They accept more kinds of input and give up nothing. Clippy
warns about the other form (ptr_arg).
8.2 String vs &str
String |
&str |
|
|---|---|---|
| Owns its bytes? | yes (heap) | no (borrowed view) |
| Layout | (ptr, len, cap) | (ptr, len) |
| Growable? | yes | no |
| Literal | String::from("x"), "x".to_string() |
"x", which is a &'static str stored in the binary |
| Guarantee | always valid UTF-8 | always valid UTF-8 |
fn greet(name: &str) -> String {
format!("hello, {name}") // format! allocates a new String
}
fn main() {
let owned: String = String::from("Ada");
let borrowed: &str = &owned; // &String coerces to &str (Deref, Ch 18)
let lit: &'static str = "Lovelace";
let mut full = greet(borrowed);
full.push(' ');
full.push_str(lit);
println!("{full}");
}
8.3 UTF-8: why s[0] does not compile
fn main() {
let s = "héllo"; // 'é' takes 2 bytes in UTF-8
println!("{}", s.len()); // 6 BYTES, not 5 characters
println!("{}", s.chars().count()); // 5 chars (Unicode scalar values)
// let c = s[0]; // error[E0277]: the type `str` cannot be indexed by `{integer}`
println!("{}", &s[0..1]); // "h": byte-range slicing is allowed...
// println!("{}", &s[1..2]); // ...but this PANICS: byte 2 is inside 'é'
println!("{:?}", s.get(1..2)); // None: the non-panicking version
for (i, c) in s.char_indices() { print!("{i}:{c} "); }
println!();
println!("{:?}", s.as_bytes()); // the raw bytes, when you need them
}
Compare Go §12:
Go strings are UTF-8 by convention and can hold invalid bytes. Rust strings
are UTF-8 by invariant. String::from_utf8(bytes) returns a Result,
so invalid input is rejected at the boundary. For bytes that are not
necessarily text, such as file contents, network payloads, or paths, use
Vec<u8>/&[u8], OsString, or PathBuf.
Across the series: Unicode normalisation bugs, for example two different
byte sequences that render as the same username, are covered in
Security from Zero Ch 7.
Valid UTF-8 does not make strings normalised. Handle that separately with
the unicode-normalization crate.