bookworm-smart-assistant/skills/rust-engineer/references/ownership.md

6.2 KiB

Ownership, Borrowing, and Lifetimes

Ownership Patterns

// Move semantics (ownership transfer)
fn take_ownership(s: String) {
    println!("{}", s);
} // s dropped here

// Borrowing (immutable reference)
fn borrow(s: &String) {
    println!("{}", s);
} // s NOT dropped, caller still owns

// Mutable borrowing
fn borrow_mut(s: &mut String) {
    s.push_str(" world");
}

// Usage
let s = String::from("hello");
borrow(&s);           // OK, immutable borrow
let mut s2 = s;       // Move, s no longer valid
borrow_mut(&mut s2);  // OK, mutable borrow

Lifetime Annotations

// Explicit lifetime: returned reference lives as long as input
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Multiple lifetimes
fn first_word<'a, 'b>(s: &'a str, _other: &'b str) -> &'a str {
    s.split_whitespace().next().unwrap_or("")
}

// Lifetime in structs
struct Excerpt<'a> {
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    fn announce_and_return(&self, announcement: &str) -> &'a str {
        println!("Attention: {}", announcement);
        self.part
    }
}

// Static lifetime (lives for entire program)
const GREETING: &'static str = "Hello, world!";

Smart Pointers

use std::rc::Rc;
use std::cell::RefCell;
use std::sync::{Arc, Mutex};

// Box: heap allocation, single owner
let b = Box::new(5);

// Rc: reference counting (single-threaded)
let rc1 = Rc::new(vec![1, 2, 3]);
let rc2 = Rc::clone(&rc1);  // Increment count
println!("Count: {}", Rc::strong_count(&rc1));  // 2

// Arc: atomic reference counting (thread-safe)
let arc1 = Arc::new(vec![1, 2, 3]);
let arc2 = Arc::clone(&arc1);
std::thread::spawn(move || {
    println!("{:?}", arc2);
});

// RefCell: interior mutability (runtime borrow checking)
let data = RefCell::new(5);
*data.borrow_mut() += 1;  // Mutable borrow at runtime

// Combining Rc + RefCell for shared mutable state
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
shared.borrow_mut().push(4);

// Combining Arc + Mutex for thread-safe shared state
let counter = Arc::new(Mutex::new(0));
let counter_clone = Arc::clone(&counter);
std::thread::spawn(move || {
    let mut num = counter_clone.lock().unwrap();
    *num += 1;
});

Interior Mutability

use std::cell::{Cell, RefCell};

// Cell: Copy types only
let c = Cell::new(5);
c.set(10);
let val = c.get();

// RefCell: runtime borrow checking
let data = RefCell::new(vec![1, 2, 3]);
data.borrow_mut().push(4);

// Pattern: mock objects with interior mutability
struct MockLogger {
    messages: RefCell<Vec<String>>,
}

impl MockLogger {
    fn new() -> Self {
        Self { messages: RefCell::new(Vec::new()) }
    }

    fn log(&self, msg: &str) {
        self.messages.borrow_mut().push(msg.to_string());
    }

    fn get_messages(&self) -> Vec<String> {
        self.messages.borrow().clone()
    }
}

Pin and Self-Referential Types

use std::pin::Pin;
use std::marker::PhantomPinned;

// Self-referential struct (requires Pin)
struct SelfReferential {
    data: String,
    pointer: *const String,
    _pin: PhantomPinned,
}

impl SelfReferential {
    fn new(data: String) -> Pin<Box<Self>> {
        let mut boxed = Box::pin(Self {
            data,
            pointer: std::ptr::null(),
            _pin: PhantomPinned,
        });

        // Safe: we're not moving the data after this
        let ptr = &boxed.data as *const String;
        unsafe {
            let mut_ref = Pin::as_mut(&mut boxed);
            Pin::get_unchecked_mut(mut_ref).pointer = ptr;
        }

        boxed
    }
}

// Pin in async contexts
async fn pinned_future() {
    // Futures are often self-referential, hence Pin
    let fut = async { 42 };
    let pinned = Box::pin(fut);
    pinned.await;
}

Cow (Clone on Write)

use std::borrow::Cow;

fn process_text(input: &str) -> Cow<str> {
    if input.contains("bad") {
        // Need to modify: allocate new String
        Cow::Owned(input.replace("bad", "good"))
    } else {
        // No modification needed: just borrow
        Cow::Borrowed(input)
    }
}

// Usage
let text1 = "hello world";
let result1 = process_text(text1);  // Borrowed (no allocation)

let text2 = "bad word";
let result2 = process_text(text2);  // Owned (allocated)

Drop Trait and RAII

struct FileGuard {
    name: String,
}

impl FileGuard {
    fn new(name: String) -> Self {
        println!("Opening {}", name);
        Self { name }
    }
}

impl Drop for FileGuard {
    fn drop(&mut self) {
        println!("Closing {}", self.name);
    }
}

// Usage: automatic cleanup
{
    let _file = FileGuard::new("data.txt".to_string());
    // Use file...
} // Drop called automatically here

Common Patterns

// Builder pattern with ownership
struct Config {
    host: String,
    port: u16,
}

impl Config {
    fn builder() -> ConfigBuilder {
        ConfigBuilder::default()
    }
}

struct ConfigBuilder {
    host: Option<String>,
    port: Option<u16>,
}

impl ConfigBuilder {
    fn host(mut self, host: impl Into<String>) -> Self {
        self.host = Some(host.into());
        self
    }

    fn port(mut self, port: u16) -> Self {
        self.port = Some(port);
        self
    }

    fn build(self) -> Result<Config, &'static str> {
        Ok(Config {
            host: self.host.ok_or("host required")?,
            port: self.port.unwrap_or(8080),
        })
    }
}

// Usage
let config = Config::builder()
    .host("localhost")
    .port(3000)
    .build()?;

Best Practices

  • Prefer borrowing (&T) over ownership transfer when possible
  • Use &str over String for function parameters
  • Use &[T] over Vec for function parameters
  • Clone only when necessary (profile first)
  • Use Cow<'a, T> for conditional cloning
  • Document lifetime relationships in complex cases
  • Use Arc<Mutex> for shared mutable state across threads
  • Use Rc<RefCell> for shared mutable state in single thread
  • Implement Drop for RAII patterns
  • Use PhantomData to constrain variance when needed