Files
Sprimo/skills/m01-ownership/examples/best-practices.md
2026-02-12 22:58:33 +08:00

6.5 KiB

Ownership Best Practices

API Design Patterns

1. Prefer Borrowing Over Ownership

// BAD: takes ownership unnecessarily
fn print_name(name: String) {
    println!("Name: {}", name);
}

// GOOD: borrows instead
fn print_name(name: &str) {
    println!("Name: {}", name);
}

// Caller benefits:
let name = String::from("Alice");
print_name(&name);  // can reuse name
print_name(&name);  // still valid

2. Return Owned Values from Constructors

// GOOD: return owned value
impl User {
    fn new(name: &str) -> Self {
        User {
            name: name.to_string(),
        }
    }
}

// GOOD: accept Into<String> for flexibility
impl User {
    fn new(name: impl Into<String>) -> Self {
        User {
            name: name.into(),
        }
    }
}

// Usage:
let u1 = User::new("Alice");        // &str
let u2 = User::new(String::from("Bob"));  // String

3. Use AsRef for Generic Borrowing

// GOOD: accepts both &str and String
fn process<S: AsRef<str>>(input: S) {
    let s = input.as_ref();
    println!("{}", s);
}

process("literal");           // &str
process(String::from("owned")); // String
process(&String::from("ref")); // &String

4. Cow for Clone-on-Write

use std::borrow::Cow;

// Return borrowed when possible, owned when needed
fn maybe_modify(s: &str, uppercase: bool) -> Cow<'_, str> {
    if uppercase {
        Cow::Owned(s.to_uppercase())  // allocates
    } else {
        Cow::Borrowed(s)  // zero-cost
    }
}

let input = "hello";
let result = maybe_modify(input, false);
// result is borrowed, no allocation

Struct Design Patterns

1. Owned Fields vs References

// Use owned fields for most cases
struct User {
    name: String,
    email: String,
}

// Use references only when lifetime is clear
struct UserView<'a> {
    name: &'a str,
    email: &'a str,
}

// Pattern: owned data + view for efficiency
impl User {
    fn view(&self) -> UserView<'_> {
        UserView {
            name: &self.name,
            email: &self.email,
        }
    }
}

2. Builder Pattern with Ownership

#[derive(Default)]
struct RequestBuilder {
    url: Option<String>,
    method: Option<String>,
    body: Option<Vec<u8>>,
}

impl RequestBuilder {
    fn new() -> Self {
        Self::default()
    }

    // Take self by value for chaining
    fn url(mut self, url: impl Into<String>) -> Self {
        self.url = Some(url.into());
        self
    }

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

    fn build(self) -> Result<Request, Error> {
        Ok(Request {
            url: self.url.ok_or(Error::MissingUrl)?,
            method: self.method.unwrap_or_else(|| "GET".to_string()),
            body: self.body.unwrap_or_default(),
        })
    }
}

// Usage:
let req = RequestBuilder::new()
    .url("https://example.com")
    .method("POST")
    .build()?;

3. Interior Mutability When Needed

use std::cell::RefCell;
use std::rc::Rc;

// Shared mutable state in single-threaded context
struct Counter {
    value: Rc<RefCell<u32>>,
}

impl Counter {
    fn new() -> Self {
        Counter {
            value: Rc::new(RefCell::new(0)),
        }
    }

    fn increment(&self) {
        *self.value.borrow_mut() += 1;
    }

    fn get(&self) -> u32 {
        *self.value.borrow()
    }

    fn clone_handle(&self) -> Self {
        Counter {
            value: Rc::clone(&self.value),
        }
    }
}

Collection Patterns

1. Efficient Iteration

let items = vec![1, 2, 3, 4, 5];

// Iterate by reference (no move)
for item in &items {
    println!("{}", item);
}

// Iterate by mutable reference
for item in &mut items.clone() {
    *item *= 2;
}

// Consume with into_iter when done
let sum: i32 = items.into_iter().sum();

2. Collecting Results

// Collect into owned collection
let strings: Vec<String> = (0..5)
    .map(|i| format!("item_{}", i))
    .collect();

// Collect references
let refs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();

// Collect with transformation
let result: Result<Vec<i32>, _> = ["1", "2", "3"]
    .iter()
    .map(|s| s.parse::<i32>())
    .collect();

3. Entry API for Maps

use std::collections::HashMap;

let mut map: HashMap<String, Vec<i32>> = HashMap::new();

// Efficient: don't search twice
map.entry("key".to_string())
   .or_insert_with(Vec::new)
   .push(42);

// With entry modification
map.entry("key".to_string())
   .and_modify(|v| v.push(43))
   .or_insert_with(|| vec![43]);

Error Handling with Ownership

1. Preserve Context in Errors

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct ParseError {
    input: String,  // owns the problematic input
    message: String,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Failed to parse '{}': {}", self.input, self.message)
    }
}

fn parse(input: &str) -> Result<i32, ParseError> {
    input.parse().map_err(|_| ParseError {
        input: input.to_string(),  // clone for error context
        message: "not a valid integer".to_string(),
    })
}

2. Ownership in Result Chains

fn process_data(path: &str) -> Result<ProcessedData, Error> {
    let content = std::fs::read_to_string(path)?;  // owned String
    let parsed = parse_content(&content)?;          // borrow
    let processed = transform(parsed)?;             // ownership moves
    Ok(processed)                                   // return owned
}

Performance Considerations

1. Avoid Unnecessary Clones

// BAD: cloning just to compare
fn contains_item(items: &[String], target: &str) -> bool {
    items.iter().any(|s| s.clone() == target)  // unnecessary clone
}

// GOOD: compare references
fn contains_item(items: &[String], target: &str) -> bool {
    items.iter().any(|s| s == target)  // String implements PartialEq<str>
}

2. Use Slices for Flexibility

// BAD: requires Vec
fn sum(numbers: &Vec<i32>) -> i32 {
    numbers.iter().sum()
}

// GOOD: accepts any slice
fn sum(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

// Now works with:
sum(&vec![1, 2, 3]);     // Vec
sum(&[1, 2, 3]);         // array
sum(&array[1..3]);       // slice

3. In-Place Mutation

// BAD: allocates new String
fn make_uppercase(s: &str) -> String {
    s.to_uppercase()
}

// GOOD when you own the data: mutate in place
fn make_uppercase(mut s: String) -> String {
    s.make_ascii_uppercase();  // in-place for ASCII
    s
}