String, &str, Vec<T>, and slices like &[T].These types are connected. If you learn them together, Rust’s API style becomes much easier to read.
String is owned text#
Use String when you need a growable, owned string:
let mut name = String::from("Ada");
name.push_str(" Lovelace");Because the string is owned, the variable is responsible for dropping it when it goes out of scope.
&str is a borrowed string slice#
A string slice is a borrowed view into string data:
let language: &str = "Rust";You also get &str when borrowing from a String:
let name = String::from("m58");
let borrowed: &str = &name;As a general API design rule, take &str when you only need to read text.
Why functions often prefer &str#
Compare these signatures:
fn greet_bad(name: String) { /* ... */ }
fn greet_good(name: &str) { /* ... */ }The second is more flexible. It accepts:
- string literals
- borrowed
Strings - slices of larger strings
That is why borrowed parameters are common in Rust libraries.
Vec<T> is an owned growable collection#
Vectors are Rust’s standard dynamic array type:
let mut ports = Vec::new();
ports.push(80);
ports.push(443);
ports.push(8080);You can also initialize one with vec!:
let ports = vec![80, 443, 8080];Iterating over vectors#
Read-only iteration borrows each item:
let ports = vec![80, 443, 8080];
for port in &ports {
println!("{port}");
}Mutable iteration lets you change items in place:
let mut ports = vec![80, 443, 8080];
for port in &mut ports {
*port += 1;
}And consuming iteration moves items out:
let ports = vec![80, 443, 8080];
for port in ports {
println!("{port}");
}After that loop, ports is no longer available because ownership moved into the loop.
Slices are borrowed views into collections#
Just like &str is a string slice, &[T] is a slice of elements:
let values = vec![10, 20, 30, 40];
let first_two: &[i32] = &values[0..2];A slice does not own the data. It just points to a contiguous region.
That is why APIs often take slices instead of vectors:
fn sum(values: &[i32]) -> i32 {
let mut total = 0;
for value in values {
total += *value;
}
total
}This works with both arrays and vectors, which makes the function more reusable.
Indexing versus safe access#
Rust supports indexing:
let ports = vec![80, 443, 8080];
let https = ports[1];But indexing panics if the index is out of bounds.
Safer code often uses .get():
match ports.get(10) {
Some(port) => println!("{port}"),
None => println!("missing"),
}That returns an Option, which forces you to handle the missing case.
Strings are UTF-8, so indexing is restricted#
This surprises beginners:
let s = String::from("hello");
// let first = s[0]; // invalid
Rust strings are UTF-8, so a “character” is not always one byte. Direct indexing would be ambiguous and error-prone.
Instead, use methods like:
.chars()for Unicode scalar values.bytes()for raw bytes- slicing only when you are sure the boundaries are valid UTF-8 boundaries
Summary#
Stringowns text, while&strborrows text.Vec<T>owns a growable collection, while&[T]borrows a slice of elements.- Idiomatic Rust APIs prefer borrowed forms like
&strand&[T]when ownership is not needed. - Safe access methods like
.get()fit naturally withOption.
If you learn these four types early, a large chunk of beginner Rust stops feeling mysterious.