Rust - Slices

Overview

Estimated time: 40–50 minutes

Master Rust slices, which allow you to reference a contiguous sequence of elements in a collection without taking ownership. Learn string slices, array slices, slice methods, and how slices work with Rust's ownership system.

Learning Objectives

Prerequisites

What Are Slices?

A slice is a reference to a contiguous sequence of elements in a collection. Slices are borrowing references that don't own the data they point to.

Slice Fundamentals

fn main() {
    let numbers = [1, 2, 3, 4, 5, 6, 7, 8];
    
    // Full slice of the array
    let full_slice: &[i32] = &numbers;
    println!("Full slice: {:?}", full_slice);
    
    // Partial slices using range syntax
    let first_three = &numbers[0..3];     // Elements 0, 1, 2
    let middle = &numbers[2..6];          // Elements 2, 3, 4, 5
    let last_two = &numbers[6..8];        // Elements 6, 7
    
    println!("First three: {:?}", first_three);
    println!("Middle: {:?}", middle);
    println!("Last two: {:?}", last_two);
    
    // Slice shortcuts
    let from_start = &numbers[..4];       // Same as &numbers[0..4]
    let to_end = &numbers[3..];           // From index 3 to end
    let entire = &numbers[..];            // Entire array
    
    println!("From start: {:?}", from_start);
    println!("To end: {:?}", to_end);
    println!("Entire: {:?}", entire);
    
    // Slice properties
    println!("Slice length: {}", middle.len());
    println!("Is empty: {}", middle.is_empty());
}

Expected Output:

Full slice: [1, 2, 3, 4, 5, 6, 7, 8]
First three: [1, 2, 3]
Middle: [3, 4, 5, 6]
Last two: [7, 8]
From start: [1, 2, 3, 4]
To end: [4, 5, 6, 7, 8]
Entire: [1, 2, 3, 4, 5, 6, 7, 8]
Slice length: 4
Is empty: false

String Slices (&str)

String slices are the most common type of slice in Rust. String literals are string slices.

String Slice Basics

fn main() {
    let text = String::from("Hello, beautiful world!");
    
    // String slices from String
    let hello = &text[0..5];              // "Hello"
    let beautiful = &text[7..16];         // "beautiful"
    let world = &text[17..22];            // "world"
    
    println!("Original: {}", text);
    println!("hello: {}", hello);
    println!("beautiful: {}", beautiful);
    println!("world: {}", world);
    
    // String literals are already string slices
    let literal: &str = "This is a string slice";
    println!("Literal: {}", literal);
    
    // Slicing with different ranges
    let greeting = &text[..5];            // From start to index 5
    let ending = &text[17..];             // From index 17 to end
    let full = &text[..];                 // Entire string
    
    println!("Greeting: {}", greeting);
    println!("Ending: {}", ending);
    println!("Full: {}", full);
    
    // Functions that work with string slices
    fn print_slice(s: &str) {
        println!("Slice: '{}'", s);
    }
    
    print_slice(&text);        // Pass String as slice
    print_slice(literal);      // Pass string literal
    print_slice(&text[7..16]); // Pass substring slice
}

Expected Output:

Original: Hello, beautiful world!
hello: Hello
beautiful: beautiful
world: world
Literal: This is a string slice
Greeting: Hello
Ending: world!
Full: Hello, beautiful world!
Slice: 'Hello, beautiful world!'
Slice: 'This is a string slice'
Slice: 'beautiful'

Safe String Slicing

fn main() {
    let text = "Hello, 世界! 🦀";
    
    println!("Text: {}", text);
    println!("Byte length: {}", text.len());
    println!("Character count: {}", text.chars().count());
    
    // Unsafe: Direct byte indexing can panic with Unicode
    // let bad_slice = &text[0..8]; // Might panic if it cuts through a Unicode character
    
    // Safe: Use char boundaries
    let char_indices: Vec<_> = text.char_indices().collect();
    println!("Character boundaries: {:?}", char_indices);
    
    // Safe slicing using char_indices
    if let Some((start, _)) = char_indices.get(0) {
        if let Some((end, _)) = char_indices.get(7) {
            let safe_slice = &text[*start..*end];
            println!("Safe slice: {}", safe_slice);
        }
    }
    
    // Using get() for safe slicing
    match text.get(0..5) {
        Some(slice) => println!("Got slice: {}", slice),
        None => println!("Invalid slice range"),
    }
    
    // Working with characters directly
    let chars: Vec = text.chars().collect();
    let char_slice = &chars[0..5];
    let back_to_string: String = char_slice.iter().collect();
    println!("Character slice: {:?}", char_slice);
    println!("Back to string: {}", back_to_string);
}

