Updated Aug 5, 2026

09. Vec, Slices, and Strings

📋 Jump to Takeaways

🎁 Arrays are great when you know exactly how many items you need at compile time. But what if you're reading user input, parsing a file, or building a list dynamically? Rust's standard library gives you powerful, heap-allocated collections that grow and shrink at runtime.

Growable Arrays with Vec

Reach for Vec<T> first. It's the collection you'll use most in Rust: a single type, packed together in a heap buffer that grows as you push into it.

fn main() {
    let mut numbers: Vec<i32> = Vec::new();
    numbers.push(10);
    numbers.push(20);
    numbers.push(30);
    println!("{:?}", numbers); // [10, 20, 30]

    // You can also use the vec! macro
    let colors = vec!["red", "green", "blue"];
    println!("{:?}", colors); // ["red", "green", "blue"]
}

You create an empty vector with Vec::new() or a pre-filled one with the vec! macro. The type parameter T is inferred from what you push into it.

Already have a fixed-size array? Turn it into a Vec with Vec::from() or .into().

fn main() {
    let arr = [1, 2, 3, 4, 5];

    let v1 = Vec::from(arr);
    println!("{:?}", v1); // [1, 2, 3, 4, 5]

    let v2: Vec<i32> = arr.into();
    println!("{:?}", v2); // [1, 2, 3, 4, 5]
}

Both copy the array's elements from the stack into the Vec's heap-allocated buffer. The original array is still on the stack (since i32 is Copy), but the Vec now owns its own heap copy.

With owned types like String, the behavior is different — elements are moved, not copied:

fn main() {
    let arr = [String::from("hello"), String::from("world")];
    let v = Vec::from(arr);

    println!("{:?}", v); // ["hello", "world"]
    // println!("{:?}", arr); // ❌ ERROR: arr was moved — String isn't Copy
}

String owns heap data, so Vec::from(arr) takes ownership of each string. The original array is consumed and can't be used again. No cloning of the actual character data happens — the strings just move into the Vec's buffer.

If you have a slice (&[T]) instead of an owned array, you can't move out of it — it's just a borrowed view. Use .to_vec() to clone each element into a new owned Vec:

fn main() {
    let v = vec![String::from("hello"), String::from("world")];
    let slice: &[String] = &v;

    let cloned = slice.to_vec(); // clones each String

    println!("{:?}", v);      // still works — v wasn't moved
    println!("{:?}", cloned); // independent copy
}

.to_vec() requires T: Clone — it always clones because a borrowed view can't give up ownership of data it doesn't own.

Accessing Elements

What happens when you ask for the 100th element of a 4-element vector? It depends how you ask. Indexing with [] panics on out-of-bounds. .get() hands you an Option instead.

fn main() {
    let scores = vec![85, 92, 78, 96];

    // Direct indexing — copies the value (i32 is Copy), panics if out of bounds
    let first = scores[0]; // first is i32, not a reference
    println!("First: {}", first); // First: 85

    // .get() returns Option<&i32> — a reference to the value inside the vec
    match scores.get(10) {
        Some(val) => println!("Found: {}", val), // val is &i32
        None => println!("Index out of bounds!"), // Index out of bounds!
    }
}

You can also use if let instead of a full match when handling .get():

fn main() {
    let scores = vec![85, 92, 78, 96];

    if let Some(val) = scores.get(2) { // val is &i32 (reference into the vec)
        println!("Found: {}", val);    // Found: 78 — println auto-dereferences
    } else {
        println!("Out of bounds!");
    }
}

.get() returns Option<&T>, a reference to the element inside the Vec. The value stays in the vector — you're just looking at it. Same pattern as HashMap.get().

A few more common operations:

fn main() {
    let colors = vec!["red", "green", "blue"];

    println!("{}", colors.len());              // 3
    println!("{}", colors.is_empty());         // false
    println!("{}", colors.contains(&"green")); // true — &"green" because contains() takes a reference
    println!("{}", colors.contains(&"pink"));  // false
}

