Ownership Practice | Rust - Wyatt's Notes
Ownership Practice
Exercise 1: Reference Counting
Problem: Use Rc<T> to share ownership of a value across multiple structures.
Hint: Rc::clone increments the reference count; the value is dropped when the last Rc is dropped.
use std::rc::Rc;
#[derive(Debug)]
struct Node \{
value: i32,
next: Option<Rc<Node>>,
\}
fn shared_list() -> (Rc<Node>, Rc<Node>) \{
let tail = Rc::new(Node \{
value: 3,
next: None,
\});
let head = Rc::new(Node \{
value: 1,
next: Some(Rc::clone(&tail)),
\});
(head, tail)
\}
fn count_references(node: &Rc<Node>) -> usize \{
Rc::strong_count(node)
\}
Exercise 2: Lifetime Annotations
Problem: Write a function that returns the longer of two string slices, using explicit lifetime annotations.
Hint: The returned slice borrows from one of the inputs; the lifetime must outlive both.
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str \{
if s1.len() >= s2.len() \{
s1
\} else \{
s2
\}
\}
struct Excerpt<'a> \{
text: &'a str,
\}
impl<'a> Excerpt<'a> \{
fn new(text: &'a str) -> Self \{
Excerpt \{ text \}
\}
fn level(&self) -> i32 \{
3
\}
fn announce(&self, announcement: &str) -> &str \{
println!("Attention: \{\}", announcement);
self.text
\}
\}
Exercise 3: Trait Objects with Ownership
Problem: Store heterogeneous objects behind a Box<dyn Trait> in a collection.
Hint: Box<dyn Trait> is a fat pointer; each element can be a different concrete type.
trait Shape \{
fn area(&self) -> f64;
fn describe(&self) -> String;
\}
struct Circle \{
radius: f64,
\}
impl Shape for Circle \{
fn area(&self) -> f64 \{
std::f64::consts::PI * self.radius * self.radius
\}
fn describe(&self) -> String \{
format!("Circle with radius \{\}", self.radius)
\}
\}
struct Rectangle \{
width: f64,
height: f64,
\}
impl Shape for Rectangle \{
fn area(&self) -> f64 \{
self.width * self.height
\}
fn describe(&self) -> String \{
format!("Rectangle \{\}x\{\}", self.width, self.height)
\}
\}
fn create_shapes() -> Vec<Box<dyn Shape>> \{
vec![
Box::new(Circle \{ radius: 5.0 \}),
Box::new(Rectangle \{
width: 4.0,
height: 6.0,
\}),
]
\}
Exercise 4: Self-Referential Structs
Problem: Use the ouroboros crate or manual unsafe code to build a self-referential struct.
Hint: The ouroboros crate provides a safe macro for self-referential types.
use ouroboros::self_referencing;
#[self_referencing]
struct TextBuffer \{
data: String,
#[borrows(data)]
#[covariant]
lines: Vec<&'this str>,
\}
impl TextBuffer \{
fn new(text: String) -> Self \{
TextBufferBuilder \{
data: text,
lines_builder: |data: &str| data.lines().collect(),
\}
.build()
\}
fn lines(&self) -> Vec<&str> \{
self.borrow_lines().clone()
\}
\}
fn process_text(input: String) -> Vec<String> \{
let buffer = TextBuffer::new(input);
buffer.lines().iter().map(|s| s.to_uppercase()).collect()
\}
Exercise 5: Unsafe Rust Patterns
Problem: Implement a fixed-size array wrapper that provides safe indexing with bounds checking, backed by unsafe code internally.
Hint: Use unsafe to access raw pointers but validate bounds before dereferencing.
struct SafeArray<T, const N: usize> \{
data: [std::mem::MaybeUninit<T>; N],
len: usize,
\}
impl<T, const N: usize> SafeArray<T, N> \{
fn new() -> Self \{
SafeArray \{
data: unsafe \{ std::mem::MaybeUninit::uninit().assume_init() \},
len: 0,
\}
\}
fn push(&mut self, value: T) -> Result<(), &'static str> \{
if self.len >= N \{
return Err("array is full");
\}
self.data[self.len] = std::mem::MaybeUninit::new(value);
self.len += 1;
Ok(())
\}
fn get(&self, index: usize) -> Option<&T> \{
if index < self.len \{
Some(unsafe \{ self.data[index].assume_init_ref() \})
\} else \{
None
\}
\}
\}
impl<T, const N: usize> Drop for SafeArray<T, N> \{
fn drop(&mut self) \{
for i in 0..self.len \{
unsafe \{
self.data[i].assume_init_drop();
\}
\}
\}
\}
Intuition
Rust’s ownership system ensures memory safety without garbage collection: Every value has exactly one owner, and the value is dropped when the owner goes out of scope. Borrowing lets you use values without taking ownership, with strict rules that prevent data races.
Why it matters: Ownership eliminates memory leaks, dangling pointers, and data races at compile time — bugs that plague C and C++ programs.
The key insight: The borrow checker is not fighting you — it is preventing bugs that would otherwise require runtime detection or cause subtle memory corruption.
Common Mistakes
Cloning to avoid borrow checker issues: .clone() copies data, which is expensive for large structures. Its a valid workaround but often indicates a design problem. Restructure code to use references or interior mutability patterns instead.
Confusing Copy and Clone: Copy is implicit bitwise copying (stack-only types like i32, bool). Clone is explicit deep copying (can be expensive). Types that implement Copy are automatically cloned when moved. Dont assume all types can be Copy.
Ignoring lifetime annotations: Lifetimes tell the compiler how long references are valid. When a function returns a reference, the lifetime annotation indicates which input the output is tied to. Omitting annotations when required causes compilation errors.