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
Derefto fake inheritance. It's tempting toimpl Deref<Target = Engine> for Carsocar.start()"just works", butDerefis 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 expectCarto behave like anEngine. IfCarmerely has anEngine, 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
BorrowandAsRefaren't interchangeable.AsRefmakes no such guarantee, a type couldimpl 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. ImplementBorrow<T>only whenselfand the borrowed&Tare truly indistinguishable as keys; otherwise implementAsRef<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 want | Use | Conversion is |
|---|---|---|
| A newtype/smart pointer to expose its inner value's whole API | Deref | implicit, automatic, one target |
| A function parameter that accepts many reference-like inputs | AsRef<T> | explicit (.as_ref()), many targets |
| To look up an owned-keyed map/set with a borrowed key | rely on Borrow<T> | explicit, must hash/compare identically |
| To turn a borrowed value back into an owned one | ToOwned | the 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:&Tbecomes&Uautomatically, which is why&Stringworks as&strandBox<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; useimpl AsRef<Path>/AsRef<str>parameters to accept many input types and do the one.as_ref()inside the function (thestdconvention).Derefis implicit and single-target;AsRefis explicit and multi-target. PickDerefto build a transparent type,AsRefto write a flexible function signature.Borrow<T>isAsRef<T>plus the promise that the borrowed form hashes and compares identically; that contract is what letsHashMap<String, V>::gettake a&strwithout allocating. Implement it only when the borrowed form is a faithful key stand-in.ToOwnedis the inverse ofBorrow(&str -> String) and the machinery behindCow.