Expected Output:

Text: Hello, 世界! 🦀
Byte length: 17
Character count: 10
Character boundaries: [(0, 'H'), (1, 'e'), (2, 'l'), (3, 'l'), (4, 'o'), (5, ','), (6, ' '), (7, '世'), (10, '界'), (13, '!'), (14, ' '), (15, '🦀')]
Safe slice: Hello, 世
Got slice: Hello
Character slice: ['H', 'e', 'l', 'l', 'o']
Back to string: Hello

Array and Vector Slices

Working with Array Slices

fn main() {
    let numbers = [10, 20, 30, 40, 50, 60, 70, 80];
    
    // Different slice types
    let slice: &[i32] = &numbers[2..6];
    println!("Slice: {:?}", slice);
    
    // Slices maintain type information
    let first_half = &numbers[..4];
    let second_half = &numbers[4..];
    
    println!("First half: {:?}", first_half);
    println!("Second half: {:?}", second_half);
    
    // Functions that work with slices
    fn sum_slice(slice: &[i32]) -> i32 {
        slice.iter().sum()
    }
    
    fn find_max(slice: &[i32]) -> Option<&i32> {
        slice.iter().max()
    }
    
    println!("Sum of slice: {}", sum_slice(slice));
    println!("Max in first half: {:?}", find_max(first_half));
    
    // Working with different array types
    let floats = [1.1, 2.2, 3.3, 4.4, 5.5];
    let float_slice: &[f64] = &floats[1..4];
    println!("Float slice: {:?}", float_slice);
    
    // Slices of different sizes from same array
    let small = &numbers[0..2];
    let medium = &numbers[0..4];
    let large = &numbers[0..6];
    
    println!("Small: {:?} (len: {})", small, small.len());
    println!("Medium: {:?} (len: {})", medium, medium.len());
    println!("Large: {:?} (len: {})", large, large.len());
}

Expected Output:

Slice: [30, 40, 50, 60]
First half: [10, 20, 30, 40]
Second half: [50, 60, 70, 80]
Sum of slice: 180
Max in first half: Some(40)
Float slice: [2.2, 3.3, 4.4]
Small: [10, 20] (len: 2)
Medium: [10, 20, 30, 40] (len: 4)
Large: [10, 20, 30, 40, 50, 60] (len: 6)

Vector Slices

fn main() {
    let mut vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // Immutable slices from vector
    let middle = &vec[3..7];
    println!("Middle slice: {:?}", middle);
    
    // Mutable slices
    let mutable_slice = &mut vec[0..3];
    println!("Before modification: {:?}", mutable_slice);
    
    // Modify through mutable slice
    for element in mutable_slice.iter_mut() {
        *element *= 2;
    }
    
    println!("After modification: {:?}", vec);
    
    // Slice methods work on vectors too
    let odds: Vec<&i32> = vec.iter().filter(|&&x| x % 2 == 1).collect();
    println!("Odd numbers: {:?}", odds);
    
    // Dynamic data with slices
    vec.push(11);
    vec.push(12);
    
    let last_four = &vec[vec.len()-4..];
    println!("Last four elements: {:?}", last_four);
    
    // Converting vector to slice for function calls
    fn process_data(data: &[i32]) -> i32 {
        data.iter().map(|x| x * x).sum()
    }
    
    let result = process_data(&vec);  // Convert Vec to slice
    println!("Sum of squares: {}", result);
}

Expected Output:

Middle slice: [4, 5, 6, 7]
Before modification: [1, 2, 3]
After modification: [2, 4, 6, 4, 5, 6, 7, 8, 9, 10, 11, 12]
Odd numbers: [5, 7, 9, 11]
Last four elements: [9, 10, 11, 12]
Sum of squares: 650

