---
title: Slices
source: 'https://academia.sh/en/courses/ownership/slices'
course: 'Ownership and the Memory Model'
language: en
updated: '2026-09-15T04:58:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows by running it that a slice views a string or a growable array without taking ownership, and that the source never ends while the slice lasts'
    - "Verifies that a slice's length derives from the source and that an empty slice is valid"
    - 'Proves that the source cannot be changed while the slice lives, and that a change goes through without a problem once the slice ends'
---

# Slices

A slice views a specific range of a string or a growable array without taking ownership: the slice gets used, the slice ends, but the source never ends. A slice's length derives from the source, and an empty slice is valid; the source cannot be changed while the slice lives, and a change goes through without a problem once the slice ends.

The previous lesson measured references lent out over a value's **entirety**. This
lesson applies the same access-without-ownership principle to only a **part** of a
value: a slice, a kind of reference that views a contiguous range of a string or a
growable array without taking ownership. What is measured is again a value's ending,
not a running unit stopping — the Concurrency course's goroutine question stays outside
this course's axis.

The concept of a slice itself had already been established in the Concurrency
course — a view representing a contiguous sub-range of an array and carrying its own
length. What this lesson measures is that Rust builds a slice with a range syntax
(`[start..end]`), and that the previous three lessons' rules (ownership is not taken,
the source does not end, a borrow's life cannot exceed the source) apply here exactly
as they were.

The previous three lessons assumed a reference always pointed to **a single value**:
`&Trace`, `&mut Trace`. A slice loosens this assumption — a reference can now point to
several contiguous values at once, but in doing so it changes none of the rules of
ownership, scope, or the shared/mutable distinction. What is measured in this lesson is
that the rules stay the same; only what gets borrowed changes.

## Viewing Part of a Value

A slice is a kind of `&T`: `&[Trace]` is a view taken from a growable array, `&str` is
a view taken from a string. Both point to the source's own data — they do not copy the
data — and separately carry their own length. When a slice is taken the source's owner
does not change; only temporary access to that range is set up.

The range syntax has the form `[start..end]`: `start` is included, `end` is excluded.
In its simplest form, with the ownership question never coming into play at all, the
range itself reads as follows.

```rust
// basic_slice.rs — slice syntax: the start..end range
fn main() {
    let numbers = [10, 20, 30, 40, 50];
    let middle = &numbers[1..4];
    println!("source: {:?}", numbers);
    println!("slice: {:?}", middle);
    println!("slice length: {}", middle.len());
}
```

```
source: [10, 20, 30, 40, 50]
slice: [20, 30, 40]
slice length: 3
```

`&numbers[1..4]` views three elements starting at index 1, up to (not including) index
4 — `20`, `30`, `40`. Because `numbers` carries a copyable type like `i32` here, the
ownership question does not show up; the real question arises when the source itself
carries a type that cannot be copied and implements `Drop` — exactly the situation the
rest of this lesson measures. The `[1..4]` syntax itself does not change `numbers`'s
type; `numbers` is still a fixed-length array, while `middle` is a separate type — a
slice with no length of its own, one that derives from the source.

## Measurement: The Source Never Ends

The measurement below takes a slice viewing the first two elements from a growable
array holding three `Trace` values, uses the slice, closes the slice's scope, and then
changes the source.

```rust
// trace.rs — a measurement helper that records a value's ending
use std::cell::RefCell;
use std::rc::Rc;

pub type Ledger = Rc<RefCell<Vec<String>>>;

pub struct Trace {
    pub name: String,
    ledger: Ledger,
}

impl Trace {
    pub fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace { name: name.to_string(), ledger: Rc::clone(ledger) }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

pub fn new_ledger() -> Ledger {
    Rc::new(RefCell::new(Vec::new()))
}

pub fn read_log(ledger: &Ledger) -> Vec<String> {
    ledger.borrow().clone()
}
```

```rust
// slice_life.rs — a slice views without taking ownership; it does not end the source, but it locks it
mod trace;
use trace::{Trace, new_ledger, read_log};

fn main() {
    let ledger = new_ledger();
    let mut list = vec![
        Trace::new("one", &ledger),
        Trace::new("two", &ledger),
        Trace::new("three", &ledger),
    ];
    let before = read_log(&ledger).len();

    {
        let view = &list[0..2];
        println!("slice length: {}, first name: {}", view.len(), view[0].name);
        println!("ledger while the slice lives: [{}]", read_log(&ledger)[before..].join(", "));
    }
    println!("ledger after the slice ends: [{}]", read_log(&ledger)[before..].join(", "));

    list.push(Trace::new("four", &ledger));
    println!("list changed because the slice ended, length: {}", list.len());
}
```

```
slice length: 2, first name: one
ledger while the slice lives: []
ledger after the slice ends: []
list changed because the slice ended, length: 4
```

