Skip to main content
  1. Posts/

Rust basics: strings, vectors, and slices

·574 words·3 mins
Author
m58
0xm58.xyz
Table of Contents
Many beginner Rust questions are really collection questions in disguise. Most of them come down to four types: 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
#

  • String owns text, while &str borrows text.
  • Vec<T> owns a growable collection, while &[T] borrows a slice of elements.
  • Idiomatic Rust APIs prefer borrowed forms like &str and &[T] when ownership is not needed.
  • Safe access methods like .get() fit naturally with Option.

If you learn these four types early, a large chunk of beginner Rust stops feeling mysterious.

Related

Rust basics: Option and Result

·481 words·3 mins
Rust does not use null for ordinary absence, and it does not rely on exceptions for recoverable errors. Instead, it uses enums: Option<T> and Result<T, E>. These two types show up everywhere. If you understand them early, a lot of Rust APIs stop looking strange.

Rust basics: control flow

·665 words·4 mins
Rust control flow is more than branching and loops. The important part is that many control-flow forms are expressions, which means they produce values. That design shows up everywhere in day-to-day Rust. It makes code concise, but it also forces you to be explicit about the shapes of the values you return.