Slice Methods and Operations

Common Slice Methods

fn main() {
    let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let slice = &numbers[..];
    
    // Basic properties
    println!("Length: {}", slice.len());
    println!("Is empty: {}", slice.is_empty());
    println!("First element: {:?}", slice.first());
    println!("Last element: {:?}", slice.last());
    
    // Safe element access
    println!("Element at index 3: {:?}", slice.get(3));
    println!("Element at index 100: {:?}", slice.get(100));
    
    // Splitting slices
    let (left, right) = slice.split_at(5);
    println!("Split at 5: {:?} | {:?}", left, right);
    
    // Finding elements
    println!("Contains 7: {}", slice.contains(&7));
    println!("Position of 6: {:?}", slice.iter().position(|&x| x == 6));
    
    // Binary search (requires sorted slice)
    match slice.binary_search(&7) {
        Ok(index) => println!("Found 7 at index {}", index),
        Err(index) => println!("7 would be inserted at index {}", index),
    }
    
    // Windows and chunks
    println!("Windows of size 3:");
    for window in slice.windows(3) {
        println!("  {:?}", window);
    }
    
    println!("Chunks of size 3:");
    for chunk in slice.chunks(3) {
        println!("  {:?}", chunk);
    }
}

Expected Output:

Length: 10
Is empty: false
First element: Some(1)
Last element: Some(10)
Element at index 3: Some(4)
Element at index 100: None
Split at 5: [1, 2, 3, 4, 5] | [6, 7, 8, 9, 10]
Contains 7: true
Position of 6: Some(5)
Found 7 at index 6
Windows of size 3:
  [1, 2, 3]
  [2, 3, 4]
  [3, 4, 5]
  [4, 5, 6]
  [5, 6, 7]
  [6, 7, 8]
  [7, 8, 9]
  [8, 9, 10]
Chunks of size 3:
  [1, 2, 3]
  [4, 5, 6]
  [7, 8, 9]
  [10]

Slice Iteration Patterns

fn main() {
    let data = [10, 20, 30, 40, 50];
    let slice = &data[..];
    
    // Basic iteration
    println!("Basic iteration:");
    for item in slice {
        println!("  {}", item);
    }
    
    // Iteration with indices
    println!("With indices:");
    for (index, item) in slice.iter().enumerate() {
        println!("  {}: {}", index, item);
    }
    
    // Iterator methods
    let doubled: Vec = slice.iter().map(|x| x * 2).collect();
    println!("Doubled: {:?}", doubled);
    
    let sum: i32 = slice.iter().sum();
    println!("Sum: {}", sum);
    
    let even_count = slice.iter().filter(|&&x| x % 20 == 0).count();
    println!("Count of numbers divisible by 20: {}", even_count);
    
    // Finding elements
    let first_big = slice.iter().find(|&&x| x > 25);
    println!("First number > 25: {:?}", first_big);
    
    // All/any operations
    let all_positive = slice.iter().all(|&&x| x > 0);
    let any_even = slice.iter().any(|&&x| x % 2 == 0);
    
    println!("All positive: {}", all_positive);
    println!("Any even: {}", any_even);
    
    // Folding (reducing)
    let product = slice.iter().fold(1, |acc, &x| acc * x);
    println!("Product: {}", product);
    
    // Collecting different types
    let as_strings: Vec = slice.iter().map(|x| x.to_string()).collect();
    println!("As strings: {:?}", as_strings);
}

Expected Output:

Basic iteration:
  10
  20
  30
  40
  50
With indices:
  0: 10
  1: 20
  2: 30
  3: 40
  4: 50
Doubled: [20, 40, 60, 80, 100]
Sum: 150
Count of numbers divisible by 20: 2
First number > 25: Some(30)
All positive: true
Any even: true
Product: 12000000
As strings: ["10", "20", "30", "40", "50"]

Mutable Slices

Modifying Through Mutable Slices

