Smart pointers are variables that contain an address in memory and reference some other data, but they also have additional metadata and capabilities.
Smart pointers in Rust often own the data they point to, while references only borrow data.
Box<T>: Handle recursive types, which the compiler cannot compute their size at compile-time.
Rc<T> (Reference Counted): Keeps track of how many references exist to the data and automatically cleans up when the reference count drops to zero.
Arc<T> (Atomic Reference Counted): Similar like RC but safe to be shared across threads.
Cow<T> (Clone On Write): provide immutable access to borrowed data, and clone the data lazily when mutation or ownership is required.
References:
box1.rs
// At compile time, Rust needs to know how much space a type takes up. This
// becomes problematic for recursive types, where a value can have as part of
// itself another value of the same type. To get around the issue, we can use a
// `Box` - a smart pointer used to store data on the heap, which also allows us
// to wrap a recursive type.
//
// The recursive type we're implementing in this exercise is the "cons list", a
// data structure frequently found in functional programming languages. Each
// item in a cons list contains two elements: The value of the current item and
// the next item. The last item is a value called `Nil`.
// Use a `Box` in the enum definition to make the code compile.
#[derive(PartialEq, Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
// Create an empty cons list.
fn create_empty_list() -> List {
List::Nil
}
// Create a non-empty cons list.
fn create_non_empty_list() -> List {
List::Cons(10, Box::new(List::Nil))
}
fn main() {
println!("This is an empty cons list: {:?}", create_empty_list());
println!(
"This is a non-empty cons list: {:?}",
create_non_empty_list(),
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_empty_list() {
assert_eq!(create_empty_list(), List::Nil);
}
#[test]
fn test_create_non_empty_list() {
assert_ne!(create_empty_list(), create_non_empty_list());
}
}
As the comment stated in this exercise, we need to use Box to wrap the recursive.
So we need to change the enum variant to Cons(i32, Box<List>).
Then complete the create function with List::Nill for empty cons list.
And List::Cons(10, Box::new(List::Nil)) for non-empty cons list.
rc1.rs
// In this exercise, we want to express the concept of multiple owners via the
// `Rc<T>` type. This is a model of our solar system - there is a `Sun` type and
// multiple `Planet`s. The planets take ownership of the sun, indicating that
// they revolve around the sun.
use std::rc::Rc;
#[derive(Debug)]
struct Sun;
#[derive(Debug)]
enum Planet {
Mercury(Rc<Sun>),
Venus(Rc<Sun>),
Earth(Rc<Sun>),
Mars(Rc<Sun>),
Jupiter(Rc<Sun>),
Saturn(Rc<Sun>),
Uranus(Rc<Sun>),
Neptune(Rc<Sun>),
}
impl Planet {
fn details(&self) {
println!("Hi from {self:?}!");
}
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rc1() {
let sun = Rc::new(Sun);
println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference
let mercury = Planet::Mercury(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 2 references
mercury.details();
let venus = Planet::Venus(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 3 references
venus.details();
let earth = Planet::Earth(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 4 references
earth.details();
let mars = Planet::Mars(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 5 references
mars.details();
let jupiter = Planet::Jupiter(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 6 references
jupiter.details();
// Clone the sun
let saturn = Planet::Saturn(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 7 references
saturn.details();
// Clone the sun
let uranus = Planet::Uranus(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 8 references
uranus.details();
// Clone the sun
let neptune = Planet::Neptune(Rc::clone(&sun));
println!("reference count = {}", Rc::strong_count(&sun)); // 9 references
neptune.details();
assert_eq!(Rc::strong_count(&sun), 9);
drop(neptune);
println!("reference count = {}", Rc::strong_count(&sun)); // 8 references
drop(uranus);
println!("reference count = {}", Rc::strong_count(&sun)); // 7 references
drop(saturn);
println!("reference count = {}", Rc::strong_count(&sun)); // 6 references
drop(jupiter);
println!("reference count = {}", Rc::strong_count(&sun)); // 5 references
drop(mars);
println!("reference count = {}", Rc::strong_count(&sun)); // 4 references
// drop earth
drop(earth);
println!("reference count = {}", Rc::strong_count(&sun)); // 3 references
// drop venus
drop(venus);
println!("reference count = {}", Rc::strong_count(&sun)); // 2 references
// drop mercury
drop(mercury);
println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference
assert_eq!(Rc::strong_count(&sun), 1);
}
}
With Rc reference can be shared and have multiple owners.
In this case we only need to use Rc::clone(&sun) instead of creating new Sun.
And then properly drop the planet so the test case at the end will not fail.
arc1.rs
// In this exercise, we are given a `Vec` of `u32` called `numbers` with values
// ranging from 0 to 99. We would like to use this set of numbers within 8
// different threads simultaneously. Each thread is going to get the sum of
// every eighth value with an offset.
//
// The first thread (offset 0), will sum 0, 8, 16, …
// The second thread (offset 1), will sum 1, 9, 17, …
// The third thread (offset 2), will sum 2, 10, 18, …
// …
// The eighth thread (offset 7), will sum 7, 15, 23, …
//
// Each thread should own a reference-counting pointer to the vector of
// numbers. But `Rc` isn't thread-safe. Therefore, we need to use `Arc`.
//
// Don't get distracted by how threads are spawned and joined. We will practice
// that later in the exercises about threads.
// Don't change the lines below.
#![forbid(unused_imports)]
use std::{sync::Arc, thread};
fn main() {
let numbers: Vec<_> = (0..100u32).collect();
// Define `shared_numbers` by using `Arc`.
let shared_numbers = Arc::new(numbers);
let mut join_handles = Vec::new();
for offset in 0..8 {
// Define `child_numbers` using `shared_numbers`.
let child_numbers = Arc::clone(&shared_numbers);
let handle = thread::spawn(move || {
let sum: u32 = child_numbers.iter().filter(|&&n| n % 8 == offset).sum();
println!("Sum of offset {offset} is {sum}");
});
join_handles.push(handle);
}
for handle in join_handles.into_iter() {
handle.join().unwrap();
}
}
This exercise is straightforward, we need to use Arc.
First create shared_numbers using Arc::new(numbers).
Then inside th for block create child_numbers using Arc::clone(&shared_numbers).
cow1.rs
// This exercise explores the `Cow` (Clone-On-Write) smart pointer. It can
// enclose and provide immutable access to borrowed data and clone the data
// lazily when mutation or ownership is required. The type is designed to work
// with general borrowed data via the `Borrow` trait.
use std::borrow::Cow;
fn abs_all(input: &mut Cow<[i32]>) {
for ind in 0..input.len() {
let value = input[ind];
if value < 0 {
// Clones into a vector if not already owned.
input.to_mut()[ind] = -value;
}
}
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reference_mutation() {
// Clone occurs because `input` needs to be mutated.
let vec = vec![-1, 0, 1];
let mut input = Cow::from(&vec);
abs_all(&mut input);
assert!(matches!(input, Cow::Owned(_)));
}
#[test]
fn reference_no_mutation() {
// No clone occurs because `input` doesn't need to be mutated.
let vec = vec![0, 1, 2];
let mut input = Cow::from(&vec);
abs_all(&mut input);
// Replace `todo!()` with `Cow::Owned(_)` or `Cow::Borrowed(_)`.
assert!(matches!(input, Cow::Borrowed(_)));
}
#[test]
fn owned_no_mutation() {
// We can also pass `vec` without `&` so `Cow` owns it directly. In this
// case, no mutation occurs (all numbers are already absolute) and thus
// also no clone. But the result is still owned because it was never
// borrowed or mutated.
let vec = vec![0, 1, 2];
let mut input = Cow::from(vec);
abs_all(&mut input);
// Replace `todo!()` with `Cow::Owned(_)` or `Cow::Borrowed(_)`.
assert!(matches!(input, Cow::Owned(_)));
}
#[test]
fn owned_mutation() {
// Of course this is also the case if a mutation does occur (not all
// numbers are absolute). In this case, the call to `to_mut()` in the
// `abs_all` function returns a reference to the same data as before.
let vec = vec![-1, 0, 1];
let mut input = Cow::from(vec);
abs_all(&mut input);
// Replace `todo!()` with `Cow::Owned(_)` or `Cow::Borrowed(_)`.
assert!(matches!(input, Cow::Owned(_)));
}
}
This exercise will simulate the Cow pattern.
The type Cow is a smart pointer providing clone-on-write functionality: it can enclose and provide immutable access to borrowed data, and clone the data lazily when mutation or ownership is required.
We just need to put proper type match, either Cow::Owned(_) or Cow::Borrowed(_).