`view` only views `list`'s first two elements, but `list` is still the owner of all
three elements. The ledger stays empty both while the slice lives and after the
slice's scope closes — none of the values `one`, `two`, and `three` end, because the
slice never owned them, it only looked at them. `list` itself never ends in this
measurement (because the code does not close at the end of the file); the only ending
seen is the slice's own scope closing — and that writes nothing to the ledger either,
because a slice of type `&[Trace]` does not implement `Drop`.

One detail deserves attention here: `view` only views the first two elements (`one`,
`two`); the third element (`three`) is left outside the slice. Even so, the measurement
shows that `list` in its entirety still sits in its owner — `three`, which the slice
does not cover, and `one` and `two`, which it does, are unaffected in the same way. A
slice only opens a window to **read** a subset of the source; it does not interfere in
any way with the ownership or life of the rest of the source.

The `view[0].name` access makes this concrete too: `view[0]` produces a `&Trace`
pointing to the slice's first element (not ownership, a new borrow), and `.name` is
read through that borrow. Three separate borrows are nested in the same line — from
`list` to `view`, from `view` to `view[0]`, from `view[0]` to `.name` — but none of
them asks for ownership, all of them trace back to the same underlying `Trace` values.

**OR10.** A slice, just like an ordinary `&T` reference, is not the owner of the
elements it points to. When its scope closes, only it (the slice's own `start, length`
pair) disappears; none of the elements are affected by this. The previous lesson's rule
that "a reference does not end the source" applies here exactly as it was, only
visibility is over a contiguous range rather than a single value.

## Boundary Measurement: A Slice Locks Its Source

The `list.push(...)` line runs **after** `view`'s scope closes. This ordering is
required: if the same line had been added while `view` was still in scope, the compiler
would reject it. `view` is a shared borrow of `list`, and the `push` call asks for a
mutable borrow to change `list` — the Borrowing lesson's "one writer or many readers"
rule applies here too, only what it borrows is no longer the whole value but a part of
it.

**OR11.** A slice locks its source's **entirety**, not just the range it views. Even
though `view` only views the first two elements, it also blocks adding an element
anywhere in `list` (at the end, for example) — because an insertion can rearrange a
growable array's own internal layout and invalidate the positions the slice points to.
The constraint is not limited to the elements the slice covers; it spreads across the
source as a whole.

This might look, at first glance, like it contradicts the statement that "a slice
views only part of the source" — the part viewed is small, but the surface locked is
large. There is no contradiction: a slice only grants permission to **read** the
elements in its own range, but because any change to the source's own layout (how many
elements it holds, where the elements sit) could break the assumption the slice relies
on, that change can never be started while the slice lives. What is viewed is narrow;
what is protected is broad.

The accepted code already preserves this order: the slice is used and let go within
its own narrow scope first, and `push` is only called afterward, at a point where
`list` has sole access. Trying to write the same job in reverse order — changing first,
then continuing to read the old slice — does not compile at all.

What this constraint prevents is concrete: `push` can reallocate a growable array's
backing storage when needed — as the array grows, all its elements can move to a new
location. If `view` pointed to the old locations and `push` triggered this move, `view`
would end up pointing to a region of memory that was no longer valid. The compiler
shuts this scenario down from the start by allowing no call that changes the source
while the slice lives — it does not even need to know whether `push` will actually
reallocate; the rule is the same for every mutating call.

The same constraint applies to reading a single element too: a direct index like
`list[2]` causes no trouble while `view` is in scope, because it also only reads — two
shared borrows can stand together. Trouble only starts when a **change** is requested
at the same time.

## A Slice Passes to a Function Without Moving the Source

The Borrowing lesson's `read` function took a single value without asking for
ownership. The same mechanism applies to a slice too: if a function writes its
parameter as `&[Trace]`, it can accept both the source's entirety and just a range of
it, without asking for ownership.

```rust
// slice_param.rs — a function accepting a slice processes the source without moving it
mod trace;
use trace::{Trace, read_log, new_ledger};

fn join_names(slice: &[Trace]) -> String {
    let mut result = String::new();
    for (i, trace) in slice.iter().enumerate() {
        if i > 0 {
            result.push('+');
        }
        result.push_str(&trace.name);
    }
    result
}

fn main() {
    let ledger = new_ledger();
    let list = vec![
        Trace::new("a", &ledger),
        Trace::new("b", &ledger),
        Trace::new("c", &ledger),
    ];

    println!("full list: {}", join_names(&list));
    println!("first two: {}", join_names(&list[..2]));
    println!("ledger after the calls: [{}]", read_log(&ledger).join(", "));
}
```

```
full list: a+b+c
first two: a+b
ledger after the calls: []
```

The `join_names` function can be called with both `&list` (the entire source) and
`&list[..2]` (a sub-range) — because the parameter type is `&[Trace]`, both fit the same
function. `&list` is **converted** into a slice here: a reference to the growable
array's entirety automatically corresponds to a slice covering it start to end. The
ledger stays empty after both calls too — `join_names` never owned `list`'s elements,
it only read them. This is the concrete example of a function accepting **both the
whole and a part** with the same signature, without asking for ownership.

## A Mutable Slice Changes the Elements It Covers

