Skip to content

Q038. When should an API take ownership, borrow a value, or use shared ownership?

Short Answer

Take ownership when the callee must store, consume, transform, or transfer a value independently of the caller. Borrow with &T for temporary observation and &mut T for temporary in-place mutation. Use Rc or Arc only when multiple owners genuinely need independent lifetimes. Good signatures expose retention and cost instead of hiding clones or reference counting.

Deep Dive

Prefer the least powerful ownership contract that satisfies the implementation:

  • accept &str rather than String when only reading text during the call;
  • accept String or impl Into<String> when the callee must retain owned text;
  • accept &mut T when mutation should remain visible through the caller's value;
  • return an owned T when the result must outlive borrowed inputs;
  • use Cow<'a, T> when an API usually borrows but occasionally needs an owned modification;
  • use Rc<T> for single-threaded shared ownership and Arc<T> for cross-thread shared ownership.

Shared ownership introduces runtime counting and possible cycles, so it should model a real lifecycle requirement rather than bypass ownership design. APIs should also avoid cloning internally merely to simplify lifetime signatures without documenting the cost.

Internal Model

Borrowed APIs carry compiler-checked lifetime relationships and usually no ownership bookkeeping. Owned parameters transfer drop responsibility to the callee. Rc and Arc clone operations update runtime counters, and cleanup occurs when the final strong owner is dropped.

The signature lets callers predict allocation, retention, mutation, and synchronization boundaries before reading the implementation.

Example

rust
struct Registry {
    names: Vec<String>,
}

impl Registry {
    fn add(&mut self, name: impl Into<String>) {
        self.names.push(name.into());
    }

    fn contains(&self, name: &str) -> bool {
        self.names.iter().any(|stored| stored == name)
    }
}

fn main() {
    let mut registry = Registry { names: Vec::new() };
    registry.add("worker");

    println!("{}", registry.contains("worker"));
}

Common Mistakes

  • Taking ownership when a short-lived borrow fully satisfies the operation.
  • Adding Arc<Mutex<T>> by default instead of identifying actual sharing and mutation requirements.
  • Hiding expensive clones inside APIs whose signatures appear to borrow cheaply.

Follow-ups

  • When is Cow<'a, T> preferable to always cloning?
  • How should an API expose ownership transfer of OS handles?
  • What cycle risks do Rc<T> and Arc<T> introduce?

References

  • The Rust Programming Language
  • Rust API Guidelines
  • Rust Standard Library