fn main() {
    let mut numbers = [5, 2, 8, 1, 9, 3, 7, 4, 6];
    println!("Original: {:?}", numbers);
    
    // Mutable slice
    let slice = &mut numbers[..];
    
    // Modify elements directly
    slice[0] = 100;
    slice[8] = 200;
    println!("After direct modification: {:?}", numbers);
    
    // Modify through iteration
    for element in slice.iter_mut() {
        if *element < 10 {
            *element *= 10;
        }
    }
    println!("After iteration modification: {:?}", numbers);
    
    // Slice operations that modify
    let middle = &mut numbers[2..7];
    middle.reverse();
    println!("After reversing middle section: {:?}", numbers);
    
    // Sort a portion
    let to_sort = &mut numbers[1..6];
    to_sort.sort();
    println!("After sorting section: {:?}", numbers);
    
    // Swap elements
    slice.swap(0, 8);
    println!("After swapping first and last: {:?}", numbers);
    
    // Fill with value
    let end_section = &mut numbers[6..9];
    end_section.fill(999);
    println!("After filling end section: {:?}", numbers);
}

Expected Output:

Original: [5, 2, 8, 1, 9, 3, 7, 4, 6]
After direct modification: [100, 2, 8, 1, 9, 3, 7, 4, 200]
After iteration modification: [100, 20, 80, 10, 90, 30, 70, 40, 200]
After reversing middle section: [100, 20, 30, 90, 10, 80, 70, 40, 200]
After sorting section: [100, 10, 20, 30, 80, 90, 70, 40, 200]
After swapping first and last: [200, 10, 20, 30, 80, 90, 70, 40, 100]
After filling end section: [200, 10, 20, 30, 80, 90, 999, 999, 999]

Splitting Mutable Slices

fn main() {
    let mut data = [1, 2, 3, 4, 5, 6, 7, 8];
    println!("Original: {:?}", data);
    
    // Split mutable slice
    let (left, right) = data.split_at_mut(4);
    
    // Modify both parts simultaneously
    for item in left.iter_mut() {
        *item *= 10;
    }
    
    for item in right.iter_mut() {
        *item += 100;
    }
    
    println!("After split modification: {:?}", data);
    
    // Split multiple ways
    let mut matrix = [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]
    ];
    
    // Get mutable slices of rows
    let (first_two, last_one) = matrix.split_at_mut(2);
    
    // Modify first two rows
    for row in first_two.iter_mut() {
        for cell in row.iter_mut() {
            *cell *= 2;
        }
    }
    
    // Modify last row
    for cell in last_one[0].iter_mut() {
        *cell += 10;
    }
    
    println!("Modified matrix:");
    for row in &matrix {
        println!("  {:?}", row);
    }
}

Expected Output:

Original: [1, 2, 3, 4, 5, 6, 7, 8]
After split modification: [10, 20, 30, 40, 105, 106, 107, 108]
Modified matrix:
  [2, 4, 6]
  [8, 10, 12]
  [17, 18, 19]

Slice Patterns and Matching

Pattern Matching with Slices

fn main() {
    let numbers = [1, 2, 3, 4, 5];
    
    // Function that processes different slice patterns
    fn describe_slice(slice: &[i32]) {
        match slice {
            [] => println!("Empty slice"),
            [x] => println!("Single element: {}", x),
            [x, y] => println!("Two elements: {} and {}", x, y),
            [x, y, z] => println!("Three elements: {}, {}, {}", x, y, z),
            [first, .., last] => println!("Multiple elements: first = {}, last = {}", first, last),
        }
    }
    
    // Test different slice sizes
    describe_slice(&[]);
    describe_slice(&[42]);
    describe_slice(&[1, 2]);
    describe_slice(&[1, 2, 3]);
    describe_slice(&numbers);
    describe_slice(&numbers[1..4]);
    
    // More complex pattern matching
    fn analyze_sequence(slice: &[i32]) -> String {
        match slice {
            [] => "Empty".to_string(),
            [x] if *x > 0 => format!("Single positive: {}", x),
            [x] if *x < 0 => format!("Single negative: {}", x),
            [x] => format!("Single zero: {}", x),
            [x, y] if x < y => format!("Ascending pair: {} < {}", x, y),
            [x, y] if x > y => format!("Descending pair: {} > {}", x, y),
            [x, y] => format!("Equal pair: {} = {}", x, y),
            [first, middle @ .., last] if first < last => {
                format!("Ascending sequence: {} .. {} (middle: {} elements)", 
                        first, last, middle.len())
            },
            [first, middle @ .., last] => {
                format!("Non-ascending sequence: {} .. {} (middle: {} elements)", 
                        first, last, middle.len())
            },
        }
    }
    
    println!("\nSequence analysis:");
    let test_cases = [
        &[][..],           // Empty
        &[42][..],         // Single
        &[1, 5][..],       // Ascending pair
        &[5, 1][..],       // Descending pair
        &[3, 3][..],       // Equal pair
        &[1, 2, 3, 4, 5][..], // Ascending sequence
        &[5, 3, 1, 2, 0][..], // Non-ascending sequence
    ];
    
    for case in &test_cases {
        println!("  {:?} -> {}", case, analyze_sequence(case));
    }
}

