Rust track

Lesson 08 · Part 2: Ownership, Borrowing & Lifetimes

Slices

Slice types and string/array slicing

Part 2: Ownership, Borrowing & Lifetimes4 min readFree lesson

A slice is a reference to a contiguous part of a collection. Strings are expressed as a slice type called &str, and arrays and vectors are expressed as a slice type called &[T], and in both cases, they operate as a small structure of “pointer length”. As you learn about slicing, function signatures become much more natural.

Learning Objectives

  • Understand the difference between &str and String and prefer &str as a function parameter.
  • Some sections of the array/vector are borrowed and processed as &[T].
  • Memorize [start..end] / [..end] / [start..] / [..] slicing grammar.
  • Write a function that returns a slice with a pattern such as first_word.

Key concepts

A slice is a small reference type with two fields: pointer length. Point to the original data rather than copying it.

Slice Type Pointing to original
&str String, &'static str (string literal)
&[T] [T; N] array, Vec<T>, or another slice
let s = String::from("hello world");
&s[0..5];   // "hello"
&s[6..];    // "world"
&s[..];     //All

let a = [10, 20, 30, 40];
&a[1..3];   // [20, 30]

In a string slice, the range is in bytes. If you truncate outside of a UTF-8 character boundary, a runtime panic occurs.

When a function receives a slice type, the caller can pass String, an array, or a vector as is, providing good compatibility.

parameters Compatible Calls
&str &String, &'static str("...")
&[T] &[T; N], &Vec<T>
fn first_word(s: &str) -> &str {
    for (i, &b) in s.as_bytes().iter().enumerate() {
        if b == b' ' { return &s[..i]; }
    }
    s
}

The returned &str points to a portion of the input s. Lifetime is automatically inferred from the input (more details in the next part, Lesson 9).

See as example

//Part 8 Example 1: String Slice
//
//A type that borrows part of String
//The string literal "abc" is actually

fn main() {
    let s = String::from("Hello, World!");

    //The range is [start..end). Except the end.
    let hello: &str = &s[0..5];
    let world: &str = &s[7..12];
    let all: &str = &s[..]; //All

    println!("hello = {hello}");
    println!("world = {world}");
    println!("all   = {all}");

    //
    print_len("Rust");
    print_len(&s);
}

fn print_len(s: &str) {
    println!("Byte length of \"{s}\": {}", s.len());
}
//Part 8 Example 2: Array/Vector Slice
//
//A type that borrows consecutive parts of an array or vector

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

fn main() {
    let arr = [10, 20, 30, 40, 50];
    let vec = vec![1, 2, 3, 4, 5];

    //Borrowing the entire array/vector as a slice
    println!("Total array sum = {}", sum(&arr));
    println!("vector total sum = {}", sum(&vec));

    //Rent a section
    let mid: &[i32] = &arr[1..4]; // 20, 30, 40
    println!("arr[1..4] = {:?}, sum = {}", mid, sum(mid));
}
//Part 8 Example 3: first_word — A classic example of a slice
//
//Returns the slice up to the first space in a string.
//Slice is safer than returning only the index (usize).

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

fn main() {
    let sentence = String::from("Rust is a system language");
    let word = first_word(&sentence);
    println!("First word: {word}");

    //Can also be applied to string literals
    let lit = "Hello World";
    println!("First Word (lit): {}", first_word(lit));
}

Common Mistakes

A. String slicing is byte-by-byte. UTF-8 If you truncate a character in the middle, it will panic. To handle it safely, you need to find the character boundary with s.char_indices() and then cut it (this will be covered in depth in Part 10 of the next part).

A. The borrowing rules prevent that. If you try to modify s while &s[0..5] is alive, you will get a compile error — "cannot borrow as mutable".

A. It is almost always &[T]. The caller can call it from any array, vector, or other slice, and library compatibility is good.

A: Yes. &mut [T] is a mutable slice whose slice elements can be modified. Example: arr.sort() internally uses &mut [T].

Cleanup

  • A slice &str / &[T] is a small reference to a part of a collection.
  • Slicing grammar is [start..end], [..end], [start..], [..].
  • It is more common to receive function parameters as &str instead of String and &[T] instead of Vec<T>.
  • String slicing is byte-by-byte — panic when cutting in the middle of a multibyte character.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/slices/Cargo.tomlTOML
[package]
name = "slices"
version = "0.1.0"
edition = "2021"
default-run = "slices"

[dependencies]
src/slices/src/main.rsRust
fn main() {
    println!("This crate has three running examples.");
    println!();
    println!("  cargo run --bin ex01_str_slice");
    println!("  cargo run --bin ex02_arr_slice");
    println!("  cargo run --bin ex03_first_word");
}

Assignment

  • File name: homework_01/src/main.rs
  • Core concepts: &str, split_whitespace, nth

Requirements

  • Write function nth_word(s: &str, n: usize) -> Option<&str>.
  • Returns the nth blank word starting from 0. If not, None.
  • In main, the results are output to the following input.
  • Input: "Rust is a systems language", n = 0, 1, 2, 10

Expected output

0th word: Some("Rust")
1th word: Some("is")
2nd word: Some("a")
10th word: None

Hint

  • s.split_whitespace().nth(n).

  • Lifetime is automatically inferred from the return type (refer to Part 9).

  • File name: homework_02/src/main.rs

  • Key concepts: &[f64], average calculation

Requirements

  • Write function average(values: &[f64]) -> f64 (if it is an empty slice, it returns 0.0).
  • Check the following in main.
  • Average of array [10.0, 20.0, 30.0, 40.0]
  • Average of empty slice &[]

Expected output

[10.0, 20.0, 30.0, 40.0] = 25.00 Average of
empty slice = 0.00

Hint

  • Branch to values.is_empty().
  • values.iter().sum::<f64>() / values.len() as f64.

Check the correct answer

After solving it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key points:
//- `Option<
//- `split_whitespace().nth(n)` is `Option<

fn nth_word(s: &str, n: usize) -> Option<&str> {
    s.split_whitespace().nth(n)
}

fn main() {
    let text = "Rust is a systems language";

    println!("Word 0: {:?}", nth_word(text, 0));
    println!("1st word: {:?}", nth_word(text, 1));
    println!("2nd word: {:?}", nth_word(text, 2));
    println!("10th word: {:?}", nth_word(text, 10));
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key points:
//- Put the `is_empty()` branch first to avoid attempting division on empty slices.
//- `sum::<f64>()` can be inferred only when the result type is specified.

fn average(values: &[f64]) -> f64 {
    if values.is_empty() {
        return 0.0;
    }
    values.iter().sum::<f64>() / values.len() as f64
}

fn main() {
    let xs = [10.0, 20.0, 30.0, 40.0];
    println!("Average of {:?} = {:.2}", xs, average(&xs));

    let empty: [f64; 0] = [];
    println!("Average of empty slices = {:.2}", average(&empty));
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now