After mastering structs, enums, arrays, and vectors, the next layer of Rust's data model introduces powerful abstractions that unlock advanced expressiveness. Slices provide safe views into contiguous data. Strings come in two forms with distinct ownership characteristics. Tuples bundle heterogeneous values. Pattern matching extends to destructuring. Generics allow you to write code that works for any type while remaining fully type-safe.
Slices
A slice is a reference to a contiguous sequence of elements in a collection. Unlike an array or vector, a slice does not own the data — it is a view into existing data. Its size is not known at compile time, making it a dynamically-sized type.
// A slice borrows a portion of an array or vector
fn use_slice(slice: &mut [i32]) {
slice[0] = 321;
println!("first = {}, len = {}", slice[0], slice.len());
// first = 321, len = 3
}
fn slices() {
let mut data = [1, 2, 3, 4, 5];
use_slice(&mut data[1..4]); // slice of elements 1, 2, 3 (indices 1 to 3)
println!("{:?}", data);
// [1, 321, 3, 4, 5] (data[1] was modified through the slice)
}
Array in memory: [1] [2] [3] [4] [5]
Slice data[1..4]: [2] [3] [4]
^---------^
slice points here, len = 3
Slice Types
| Type | Description |
|---|---|
&[T] | Immutable slice of T |
&mut [T] | Mutable slice of T |
&str | String slice (immutable view into string data) |
Slices are the idiomatic way to pass portions of arrays or vectors to functions without transferring ownership or copying data.
Strings
Rust has two string types, each serving a different purpose. This is one of the most important distinctions for new Rust programmers:
`&str` — String Slice
&str is an immutable reference to string data stored somewhere in memory (often in the program's binary as a string literal). It is not an owned type.
let s: &'static str = "hello there!"; // &str = string slice
// s = "abc"; // ERROR: cannot reassign a string slice
// Iterate over characters
for c in s.chars() {
print!("{} ", c);
}
println!();
// Access a specific character by index
if let Some(first) = s.chars().nth(0) {
println!("first char: {}", first); // first char: h
}
The 'static lifetime means the string data lives for the entire program duration (it is stored in the binary).
`String` — Owned, Heap-Allocated String
String is a growable, heap-allocated string that you own and can modify:
fn strings() {
// Build a String by pushing characters
let mut letters = String::new();
let mut a = 'a' as u8;
while a <= ('z' as u8) {
letters.push(a as char);
letters.push_str(",");
a += 1;
}
println!("{}", letters);
// a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,
// Create from a string literal
let mut abc = String::from("hello world");
abc.remove(0); // removes 'h'
abc.push_str("!!");
println!("{}", abc.replace("ello", "Goodbye"));
// Goodbye world!!
}
Converting Between `&str` and `String`
let s: &str = "hello";
// &str -> String
let owned: String = s.to_string();
let owned: String = String::from(s);
// String -> &str
let borrowed: &str = &owned;
String vs `&str` at a Glance
| Property | &str | String |
|---|---|---|
| Ownership | Borrowed (no ownership) | Owned |
| Memory | Stack (pointer + length) | Heap-allocated buffer |
| Mutability | Immutable | Mutable |
| Allocation | None | Dynamic |
| Use case | String literals, slices | Building/modifying strings |
String Concatenation
let s1 = String::from("Hello, ");
let s2 = String::from("Rust!");
// + operator moves s1 and borrows s2
let s3 = s1 + &s2; // s1 is moved, no longer valid
println!("{}", s3); // Hello, Rust!
// format! is cleaner for multiple strings (no ownership transfer)
let s4 = format!("{} {}", "Hello", "World");
Tuples
A tuple is a fixed-length collection of values of different types. Tuples are the idiomatic way to return multiple values from a function in Rust:
fn sum_and_product(x: i32, y: i32) -> (i32, i32) {
(x + y, x * y)
}
fn tuples() {
let x = 3;
let y = 4;
let sp = sum_and_product(x, y);
println!("sp = {:?}", sp); // sp = (7, 12)
println!("{0} + {1} = {2}, {0} * {1} = {3}",
x, y, sp.0, sp.1); // 3 + 4 = 7, 3 * 4 = 12
}
Destructuring Tuples
Tuples can be unpacked into individual variables:
let sp = sum_and_product(3, 4); // (7, 12)
// Destructuring assignment
let (a, b) = sp;
println!("a = {}, b = {}", a, b); // a = 7, b = 12
Nested Tuples
let sp1 = (7, 12);
let sp2 = (11, 28);
let combined = (sp1, sp2);
println!("{:?}", combined); // ((7, 12), (11, 28))
println!("{}", (combined.1).1); // 28
// Nested destructuring
let ((c, d), (e, f)) = combined;
Heterogeneous Tuples
let foo: (bool, f64, i8) = (true, 42.0, -1);
println!("{:?}", foo); // (true, 42.0, -1)
Distinguishing Single-Element Tuples
let not_a_tuple = (42); // This is just i32 = 42
let is_a_tuple = (42,); // This is (i32,) = a tuple with one element
The trailing comma is the syntactic marker that makes a single-element tuple.
Pattern Matching
Rust's match statement extends far beyond simple value matching. Pattern matching can destructure structs, enums, tuples, and more:
Matching with OR, Ranges, and Tagged Ranges
fn how_many(x: i32) -> &'static str {
match x {
0 => "no",
1 | 2 => "one or two", // OR pattern
12 => "a dozen",
9..=11 => "almost a dozen", // range (inclusive)
_ if x % 2 == 0 => "some even number", // guard condition
_ => "few"
}
}
Matching on Tuples
fn classify_point() {
let point = (3, 4);
match point {
(0, 0) => println!("origin"),
(0, y) => println!("on y-axis at {}", y),
(x, 0) => println!("on x-axis at {}", x),
(x, y) => println!("at ({}, {})", x, y),
}
// at (3, 4)
}
Destructuring Structs in Match
struct Point { x: i32, y: i32 }
let p = Point { x: 0, y: 7 };
match p {
Point { x: 0, y } => println!("on y-axis at {}", y),
Point { x, y: 0 } => println!("on x-axis at {}", x),
Point { x, y } => println!("at ({}, {})", x, y),
}
// on y-axis at 7
Generics
Generics allow you to write functions, structs, and enums that work with any type while remaining fully type-safe at compile time. Generics are resolved at compile time (monomorphization), so they have zero runtime overhead.
Generic Structs
// A Point that works with any numeric type
struct Point<T> {
x: T,
y: T,
}
// A Points struct with two different type parameters
struct Points<T, V> {
x: T,
y: V,
}
struct Line<T> {
start: Point<T>,
end: Point<T>,
}
Using Generic Structs
fn generics() {
let a: Points<u16, i32> = Points { x: 0, y: 4 }; // u16 x, i32 y
let b: Points<f64, f64> = Points { x: 1.2, y: 3.4 }; // both f64
let c: Point<i32> = Point { x: 1, y: 2 };
let d: Point<i32> = Point { x: 3, y: 4 };
let myline = Line { start: c, end: d };
// Generic struct for floats
let e: Point<f64> = Point { x: 1.5, y: 2.5 };
}
Generic Functions
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
let numbers = vec![34, 50, 25, 100, 65];
println!("largest = {}", largest(&numbers)); // 100
let chars = vec!['y', 'm', 'a', 'q'];
println!("largest = {}", largest(&chars)); // y
The T: PartialOrd is a trait bound — it tells the compiler that T must implement the PartialOrd trait (which provides the > operator). This is how Rust's generics enforce type safety.
Generics in the Standard Library
The Option<T> and Result<T, E> types you use constantly are themselves generic enums:
pub enum Option<T> {
None,
Some(T),
}
pub enum Result<T, E> {
Ok(T),
Err(E),
}
This is the same generic system you use when defining your own types.
Summary: Advanced Data Structures
+--------------------+--------------------------------------------------+
| Type | Key Characteristics |
+--------------------+--------------------------------------------------+
| &[T] | Borrowed view into array/vec, no allocation |
| &str | Borrowed string slice, immutable |
| String | Owned, heap-allocated, mutable string |
| (T, U, ...) | Fixed heterogeneous collection, stack-allocated |
| Pattern matching | Destructure, guard, OR, range in match arms |
| Generics<T> | Write once, work for any type, zero-cost |
+--------------------+--------------------------------------------------+
Conclusion
Rust's advanced data structures unlock a level of expressiveness that rivals any high-level language while maintaining systems-level performance. Slices provide zero-cost views into data. The two string types (&str and String) enforce the distinction between borrowing and owning at the type level. Tuples enable clean multi-return from functions without defining a struct. Advanced pattern matching makes complex data destructuring safe and exhaustive. Generics let you write highly reusable code with compile-time type guarantees and zero runtime overhead.
Together, these tools reflect Rust's core philosophy: maximum expressiveness and safety, at zero additional cost.