Expected Output:

Empty slice
Single element: 42
Two elements: 1 and 2
Three elements: 1, 2, 3
Multiple elements: first = 1, last = 5
Multiple elements: first = 2, last = 4

Sequence analysis:
  [] -> Empty
  [42] -> Single positive: 42
  [1, 5] -> Ascending pair: 1 < 5
  [5, 1] -> Descending pair: 5 > 1
  [3, 3] -> Equal pair: 3 = 3
  [1, 2, 3, 4, 5] -> Ascending sequence: 1 .. 5 (middle: 3 elements)
  [5, 3, 1, 2, 0] -> Non-ascending sequence: 5 .. 0 (middle: 3 elements)

Practical Examples

Text Processing with String Slices

fn main() {
    let text = "The quick brown fox jumps over the lazy dog";
    
    // Word extraction and analysis
    fn analyze_text(text: &str) {
        let words: Vec<&str> = text.split_whitespace().collect();
        println!("Text: {}", text);
        println!("Word count: {}", words.len());
        
        // Find longest and shortest words
        let longest = words.iter().max_by_key(|word| word.len()).unwrap();
        let shortest = words.iter().min_by_key(|word| word.len()).unwrap();
        
        println!("Longest word: '{}' ({} chars)", longest, longest.len());
        println!("Shortest word: '{}' ({} chars)", shortest, shortest.len());
        
        // Words starting with specific letter
        let t_words: Vec<&&str> = words.iter().filter(|word| word.starts_with('t')).collect();
        println!("Words starting with 't': {:?}", t_words);
        
        // Create acronym
        let acronym: String = words.iter()
            .map(|word| word.chars().next().unwrap().to_uppercase().to_string())
            .collect();
        println!("Acronym: {}", acronym);
    }
    
    analyze_text(text);
    
    // Extract file extension
    fn get_extension(filename: &str) -> Option<&str> {
        filename.rfind('.').map(|i| &filename[i+1..])
    }
    
    let filenames = ["document.pdf", "image.png", "script.js", "readme"];
    println!("\nFile extensions:");
    for filename in &filenames {
        match get_extension(filename) {
            Some(ext) => println!("  {} -> {}", filename, ext),
            None => println!("  {} -> no extension", filename),
        }
    }
}

Expected Output:

Text: The quick brown fox jumps over the lazy dog
Word count: 9
Longest word: 'quick' (5 chars)
Shortest word: 'The' (3 chars)
Words starting with 't': ["the"]
Acronym: TQBFJOTLD

File extensions:
  document.pdf -> pdf
  image.png -> png
  script.js -> js
  readme -> no extension

Data Processing with Array Slices

