$elementary.dev
[Courses][About]|
$elementary.dev

Text-first, project-driven courses for developers who want to level up by building real things.

CoursesAbout
Stay Updated

Get notified about new courses and articles.

// No spam, unsubscribe anytime

© 2026 elementary.dev
PrivacyTerms
// Course

Elementary Rust

7/11
  • 01Hello, Rust
  • 02Variables and Types
  • 03Functions and Control Flow
  • 04Why Memory Management Matters
  • 05Ownership: Rust's Solution
  • 06Borrowing and References
  • 07Slices
  • 08Structs
  • 09Enums and Pattern Matching
  • 10Organizing Rust Projects
  • 11Collections
courses/elementary-rust/07-slices

Slices

Reference contiguous sequences of data with Rust's slice types.

~20 min|#Lesson 7/11

Slices

Slices let you reference a portion of a collection without taking ownership. They are one of Rust's most useful tools for writing flexible, efficient code.

// Info-What you'll learn

By the end of this lesson, you'll understand slices, string slices (&str), array slices (&[T]), and why slice types make better function parameters than owned types.

What Are Slices?

A slice is a reference to a contiguous sequence of elements within a collection. It does not own the data it points to.

let s = String::from("hello world");
let hello = &s[0..5];   // "hello"
let world = &s[6..11];  // "world"

The hello and world variables are slices. They borrow part of the string s without taking ownership. When you see &s[start..end], you are creating a slice from index start up to (but not including) index end.

Slice Syntax Shortcuts

Rust provides shortcuts for common slice patterns:

let s = String::from("hello world");

// From the beginning
let hello = &s[..5];    // Same as &s[0..5]

// To the end
let world = &s[6..];    // Same as &s[6..11]

// The entire string
let whole = &s[..];     // Same as &s[0..11]

These shortcuts make your code cleaner. If you want everything from the start, omit the first number. If you want everything to the end, omit the second number.

// Tip-Range syntax

The .. syntax creates a range. 0..5 includes 0, 1, 2, 3, 4 (not 5). If you want to include the end, use ..= instead: 0..=5 includes 0 through 5.

Why Slices Exist

Consider a function that finds the first word in a string. Without slices, you might return an index:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    s.len()
}

This works, but there is a problem. The returned index has no connection to the string. If the string changes, the index becomes meaningless:

fn main() {
    let mut s = String::from("hello world");
    let word_end = first_word(&s);  // Returns 5

    s.clear();  // Empties the string

    // word_end is still 5, but s is now empty!
    // Using word_end would be a bug.
}

The compiler cannot catch this. The index and the string are separate values with no enforced relationship.

Slices Fix This Problem

Slices are references. The borrow checker ensures they remain valid:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}

Now the return value is tied to the input. Try to misuse it and the compiler stops you:

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);  // Immutable borrow

    s.clear();  // ERROR! Mutable borrow while immutable exists

    println!("the first word is: {word}");
}

The borrow checker prevents you from clearing s while word still references part of it. The bug that was invisible before is now a compile error.

// Warning-UTF-8 caution

String slices must occur at valid UTF-8 character boundaries. If you slice in the middle of a multi-byte character, your program will panic at runtime. For ASCII text this is never a problem, but be careful with Unicode.

Fat Pointers

Slices are sometimes called "fat pointers" because they store two pieces of data: a pointer to the first element and a length. A regular reference is just a pointer. A slice needs both the starting location and how many elements it spans.

let arr: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4];  // [2, 3, 4]

The slice variable contains a pointer to the element 2 and the length 3. Together, these define the slice.

// Challenge

Quick Check: Slice Type

What is the type of a string slice in Rust?

$

String Slices: &str

String slices have a special type: &str. You have already seen this type without realizing it:

let s = "Hello, world!";  // Type is &str

String literals are slices. They are references to text stored in the compiled binary. This is why they are immutable and have the type &str, not String.

The difference between String and &str:

TypeOwnershipStorageMutability
StringOwnedHeap-allocatedMutable
&strBorrowedPoints to String, literal, or binaryImmutable

A String owns its data. A &str borrows it. Under the hood, str is a specialized [u8] (slice of bytes) with a guarantee that the bytes form valid UTF-8.

