6.5 Slices: Borrowing Portions of Data

Slices are references to a segment of a collection, allowing you to access parts of data without owning it or making unnecessary copies. This makes working with subsets of data efficient and safe.

6.5.1 String Slices

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

String slices (&str) are references to a sequence of UTF-8 bytes within a String. They allow you to work with parts of a string without taking ownership.

6.5.2 Array Slices

#![allow(unused)]
fn main() {
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4]; // [2, 3, 4]
}

6.5.3 Slices in Functions

Slices are commonly used in function parameters to allow functions to work with parts of data without taking ownership, making functions more flexible.

fn sum(slice: &[i32]) -> i32 {
    slice.iter().sum()
}

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

    // Passing a slice of the array (partial array)
    let partial_result = sum(&arr[1..4]);
    println!("Sum of slice is {}", partial_result);

    // Passing the whole array as a slice
    let total_result = sum(&arr);
    println!("Sum of entire array is {}", total_result);
}

Explanation:

  • Function Definition:

    • The sum function takes a slice of i32 values (&[i32]) and returns their sum.
    • The function operates on the slice without taking ownership, allowing it to accept any segment of an array or vector.
  • In main:

    • We define an array arr containing five integers.
    • Passing a Partial Slice:
      • We pass a slice of the array to sum using &arr[1..4], which includes elements at indices 1 to 3 (2, 3, 4).
      • The partial_result calculates the sum of this slice.
    • Passing the Whole Array:
      • We pass the entire array to sum using &arr without specifying a range.
      • The total_result calculates the sum of all elements in the array.

By using slices, functions can operate on data without taking ownership, allowing them to accept both entire arrays and portions of arrays seamlessly.

Benefits:

  • Flexibility: The same function can operate on both full arrays and subarrays without any modification.
  • Efficiency: Since slices are references, they avoid unnecessary copying of data.
  • Safety: Rust ensures that slices do not outlive the data they reference, preventing dangling references.

Additional Example with String Slices:

fn print_slice(slice: &str) {
    println!("Slice: {}", slice);
}

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

    // Passing a substring
    print_slice(&s[0..5]); // Outputs: Slice: hello

    // Passing the whole string
    print_slice(&s);       // Outputs: Slice: hello world
}

Key Takeaways:

  • Passing the Whole Collection:

    • You can pass the entire array or string to a function expecting a slice by referencing it with &arr or &s.
  • Automatic Coercion:

    • Rust automatically coerces arrays and strings to slices when you pass them by reference to functions expecting slices.
  • No Need for Full Range Specification:

    • Specifying the full range like &arr[0..arr.len()] is unnecessary; &arr suffices.

Comparison with C

In C, you use pointers and manual length management:

#include <stdio.h>

void sum(int *slice, int length) {
    int total = 0;
    for(int i = 0; i < length; i++) {
        total += slice[i];
    }
    printf("Sum is %d\n", total);
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    sum(&arr[1], 3);
    return 0;
}

C does not perform bounds checking, whereas Rust slices include length information and are bounds-checked at runtime.