Deref, AsRef, and Borrow

Strings: Choosing the Right Type leaned on a fact it never fully explained: a &String can be passed where &str is expected, and impl AsRef<str> accepts both. Three traits sit behind that ergonomic flexibility, Deref, AsRef, and Borrow, and they look interchangeable until you need to pick one. They answer three different questions: "what does this smart pointer wrap?", "can this be viewed as a &U?", and "is this a faithful stand-in for that key?". This tutorial untangles them so you reach for the right one.


Deref: Smart Pointers That Act Like What They Wrap

Deref is what makes a smart pointer transparent. Implementing Deref<Target = U> lets the compiler automatically turn a &T into a &U wherever one is needed, deref coercion. It's why Box<T>, Rc<T>, and String feel like the thing they hold rather than a wrapper you constantly unwrap.

let name = String::from("Ferris");

fn greet(who: &str) { println!("hi {who}"); }

greet(&name);        // &String coerces to &str via String: Deref<Target = str>
let n = name.len();  // str::len called on a String, also via Deref

You never wrote a conversion, the compiler inserted it because String: Deref<Target = str>. The same mechanism gives you (*boxed).method() as just boxed.method(), and lets a Vec<T> use every &[T] slice method. Implement Deref on your own newtype only when it genuinely is a smart pointer around an inner value you want to expose wholesale:

use std::ops::Deref;

struct Sentence(String);

impl Deref for Sentence {
    type Target = str;
    fn deref(&self) -> &str { &self.0 }
}

let s = Sentence("hello world".into());
println!("{}", s.split_whitespace().count());  // str methods, free, via Deref

Gotcha: don't use Deref to fake inheritance. It's tempting to impl Deref<Target = Engine> for Car so car.start() "just works", but Deref is for pointer-like wrappers, not is-a relationships. Abusing it leaks the inner type's entire API onto the wrapper (including methods you didn't mean to expose), produces confusing autocomplete, and surprises readers who don't expect Car to behave like an Engine. If Car merely has an Engine, write the methods you actually want to forward.


AsRef<T>: Flexible, Explicit Reference Conversion

AsRef<T> answers "can this be viewed as a &T?" as a cheap, explicit conversion. Its headline use is generic function parameters that accept anything convertible to a given reference type, so callers don't have to convert by hand:

use std::path::Path;

// accepts &str, String, &Path, PathBuf, ... anything that is AsRef<Path>
fn file_size(path: impl AsRef<Path>) -> std::io::Result<u64> {
    let path = path.as_ref();        // &Path, once, inside the function
    Ok(std::fs::metadata(path)?.len())
}

file_size("config.toml")?;                       // &str
file_size(String::from("data.bin"))?;            // String
file_size(std::path::PathBuf::from("log.txt"))?; // PathBuf

Compare the naive alternative, overloading by hand or forcing the caller to convert (file_size(Path::new("config.toml")) at every call site). impl AsRef<Path> pushes that one-time .as_ref() into the function body and frees every caller. This is the idiomatic signature across std (File::open, fs::read, etc. all take AsRef<Path>).

The difference from Deref: Deref coercion is implicit and automatic (the compiler inserts it, and a type has exactly one Deref target); AsRef is explicit (you call .as_ref()) and a type can implement AsRef<U> for many different U. Use Deref to build a transparent smart pointer; use AsRef to write a function that accepts many input types.


Borrow<T>: Stand-Ins That Hash and Compare Identically

Borrow<T> looks like AsRef<T> (both turn &Self into &T) but carries an extra promise: the borrowed form must Hash, Eq, and Ord exactly the same as the owned form. That stricter contract is what powers the most-used trick you've relied on without noticing, looking up a HashMap<String, V> with a &str:

use std::collections::HashMap;

let mut scores: HashMap<String, u32> = HashMap::new();
scores.insert("alice".to_string(), 10);

// no String allocation to look up — &str works as the key
if let Some(s) = scores.get("alice") {   // get<Q>(&self, k: &Q) where String: Borrow<Q>
    println!("{s}");
}

HashMap::get is generic over Q where K: Borrow<Q>. Because String: Borrow<str> and a String hashes identically to the str it contains, the lookup is sound, you get the result without allocating a throwaway String for the key. The same Borrow bound is why a BTreeSet<String> can be queried with &str.

Gotcha: the identical-hash/compare promise is the whole reason Borrow and AsRef aren't interchangeable. AsRef makes no such guarantee, a type could impl AsRef<str> that returns a lowercased or trimmed view, which would hash differently from the original and silently break a hash map if it were allowed as the lookup bound. Implement Borrow<T> only when self and the borrowed &T are truly indistinguishable as keys; otherwise implement AsRef<T>.


ToOwned: The Other Direction

Borrow has a partner, ToOwned, the generalized .clone() that goes from a borrowed form back to an owned one: str::to_owned() -> String, [T]::to_owned() -> Vec<T>. It's the trait that makes Cow<T> (from the strings tutorial) work, Cow<str> holds either a &str (borrowed) or a String (owned), and ToOwned is how it produces the owned side on demand:

let borrowed: &str = "hello";
let owned: String = borrowed.to_owned();   // ToOwned: &str -> String

You rarely implement ToOwned yourself (the blanket impl covers any T: Clone), but it's worth recognizing as the inverse of Borrow and the machinery under Cow.


Choosing Among Them

You wantUseConversion is
A newtype/smart pointer to expose its inner value's whole APIDerefimplicit, automatic, one target
A function parameter that accepts many reference-like inputsAsRef<T>explicit (.as_ref()), many targets
To look up an owned-keyed map/set with a borrowed keyrely on Borrow<T>explicit, must hash/compare identically
To turn a borrowed value back into an owned oneToOwnedthe inverse of Borrow

The mental model: Deref is for being the inner thing (transparent wrappers), AsRef is for viewing as a reference type (flexible inputs), and Borrow is AsRef plus a behavioral promise (interchangeable as a key). When two seem to fit, the deciding question is whether you need the implicit coercion (Deref), the many-target flexibility (AsRef), or the hash/eq guarantee (Borrow).


Key Takeaways

  • Deref<Target = U> powers deref coercion: &T becomes &U automatically, which is why &String works as &str and Box<T>/Rc<T> feel transparent. Implement it only for genuine smart-pointer wrappers, never to fake inheritance.
  • AsRef<T> is an explicit, cheap "view as &T" conversion; use impl AsRef<Path> / AsRef<str> parameters to accept many input types and do the one .as_ref() inside the function (the std convention).
  • Deref is implicit and single-target; AsRef is explicit and multi-target. Pick Deref to build a transparent type, AsRef to write a flexible function signature.
  • Borrow<T> is AsRef<T> plus the promise that the borrowed form hashes and compares identically; that contract is what lets HashMap<String, V>::get take a &str without allocating. Implement it only when the borrowed form is a faithful key stand-in.
  • ToOwned is the inverse of Borrow (&str -> String) and the machinery behind Cow.