Use .get() when the index might be invalid. Use [] when you're certain the index is in range and want a panic to signal a bug.

Push, Pop, and Iteration

A vector works great as a stack. You push onto the end, pop off the end, and iterate through everything in between.

fn main() {
    let mut stack = vec![1, 2, 3];
    stack.push(4);
    println!("{:?}", stack); // [1, 2, 3, 4]

    let popped = stack.pop(); // Returns Option<T>
    println!("{:?}", popped); // Some(4)

    // Iterating by reference — num is &i32
    for num in &stack {
        print!("{} ", num); // 1 2 3 — println auto-dereferences &i32
    }
    println!();

    // Iterating with mutable references — num is &mut i32
    for num in &mut stack {
        *num *= 2; // *num dereferences &mut i32 to modify the actual value
    }
    println!("{:?}", stack); // [2, 4, 6]
}

.pop() returns Option<T> because the vector might be empty. Iteration with & borrows each element without taking ownership. When you iterate with &mut, each num is a mutable reference — you need *num to dereference it and change the value it points to.

Need a queue (FIFO) instead of a stack? Use VecDeque — it supports push/pop from both ends in O(1):

use std::collections::VecDeque;

fn main() {
    let mut queue = VecDeque::new();
    queue.push_back(1);
    queue.push_back(2);
    queue.push_back(3);

    println!("{:?}", queue.pop_front()); // Some(1) — first in, first out
    println!("{:?}", queue.pop_front()); // Some(2)
    println!("{:?}", queue); // [3]
}

Removing Elements

You can remove by index, filter in place, or clear everything:

fn main() {
    let mut items = vec![10, 20, 30, 40, 50];

    // Remove by index — shifts everything after it
    items.remove(1);
    println!("{:?}", items); // [10, 30, 40, 50]

    // Keep only elements that match a condition
    items.retain(|&x| x > 20);
    println!("{:?}", items); // [30, 40, 50]

    // Remove everything
    items.clear();
    println!("{:?}", items); // []
}

remove(i) is O(n) because elements shift to fill the gap. If order doesn't matter, swap_remove(i) is O(1) — it swaps the last element into the gap instead of shifting.

Capacity vs Length

A vector tracks two numbers, not one. Length is how many elements it holds. Capacity is how much memory it has reserved. Push past the capacity and the vector reallocates a bigger buffer.

fn main() {
    let mut v = Vec::with_capacity(5);
    v.push(1);
    v.push(2);
    println!("len: {}, capacity: {}", v.len(), v.capacity());
    // len: 2, capacity: 5
}

Use Vec::with_capacity(n) when you know roughly how many elements you'll store. This avoids repeated reallocations.

Slicing

Slicing gives you a borrowed view into a Vec or String without copying data. Use range syntax with &:

fn main() {
    let numbers = vec![10, 20, 30, 40, 50];
    let middle: &[i32] = &numbers[1..4];
    println!("{:?}", middle); // [20, 30, 40]

    let first_two = &numbers[..2];
    println!("{:?}", first_two); // [10, 20]

    let last_two = &numbers[3..];
    println!("{:?}", last_two); // [40, 50]
}

1..4 means index 1, 2, 3 (end is exclusive). Omit the start to begin at 0, omit the end to go to the last element.

Slicing with &v[..] panics if the indices are out of bounds. Use .get(range) for safe access:

fn main() {
    let numbers = vec![10, 20, 30, 40, 50];

    // &numbers[0..10]; // 💥 panic: index out of bounds

    if let Some(slice) = numbers.get(0..10) { // slice would be &[i32]
        println!("{:?}", slice);
    } else {
        println!("Out of bounds!"); // Out of bounds!
    }

    if let Some(slice) = numbers.get(1..3) { // slice is &[i32] (reference to a portion)
        println!("{:?}", slice); // [20, 30]
    }
}

Same rule as single element access: [] panics, .get() returns Option.