fn main() {
    let data = [
        85, 92, 78, 96, 88, 94, 82, 90, 87, 91,
        76, 89, 95, 83, 97, 79, 86, 93, 84, 81
    ];
    
    // Statistical analysis using slices
    fn analyze_data(slice: &[i32]) -> (f64, i32, i32, f64) {
        let sum: i32 = slice.iter().sum();
        let mean = sum as f64 / slice.len() as f64;
        let min = *slice.iter().min().unwrap();
        let max = *slice.iter().max().unwrap();
        
        // Standard deviation
        let variance = slice.iter()
            .map(|&x| (x as f64 - mean).powi(2))
            .sum::() / slice.len() as f64;
        let std_dev = variance.sqrt();
        
        (mean, min, max, std_dev)
    }
    
    println!("Full dataset analysis:");
    let (mean, min, max, std_dev) = analyze_data(&data);
    println!("  Mean: {:.2}", mean);
    println!("  Min: {}, Max: {}", min, max);
    println!("  Standard deviation: {:.2}", std_dev);
    
    // Analyze different sections
    let sections = [
        ("First half", &data[..10]),
        ("Second half", &data[10..]),
        ("Middle section", &data[5..15]),
        ("High performers", &data[..]),  // We'll filter this
    ];
    
    for (name, section) in §ions[..3] {  // Skip the last one for now
        println!("\n{} analysis:", name);
        let (mean, min, max, std_dev) = analyze_data(section);
        println!("  Mean: {:.2}, Range: {}-{}, Std Dev: {:.2}", 
                 mean, min, max, std_dev);
    }
    
    // Filter high performers (above average + 1 std dev)
    let overall_mean = mean;
    let threshold = overall_mean + std_dev;
    let high_performers: Vec = data.iter()
        .filter(|&&score| score as f64 > threshold)
        .cloned()
        .collect();
    
    println!("\nHigh performers (score > {:.2}):", threshold);
    println!("  Scores: {:?}", high_performers);
    println!("  Count: {}", high_performers.len());
}

Expected Output:

Full dataset analysis:
  Mean: 87.35
  Min: 76, Max: 97
  Standard deviation: 5.58

First half analysis:
  Mean: 87.30, Range: 78-96, Std Dev: 5.64

Second half analysis:
  Mean: 87.40, Range: 76-97, Std Dev: 5.66

Middle section analysis:
  Mean: 87.10, Range: 79-95, Std Dev: 5.63

High performers (score > 92.93):
  Scores: [96, 94, 95, 97, 93]
  Count: 5

Best Practices

Function Design with Slices

fn main() {
    // Good: Accept slices for maximum flexibility
    fn process_numbers(numbers: &[i32]) -> i32 {
        numbers.iter().map(|x| x * x).sum()
    }
    
    // This function works with arrays, vectors, and slices
    let array = [1, 2, 3, 4];
    let vector = vec![1, 2, 3, 4];
    let slice = &vector[1..3];
    
    println!("Array result: {}", process_numbers(&array));
    println!("Vector result: {}", process_numbers(&vector));
    println!("Slice result: {}", process_numbers(slice));
    
    // Good: Return slices when possible
    fn get_words(text: &str) -> Vec<&str> {
        text.split_whitespace().collect()
    }
    
    let text = "hello world rust programming";
    let words = get_words(text);
    println!("Words: {:?}", words);
    
    // Efficient: Avoid unnecessary allocations
    fn find_word_starting_with(text: &str, prefix: &str) -> Option<&str> {
        text.split_whitespace()
            .find(|word| word.starts_with(prefix))
    }
    
    if let Some(word) = find_word_starting_with(text, "rust") {
        println!("Found word starting with 'rust': {}", word);
    }
}

Expected Output:

Array result: 30
Vector result: 30
Slice result: 13
Words: ["hello", "world", "rust", "programming"]
Found word starting with 'rust': rust

Common Pitfalls and Solutions

Checks for Understanding

  1. What's the difference between a slice and an array?
  2. Why might &string[0..5] panic with Unicode text?
  3. What does &mut [T] allow you to do that &[T] doesn't?
  4. How do you safely get a substring without risking a panic?
  5. What's the benefit of accepting &[T] instead of Vec in function parameters?

Answers

  1. A slice is a reference to a contiguous sequence; an array is owned data with fixed size known at compile time
  2. Because Unicode characters can be multiple bytes, and slicing by byte index might cut through a character
  3. &mut [T] allows mutation of elements; &[T] is read-only
  4. Use get() method or ensure slice bounds are on character boundaries with char_indices()
  5. More flexible - works with arrays, vectors, and other slices; doesn't transfer ownership

← PreviousNext →