// Info-Why &str exists instead of just &[u8]
  • Enforces UTF-8 validity at the type level
  • Enables string-specific methods that assume valid UTF-8
  • Prevents accidentally treating arbitrary bytes as text

Why &str is Idiomatic

Consider this function signature:

fn process_text(s: &String) {
    // ...
}

This works, but it limits what you can pass:

let owned = String::from("hello");
let literal = "world";

process_text(&owned);   // Works
process_text(literal);  // ERROR! literal is &str, not &String

Now compare this signature:

fn process_text(s: &str) {
    // ...
}

This accepts both:

let owned = String::from("hello");
let literal = "world";

process_text(&owned);   // Works! &String coerces to &str
process_text(literal);  // Works! Already &str

Using &str as a parameter type makes your function more flexible. It accepts owned strings, string literals, and slices with no extra work.

// Tip-Idiomatic Rust

When a function only needs to read a string, use &str as the parameter type. This is considered best practice in Rust. You will see this pattern throughout the standard library and ecosystem.

Array Slices: &[T]

The same concept applies to arrays and vectors. A slice of integers has type &[i32]:

fn sum(numbers: &[i32]) -> i32 {
    let mut total = 0;
    for n in numbers {
        total += n;
    }
    total
}

This function accepts arrays, vectors, and slices:

let vec = vec![1, 2, 3];
let arr = [4, 5, 6];

sum(&vec);       // &Vec<i32> coerces to &[i32]
sum(&arr);       // &[i32; 3] coerces to &[i32]
sum(&vec[0..2]); // Slice of first two elements

Just like &str for strings, &[T] is the idiomatic parameter type when you need to read a sequence of values.

Deref Coercion

You may have noticed that &String converts to &str and &Vec<T> converts to &[T] automatically. This is called deref coercion.

Rust performs deref coercion when a reference to one type can be converted to a reference to another type. The standard library implements Deref traits that enable these conversions:

  • &String coerces to &str
  • &Vec<T> coerces to &[T]
  • &Box<T> coerces to &T

You do not need to understand the implementation details. The important point is that you get this flexibility for free when you use slice types in function signatures.

// Info-The benefit

By using &str and &[T] as parameter types, your functions automatically accept the widest variety of input types. Callers can pass owned values, references, or slices without any explicit conversion.

Putting It Together

Here is a more realistic example. This function finds the longest word in a sentence:

fn longest_word(sentence: &str) -> &str {
    let mut longest = "";
    for word in sentence.split_whitespace() {
        if word.len() > longest.len() {
            longest = word;
        }
    }
    longest
}

fn main() {
    let text = String::from("The quick brown fox");
    let result = longest_word(&text);
    println!("Longest word: {result}");  // "quick"
}

The function takes &str and returns &str. It works with any string type. The returned slice borrows from the input, so the borrow checker ensures the input outlives the result.

// Challenge

Challenge: First Word

Write a function signature that takes a string slice and returns a string slice. The function should be named first_word.

$

Summary

Slices are references to contiguous sequences within collections. They borrow data without taking ownership.

Key takeaways:

  • String slices have type &str. Array slices have type &[T].
  • Slices are fat pointers: they store a pointer and a length.
  • Use &str instead of &String for string parameters.
  • Use &[T] instead of &Vec<T> for slice parameters.
  • Deref coercion lets &String and &Vec<T> automatically convert to slice types.
  • The borrow checker ensures slices remain valid, preventing the bugs that plague index-based approaches.
// Tip-Rule of thumb

When writing functions that read collections without modifying them, use slice types (&str, &[T]) as parameters. This makes your functions maximally flexible and idiomatic.

Slices complete our exploration of references and borrowing. You now understand ownership, borrowing, and slices. These three concepts form the foundation of Rust's memory safety guarantees. In the next lesson, we will use these concepts as we build custom data types with structs.

<-[prev]Borrowing and References[next]->Structs
Progress: 0%
// On this page
  • What Are Slices?
  • Slice Syntax Shortcuts
  • Why Slices Exist
  • Slices Fix This Problem
  • Fat Pointers
  • String Slices: &str
  • Why &str is Idiomatic
  • Array Slices: &[T]
  • Deref Coercion
  • Putting It Together
  • Summary