Owning a Slice

A slice (&[T]) is a reference. Calling .clone() on it copies the reference, not the data behind it:

fn main() {
    let v = vec![1, 2, 3, 4];
    let slice = &v[1..3];

    let cloned = slice.clone();     // another &[i32] — still borrows v
    let owned = slice.to_vec();     // Vec<i32> — independent allocation

    println!("{:?}", cloned); // [2, 3]
    println!("{:?}", owned);  // [2, 3]
}

.clone() always copies the value it's called on. A Vec is data, so cloning it copies the heap data. A &[T] is a pointer, so cloning it copies the pointer. To go from "reference to someone else's data" to "my own data", use .to_vec() or .to_owned().

Strings: Owned vs Borrowed

String is the string type you build up at runtime: growable, UTF-8, and owned on the heap. That's the difference from &str, which just borrows text that lives elsewhere (usually static memory for literals, or the heap when slicing a String).

fn main() {
    let mut greeting = String::from("Hello");
    greeting.push_str(", world!");
    greeting.push('!');
    println!("{}", greeting); // Hello, world!!

    // format! for complex concatenation
    let name = "Rust";
    let message = format!("{} loves {}", greeting, name);
    println!("{}", message); // Hello, world!! loves Rust
}

Use push_str to append a string slice and push to append a single character. The format! macro builds strings without taking ownership of its arguments.

Choosing Between String and &str

So which one should your function ask for? &str borrows a view into string data. String owns its data on the heap. That one difference decides which you want.

fn print_greeting(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let owned = String::from("Alice"); // Heap-allocated, owned
    let borrowed: &str = "Bob";        // Points to static data

    print_greeting(&owned);  // String coerces to &str
    print_greeting(borrowed); // Already &str
}

Accept &str in function parameters, it works with both String and string literals. Use String when you need to own or mutate the text.

Notice &owned in the call — Rust automatically converts &String to &str through deref coercion. You don't need to do anything special, just pass &your_string and it works.

UTF-8 String Indexing

Try to grab the first character with my_string[0] and Rust stops you. Why? Strings are UTF-8, where a single character can span 1 to 4 bytes, so indexing is ambiguous: do you mean the first byte or the first character?

fn main() {
    let hello = String::from("Здравствуйте"); // Russian
    println!("bytes: {}", hello.len());        // bytes: 24
    println!("chars: {}", hello.chars().count()); // chars: 12

    // Iterate over characters
    for c in hello.chars().take(3) {
        print!("{} ", c); // З д р
    }
    println!();
}

Use .chars() to iterate by Unicode scalar value and .bytes() for raw bytes. This design prevents subtle bugs with multi-byte characters.

String slicing works with byte ranges, but panics if you slice in the middle of a multi-byte character:

fn main() {
    let s = String::from("hello world");
    let word: &str = &s[0..5];
    println!("{}", word); // hello

    let rest: &str = &s[6..];
    println!("{}", rest); // world
}

If you're working with non-ASCII text, iterate with .chars() instead of slicing.

Key Takeaways

  • Vec is your go-to growable array: push, pop, get, remove, retain
  • Vec::from(arr) / arr.into() copies Copy types or moves owned types into a heap buffer
  • .to_vec() clones from a borrowed slice — always clones, requires T: Clone
  • Use .get() for safe access that returns Option instead of panicking — works for single elements and ranges
  • *ref dereferences a mutable reference to modify the value it points to
  • Vec::with_capacity avoids reallocations when you know the approximate size
  • Slices (&[T]) are borrowed views — .to_vec() or .to_owned() creates an independent copy
  • String owns heap-allocated UTF-8 text; &str borrows it
  • Accept &str in function params — Rust auto-converts &String to &str
  • You can't index strings by position because UTF-8 characters vary in byte length — use .chars()

🎁 You can store any single type in a Vec. But what if you need to associate keys with values, count occurrences, or check membership in O(1) time?

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy