Slices in Rust

About This Module

This module introduces slices, a powerful feature in Rust that provides references to contiguous sub-sequences of collections. We'll explore how slices work with arrays and vectors, their memory representation, and how they interact with Rust's borrowing rules.

Prework

Prework Reading

Read the following sections from "The Rust Programming Language" book:

You might want to go back and review:

Pre-lecture Reflections

Before class, consider these questions:

  1. How do slices provide safe access to sub-sequences without copying data?
  2. What are the advantages of slices over passing entire arrays or vectors?
  3. How do borrowing rules apply to slices and prevent data races?
  4. When would you use slices instead of iterators for processing sub-sequences?
  5. What are the memory efficiency benefits of slices compared to copying data?

Learning Objectives

By the end of this module, you should be able to:

  • Create and use immutable and mutable slices from arrays and vectors
  • Understand slice syntax and indexing operations
  • Apply borrowing rules correctly when working with slices
  • Analyze the memory representation of slices
  • Use slices for efficient sub-sequence processing without data copying
  • Design functions that work with slice parameters for flexibility

Slices (ยง4.3)

Slice = reference to a contiguous sub-sequence of elements in a collection

Slices of an array:

  • array of type [T, _], e.g. datatype and length
  • slice of type &[T] (immutable) or &mut [T] (mutable)
fn main() {
    let arr: [i32; 5] = [0,1,2,3,4];
    println!("arr: {:?}", arr);

    // immutable slice of an array
    let slice: &[i32] = &arr[1..3];
    println!("slice: {:?}",slice);
    println!("slice[0]: {}", slice[0]);
}

The slice slice is a reference to the array arr from index 1 to 3 and hence is borrowed from arr.

Immutable slices

Note:

  • The slice is a reference to the array, which by default is immutable.
  • Even if the source array is mutable, the slice is immutable.
fn main() {
    let mut arr: [i32; 5] = [0,1,2,3,4];
    println!("arr: {:?}", arr);

    // immutable slice of an array
    let slice: &[i32] = &arr[1..3];
    println!("slice: {:?}",slice);
    println!("slice[0]: {}", slice[0]);

    slice[0] = 100;  // ERROR! Cannot modify an immutable slice
    println!("slice: {:?}", slice);
    println!("slice[0]: {}", slice[0]);
}

Mutable slices

We can create a mutable slice from a mutable array which borrows from arr mutably.

fn main(){
    // mutable slice of an array
    let mut arr = [0,1,2,3,4];
    println!("arr: {:?}", arr);

    let mut slice = &mut arr[2..4];
    println!("slice: {:?}",slice);

    // ERROR: Cannot modify the source array after a borrow
    //arr[0] = 10;
    //println!("arr: {:?}", arr);

    println!("\nLet's modify the slice[0]");
    slice[0] = slice[0] * slice[0];
    println!("slice[0]: {}", slice[0]);
    println!("slice: {:?}", slice);

    println!("arr: {:?}", arr);
}

What about this?

What's happening here?!?!?

Why are we able to modify the array after the slice is created?

fn main() {
    let mut arr: [i32; 5] = [0,1,2,3,4];
    println!("arr: {:?}", arr);

    // immutable slice of an array
    let slice: &[i32] = &arr[1..3];
    println!("slice: {:?}",slice);
    println!("slice[0]: {}", slice[0]);

    arr[0] = 10;  // OK! We can modify the array
    println!("arr: {:?}", arr);

    // What happens if you uncomment this line?
    //println!("slice: {:?}", slice);

}

Answer:

Slices with Vectors

Work for vectors too!

fn main() {
let mut v = vec![0,1,2,3,4];
{
    let slice = &v[1..3];
    println!("{:?}",slice);
}

{
    let mut slice = &mut v[1..3];
    
    // iterating over slices works as well
    for x in slice {
        *x *= 1000;
    }
};
println!("{:?}",v);
}

Slices are references: all borrowing rules still apply!

  • At most one mutable reference at a time
  • No immutable references allowed with a mutable reference
  • Many immutable references allowed simultaneously
#![allow(unused)]
fn main() {
// this won't work!
let mut v = vec![1,2,3,4,5,6,7];
{
    let ref_1 = &mut v[2..5];
    let ref_2 = &v[1..3];
    ref_1[0] = 7;
    println!("{}",ref_2[1]);
}
}
#![allow(unused)]
fn main() {
// and this reordering will
let mut v = vec![1,2,3,4,5,6,7];
{
    let ref_1 = &mut v[2..5];
    ref_1[0] = 7;   // ref_1 can be dropped
    let ref_2 = &v[1..3];
    println!("{}",ref_2[1]);
}
}

Memory representation of slices

  • Pointer
  • Length

Memory representation of slices

Let's return to &str?

&str is slice

  • &str can be a slice of a string literal or a slice of a String

  • &str itself (the reference) is stored on the stack,

  • but the string data it points to can be in different locations depending on the context.

Let's break this down:

The &str Data (Various Locations)

The actual string data that &str points to can be in:

  1. Binary's read-only data segment (most common for string literals):
#![allow(unused)]
fn main() {
let s: &str = "hello";  // "hello" is in read-only memory

println!("&s:{:p}", &s);
println!("ptr: {:p}", s.as_ptr());
println!("len: {}", s.len());
// println!("capacity: {}\n", s.capacity()); // ERROR! Not applicable
}
  1. Heap (when it's a slice of a String):
#![allow(unused)]
fn main() {
let string = String::from("hello");
let s: &str = &string;  // points to heap-allocated data

println!("&s:{:p}", &s);
println!("ptr: {:p}", s.as_ptr());
println!("len: {}", s.len());
}

True/False Statements on Rust Slices

A slice of type `&[i32]` is always immutable, even if it's created from a mutable array.

TRUE - "The slice is a reference to the array, which by default is immutable. Even if the source array is mutable, the slice is immutable." To get a mutable slice, you need to explicitly use `&mut [T]` syntax.


Slices in Rust consist of two components in memory: a pointer to the data and a length.

TRUE


You can have both an immutable slice and a mutable slice of the same vector active at the same time.

FALSE - Slices are references: all borrowing rules still apply!


The `&str` type is a slice, and the actual string data it points to is always stored in the binary's read-only data segment.

FALSE. While `&str` is indeed a slice, the string data it points to can be in different locations depending on the context, including the binary's read-only data segment (for string literals) or the heap (when it's a slice of a `String`).


Slices work with both arrays and vectors in Rust.

TRUE

Enter your answers into piazza poll.

Summary

  • Slices are references to contiguous sub-sequences of elements in a collection
  • Slices are immutable by default
  • We can create mutable slices from mutable arrays
  • Slices are references: all borrowing rules still apply!
  • &str is a slice of a string literal or a slice of a String
  • &str itself (the reference) is stored on the stack, but the string data it points to can be in different locations depending on the context.