The Slice Type

A slice is a reference to a contiguous sequence of elements in a collection. Unlike references to the whole collection, slices let you reference a specific portion of it. Slices are incredibly useful and appear throughout Oxide code.

String Slices

A string slice is a reference to part of a String:

fn main() {
    let s = String.from("hello world")

    let hello = &s[0..5]
    let world = &s[6..11]

    println!("\(hello)")  // "hello"
    println!("\(world)")  // "world"
}

Rather than taking a reference to the entire string, &s[0..5] creates a reference to a portion of the string. The range syntax [starting_index..ending_index] includes starting_index but excludes ending_index.

Rust comparison: String slices work identically to Rust. The type annotation is &str.

#![allow(unused)]
fn main() {
// Rust - identical syntax
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}

Slice Shorthand Syntax

You can omit the starting index if it's 0 or the ending index if it's the length:

fn main() {
    let s = String.from("hello")

    let slice1 = &s[0..2]  // "he"
    let slice2 = &s[..2]   // "he" - same, omit start

    let slice3 = &s[3..5]  // "lo"
    let slice4 = &s[3..]   // "lo" - same, omit end

    let slice5 = &s[..]    // "hello" - entire string
}

This shorthand makes slice syntax less verbose for common cases.

Using Slices in Functions

Slices are particularly useful in function parameters because they're more flexible than &String:

fn firstWord(s: &str): &str {
    let bytes = s.as_bytes()

    for (i, &item) in bytes.iter().enumerate() {
        if item == ' ' as u8 {
            return &s[0..i]
        }
    }

    &s[..]
}

fn main() {
    let myString = String.from("hello world")
    let word = firstWord(&myString)
    println!("\(word)")  // "hello"

    // Also works with string literals
    let word = firstWord("hello world")
    println!("\(word)")  // "hello"
}

Notice that firstWord takes &str, not &String. This makes it more flexible. We can pass:

  • A reference to a String: &myString (automatically coerced to &str)
  • A string literal: "hello world" (which is already &str)

If the function required &String, we couldn't pass string literals directly.

The Power of &str Over &String

Using &str in your API makes your code more flexible:

// Restrictive: only accepts String references
fn processBad(s: &String): UIntSize {
    s.len()
}

// Flexible: accepts string slices and String references
fn processGood(s: &str): UIntSize {
    s.len()
}

fn main() {
    let myString = String.from("hello")
    let literal = "world"

    // processBad(literal)  // Error: literal is &str, not &String
    processGood(&myString)  // OK: &String coerces to &str
    processGood(literal)    // OK: already &str
}

This is a key principle in Oxide and Rust: prefer &str over &String, &[T] over &Vec<T>, etc. Your APIs are more useful when they accept slices rather than owned collections.

Array Slices

Slices work with arrays and vectors, not just strings:

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

    // Slice part of the array
    let slice = &arr[1..4]  // [2, 3, 4]

    // Iterate over the slice
    for item in slice {
        println!("\(item)")
    }
}

With vectors, slices are even more useful:

fn main() {
    let v = vec![1, 2, 3, 4, 5]

    let slice = &v[2..]  // [3, 4, 5]

    println!("Length of slice: \(slice.len())")

    for &item in slice {
        println!("\(item)")
    }
}

Slices and Borrowing

Slices respect the borrowing rules. You cannot create a mutable reference to a collection while slices exist:

fn main() {
    var s = String.from("hello world")

    let word = firstWord(&s)  // immutable borrow

    // s.clear()  // Error: cannot borrow as mutable while borrowed as immutable

    println!("\(word)")  // word's borrow ends here

    s.clear()  // OK now: word is no longer used
}

fn firstWord(s: &str): &str {
    let bytes = s.as_bytes()

    for (i, &item) in bytes.iter().enumerate() {
        if item == ' ' as u8 {
            return &s[0..i]
        }
    }

    &s[..]
}

This prevents a common bug: modifying a collection while holding a reference to its contents. The slice is only valid as long as the underlying data doesn't change.

The Slice Type Syntax

The type of a slice is &[T], where T is the element type:

fn main() {
    let s = String.from("hello")
    let slice: &str = &s[..]  // &str is shorthand for &[char] (roughly)

    let arr = [1, 2, 3]
    let slice: &[Int] = &arr[..]  // &[Int] slice

    let v = vec![1, 2, 3]
    let slice: &[Int] = &v[..]  // &[Int] slice from vector
}

The syntax &[T] represents "a reference to a contiguous sequence of T". Notice that &str is special—it's the slice type for strings, optimized for UTF-8 text.

Practical Example: Splitting Words

Let's build something practical. A function that splits a string into words and returns an array of slices:

fn splitWords(s: &str): Vec<&str> {
    var words = vec![]
    var start = 0

    for (i, &c) in s.as_bytes().iter().enumerate() {
        if c == ' ' as u8 {
            words.push(&s[start..i])
            start = i + 1
        }
    }

    if start < s.len() {
        words.push(&s[start..])
    }

    words
}

fn main() {
    let sentence = "the quick brown fox"
    let words = splitWords(sentence)

    for word in words {
        println!("Word: \(word)")
    }
}

Output:

Word: the
Word: quick
Word: brown
Word: fox

This function takes a string slice, and returns a vector of slices pointing into the original string. No data is copied—only references are created.

Why Slices Are Important

Slices embody three key principles:

  1. Zero-Cost Abstractions: Slices have no runtime overhead. They're just a pointer and length.
  2. Safety: Slices prevent out-of-bounds access at compile time.
  3. Flexibility: Functions accepting slices work with owned collections, string literals, and stack arrays.

Because of these properties, slices appear everywhere in Oxide code. They're the idiomatic way to work with sequences.

Common Slice Methods

Slices provide useful methods for working with sequences:

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

    // Length
    println!("Length: \(slice.len())")  // 3

    // Access elements
    println!("First: \(slice[0])")      // 2
    println!("Last: \(slice[2])")       // 4

    // Iteration
    for &item in slice {
        println!("Item: \(item)")
    }

    // Check if empty
    if slice.is_empty() {
        println!("Empty slice")
    } else {
        println!("Not empty")
    }
}

Summary

  • Slices are references to contiguous portions of collections
  • String slices are denoted &str; array/vector slices are &[T]
  • Create slices with range syntax: &collection[start..end]
  • Use shorthand: &s[..] for the whole collection, &s[..n] to omit end, etc.
  • Slices are more flexible than references to whole collections; prefer them in APIs
  • Slices respect borrowing rules: no modification while slices exist
  • Slices have zero runtime cost and no bounds checking overhead

Slices combine safety with flexibility. They're one of the features that makes Oxide (and Rust) pleasant to use. By understanding ownership, references, and slices, you now have the foundation to write safe, efficient code.

The ownership system—ownership itself, borrowing, and slices—is complete. These three concepts form the bedrock of Oxide's memory safety story.