The slices seen so far have always been shared (`&[Trace]`). A slice can also be taken
as `&mut [Trace]`, and in that case it also carries write permission on the elements it
covers — the Borrowing lesson's shared/mutable distinction applies here exactly as it
was.

```rust
// mutable_slice.rs — a mutable slice can change the elements it covers
mod trace;
use trace::{Trace, new_ledger, read_log};

fn main() {
    let ledger = new_ledger();
    let mut list = vec![
        Trace::new("x", &ledger),
        Trace::new("y", &ledger),
        Trace::new("z", &ledger),
    ];

    {
        let view = &mut list[0..2];
        for trace in view.iter_mut() {
            trace.name.push_str("-new");
        }
    }
    println!("names after the change: {}, {}, {}", list[0].name, list[1].name, list[2].name);
    println!("nothing has ended, ledger: [{}]", read_log(&ledger).join(", "));
}
```

```
names after the change: x-new, y-new, z
nothing has ended, ledger: []
```

`view` here views `list`'s first two elements as mutable; `trace.name.push_str(...)`
inside the loop really changes these two elements' `name` field. The third element
(`z`) is left outside the slice, so it does not change — the output confirms this. No
value ends when the scope closes, because a mutable slice, just like a shared slice,
does not take ownership; it only carries write permission over that range.

In this example too there is only one mutable slice at a time; while `view` was in
scope, no other access to `list` (shared or mutable) could have been opened. Once
`view`'s scope closes, direct indexing like `list[0].name` can be freely used — just as
with the `push` example seen in the Boundary Measurement section, the constraint only
holds for the slice's own scope.

## Length Comes From the Source, and an Empty Slice Is Valid

A slice's length does not carry information on its own — it is computed from the
source. The same range syntax applies on a string too.

```rust
// string_slice.rs — a slice's length derives from the source, and an empty slice is valid too
fn main() {
    let text = String::from("value");
    let full = &text[..];
    let empty = &text[0..0];
    println!("source length: {}", text.len());
    println!("full slice length: {}", full.len());
    println!("empty slice length: {}", empty.len());
    println!("is the empty slice valid: {}", empty.is_empty());
}
```

```
source length: 5
full slice length: 5
empty slice length: 0
is the empty slice valid: true
```

`&text[..]` views the entire range, while `&text[0..0]` views an empty range covering
no characters at all. `full.len()` comes out the same as the source's because the range
runs start to end; `empty.len()` is zero, but `empty` is not an invalid value — it is a
valid slice in its own right, it just covers nothing. The `is_empty()` call confirms
this: an ordinary method called on an empty slice runs without hitting any special
case.

The number `text.len()` gives here is the string's byte length — this, that a string is
itself a byte sequence and diverges from the character count, was already taken up in
the Collections lesson. What this lesson contributes is separate: how the range itself
is built and when it counts as valid. Because `text`'s content carries only ASCII
characters, the byte length coincides with the character count here; the difference
between the two is not this lesson's subject.

**OR12.** A slice having `start == end` is not an error condition, it produces a valid
slice of zero length. This lets a function return an empty range as, for example, the
"matching part," and lets the caller handle it without having to treat it as a separate
special case. `len()`, `is_empty()`, even iterating with a loop (running zero times)
work without a problem on an empty slice — even when the source itself is not empty.

The three questions borrowing has carried across four lessons give the same answers
here too: does the source end (no), is the life extended (no, a slice's scope can never
exceed the source's), and does the shared/mutable distinction apply (yes — a
`&mut [Trace]` slice could also have been taken, and its constraints would have been
identical to the mutable reference in the Borrowing lesson). A slice brings no new
answer to these three questions; the only new thing it has is the **size** of the
range it views — a contiguous set instead of a single value.

## Summary

- A slice (`&[T]`, `&str`) views a contiguous range of the source without taking
  ownership; the source itself never ends because of the slice — the source's life
  ends at its own **scope end**.
- A slice locks not just the range it points to, but the source's **entirety** — the
  source cannot be changed while the slice lives, and a change goes through without a
  problem once the slice ends.
- A slice's length derives from the source; a slice does not carry independent length
  information on its own.
- A slice with `start == end` is valid and carries zero length; an empty slice is not a
  special case.
- Because a slice is a reference too, all the rules of the previous two lessons (the
  shared/mutable distinction, a scope never exceeding the source) apply here as well.
- Writing a function parameter as `&[T]` or `&mut [T]` lets that function accept both
  the source's entirety and just a range of it, with the same signature, without
  taking ownership.

## Next Step

Across these four lessons, borrowing's rules were measured one by one — the whole
value, a part, shared or mutable; each time the source did not end, the life did not
exceed the source, and the one-writer-or-many-readers constraint held. These rules
stayed the same across four separate lessons without ever conflicting, only the
surface they applied to changed. The next lesson turns to the mechanism that enforces
these rules itself: what exactly does the borrow checker test at compile time, and what
kind of choice does this check leave between different pieces of code that do the same
job?
