AsRef and AsMut
Learn how AsRef and AsMut enable cheap, flexible reference conversions in Rust and when to use them for ergonomic generic APIs.
The AsRef and AsMut traits sit at the boundary between types that are “the same thing, just represented differently.” They let you borrow a reference to a target type from some other type without moving or cloning any data. That one idea makes generic functions far easier to write and call.
Think of every function you have ever written that accepted a &str, only to realize callers sometimes have a String, a &String, a Box<str>, or even a custom wrapper type. Without AsRef, you either force callers to manually convert or write multiple overloads (which Rust does not have). AsRef collapses that combinatorial explosion into a single bound.
The Problem AsRef Solves
A function that needs a &str to read text does not need to know how the caller stores that text. It only needs a borrowed view of the bytes. If the function signature is fn process(text: &str), the caller with a String must write process(&my_string) or process(my_string.as_str()). That is fine for one call, but when many functions repeat this pattern, callers must remember which borrowing method each type supports.
AsRef moves that responsibility from the caller to the function signature. The function declares fn process<T: AsRef<str>>(text: T) and the compiler knows how to borrow a &str from any type that implements AsRef<str>. The caller just passes my_string directly. No explicit .as_str(), no &.
This applies to any reference conversion: Path, [u8], OsStr, [i32], and beyond.
AsRef is not about ownership:
AsRef never moves or copies data. If you need to own the result, use Into or From. A common mistake is writing let owned = param.as_ref().to_owned() inside a function that takes AsRef. That signals the function should take Into instead.
The Trait Definitions
Both traits are defined in std::convert. Here they are as you would see them in the standard library:
pub trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
pub trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}
The generic parameter T has a ?Sized bound. That is deliberate and important.
A type like str or [u8] has a size that is not known at compile time — it is unsized. A normal generic T implicitly requires T: Sized, which would prevent these traits from being implemented with T = str. The ?Sized opt-out allows AsRef<str> and AsRef<Path> (which is unsized) to exist. That is why you can write fn foo<T: AsRef<str>> and pass a String — the target type str is unsized, but the trait permits it.
as_ref takes &self and returns &T. There is no transfer of ownership; the returned reference points into the original value or into something the original value owns.
as_mut takes &mut self and returns &mut T. That gives the caller temporary mutable access to the inner data through the same reference interface.
Cheap, infallible conversion:
Both methods must be cheap (ideally just a pointer cast or field access) and must never fail. If a conversion could fail, use TryFrom instead. If it is expensive, consider making it explicit with a named method.
Auto-Deref for References
The standard library includes a blanket implementation that makes references “transparent” to AsRef:
impl<T: ?Sized, U: ?Sized> AsRef<U> for &T
where
T: AsRef<U>,
{
fn as_ref(&self) -> &U {
<T as AsRef<U>>::as_ref(*self)
}
}
This is why passing an &&str to a function expecting impl AsRef<str> works. The outer & strips away, and the inner str (which implements AsRef<str> trivially) provides the final reference. The same lifting applies to any depth of references: &&String, &&&Path, and so on all work automatically.
The practical effect is that callers never have to worry about whether they hold a value or a reference to it. Both satisfy the bound.
Making Functions Generic Over String-Like Types
A function that needs to read string data can accept anything that exposes a &str. The implementation calls .as_ref() to get the &str and then uses it like any other string slice.
fn print_uppercase<T: AsRef<str>>(input: T) -> String {
let s: &str = input.as_ref();
s.to_uppercase()
}
fn main() {
let literal = "hello";
let owned = String::from("world");
let boxed: Box<str> = Box::from("again");
println!("{}", print_uppercase(literal));
println!("{}", print_uppercase(&owned));
println!("{}", print_uppercase(boxed));
}
The body calls input.as_ref() to obtain the &str. From there, any str method is available. The function does not care which specific type arrived; all it needs is the ability to borrow a &str.
String, str, &str, Box<str>, Cow<'_, str>, and many others implement AsRef<str>. If you design a library, implementing AsRef<str> for your custom string wrapper gives your users the same seamless experience.
No automatic deref to the target type:
AsRef does not cause method resolution to happen through the target type the way Deref does. Calling input.len() on input: T where T: AsRef<str> will not compile unless you first write let s: &str = input.as_ref();. The compiler does not insert .as_ref() calls implicitly.
Accepting Paths Flexibly
The standard library’s std::fs::File::open declaration is one of the most visible uses of AsRef:
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File>
A Path is a thin wrapper around an OsStr that represents a filesystem path. Because Path is unsized, we usually work with &Path or PathBuf. The open function accepts any type that can yield a &Path.
Callers can pass:
&str(viastr’sAsRef<Path>impl)String(viastr’s impl, then auto-deref for references)&OsStrOsStringPathBuf&PathCow<'_, OsStr>- and combinations of references.
The caller writes File::open("config.toml") without a single conversion method. That ergonomics is what AsRef provides.
Inside open, the implementation calls path.as_ref() to get &Path and then proceeds with OS-specific logic. The conversion is zero-cost — it typically just reinterprets the bytes as a Path.
use std::path::Path;
fn get_extension<P: AsRef<Path>>(file_path: P) -> Option<String> {
file_path.as_ref()
.extension()
.and_then(|ext| ext.to_str())
.map(|s| s.to_owned())
}
assert_eq!(
get_extension("/home/user/data.txt"),
Some("txt".to_string())
);
assert_eq!(
get_extension(String::from("archive.tar.gz")),
Some("gz".to_string())
);
The function treats a &str and a String identically. Without AsRef, you would need two functions or force callers to wrap everything in Path::new.
Working with Slices
The same idea extends to slices. Vec<T> implements AsRef<[T]>, as do arrays, &[T], and various other collections. A function that reads elements can accept anything slice-like.
fn sum_elements<T: AsRef<[i32]>>(data: T) -> i32 {
data.as_ref().iter().sum()
}
let v = vec![1, 2, 3];
let a = [4, 5, 6];
assert_eq!(sum_elements(&v), 6);
assert_eq!(sum_elements(a), 15);
The as_ref() call on data: T where T: AsRef<[i32]> yields a &[i32] that can be iterated over. The function does not know or care that one call passed a Vec<i32> and another passed an array.
This is not limited to i32. You can write generic code over any element type:
fn contains<T, U>(container: T, target: U) -> bool
where
T: AsRef<[U]>,
U: PartialEq,
{
container.as_ref().contains(&target)
}
Now contains works with any combination of slice-like and element types that can be compared.
Implementing AsRef for Your Own Types
If you have a type that wraps a String and it makes sense to borrow a &str from it, implement AsRef<str>. The same applies to other inner types.
Here is a Text struct that wraps a String and provides two AsRef implementations: one for str (textual) and one for [u8] (raw bytes).
struct Text {
content: String,
}
impl AsRef<str> for Text {
fn as_ref(&self) -> &str {
&self.content
}
}
impl AsRef<[u8]> for Text {
fn as_ref(&self) -> &[u8] {
self.content.as_bytes()
}
}
A function bound on AsRef<str> can now accept Text directly:
fn greet<T: AsRef<str>>(name: T) -> String {
format!("Hello, {}!", name.as_ref())
}
let t = Text { content: "Rust".into() };
assert_eq!(greet(t), "Hello, Rust!");
The AsRef<[u8]> implementation is useful for functions that work with raw bytes, like hashing or network serialization.
Only borrow what is invariant:
If the borrowed reference could become dangling after a mutation to the outer type, do not implement AsRef. For example, if your type can resize or reallocate internal storage while a reference is outstanding, exposing a reference to that storage through AsRef is unsound unless you can guarantee the reference remains valid. Rust’s ownership model usually prevents this, but custom unsafe code can create issues.
Implementing AsRef for a wrapper is straightforward as long as the target type is a direct view of the inner data. Avoid transformations that allocate, parse, or reinterpret data. The operation must be cheap and infallible.
Mutable Borrow Conversions with AsMut
AsMut is the mutable sibling. It allows a function to borrow a &mut T from some type that owns a T. The canonical use case is modifying the contents of a collection through a generic parameter.
Consider a function that doubles every element in a mutable slice of i32. Instead of only accepting &mut [i32], you can accept anything that yields one.
fn double_all<T: AsMut<[i32]>>(mut data: T) {
for val in data.as_mut().iter_mut() {
*val *= 2;
}
}
let mut v = vec![1, 2, 3];
double_all(&mut v);
assert_eq!(v, vec![2, 4, 6]);
let mut arr = [10, 20, 30];
double_all(&mut arr);
assert_eq!(arr, [20, 40, 60]);
as_mut() gives a &mut [i32], which is iterated over. The original collection is mutated in place. The function remains generic and works with Vec<i32>, arrays, and any custom type that implements AsMut<[i32]>.
Similarly, you can write a function that appends to a buffer using AsMut<Vec<u8>>:
fn write_message<T: AsMut<Vec<u8>>>(mut buffer: T, msg: &[u8]) {
buffer.as_mut().extend_from_slice(msg);
}
let mut buf = Vec::new();
write_message(&mut buf, b"hello");
write_message(&mut buf, b" world");
assert_eq!(buf, b"hello world");
Only types that directly contain or can produce a mutable reference to the target type should implement AsMut. Returning a mutable reference to part of a value is fine, but the mutable reference must not violate the type’s internal consistency.
AsMut can expose internal mutability:
If you implement AsMut<T> for a type where modifying T breaks invariants of the original type, your code becomes incorrect. For example, a SortedVec that maintains sorted order must not let callers change elements arbitrarily through AsMut<[i32]> without running the sort again. Think carefully about whether your type can safely hand out a &mut T.
Choosing Between AsRef, Deref, Borrow, and Into
Several traits seem to offer similar conversions, but each has a distinct contract.
AsRef: cheap, infallible reference-to-reference conversion. No equivalence requirement. The target type is something the source can expose as a borrow.Deref: implicit coercion for smart pointers. WhenDerefis implemented, the compiler automatically inserts*and.operations to reach the target type. UseDereffor smart pointers (Box,Rc,Arc) and owning types that are their target (Vecderefs to[T],Stringtostr). Do not implementDerefjust to inherit methods; useAsReffor explicit conversions.Borrow: similar toAsRefbut additionally requires that the borrowed value has the sameHash,Eq, andOrdbehavior as the owned value. This is essential for hash map keys:HashMap<Cow<'_, str>, V>can look up entries with&strbecauseCow<str>implementsBorrow<str>. If your type does not maintain that equivalence, implementAsRefinstead.Into(andFrom): consuming, owning conversion. Use when the function needs to take ownership of the transformed value (e.g., storing it in a struct). AnInto<String>parameter lets callers pass&str,String,Box<str>, and more, but the function ends up with an ownedString.
A common pattern is to use AsRef for read-only borrowed access and Into when the function must own the result. Mixing them in a single API is natural: a builder might accept Into<String> for the final stored value, but a processing function that only needs to inspect data should use AsRef<str>.
Mistakes to Avoid
Using AsRef when you need ownership
If the function body calls param.as_ref().to_owned() immediately, change the bound to Into. AsRef signals “I only need a temporary view.” Owning the data is a stronger requirement.
Assuming AsRef enables method resolution
Calling methods directly on the generic parameter without .as_ref() will fail to compile unless the type itself provides those methods. AsRef does not trigger deref coercion. Always write let borrowed: &Target = value.as_ref(); and use that.
Implementing AsRef for a conversion that allocates
AsRef must be cheap. If your implementation calls .to_string() or otherwise allocates, you are hiding a cost that callers cannot see. Use a named method or From/Into instead.
Implementing AsRef for types that violate safety through AsMut
If a type hands out a mutable reference that, when mutated, breaks internal invariants, the type is unsound. Only implement AsMut when the mutation through the reference is always safe.
Overusing AsRef as a blanket bound
While AsRef makes a function flexible, it also makes the signature more complex. When only one concrete type is ever used in practice, a plain &str parameter is simpler and just as clear. Use AsRef when you know multiple input types are genuinely useful.
Do not call .as_ref() and then forget to use the result:
A forgotten let _ = value.as_ref(); does nothing. The borrowed reference created by as_ref must be bound to a variable and used. If you never need the reference, you do not need AsRef at all.
Summary
AsRef and AsMut are the tools that make Rust APIs feel less rigid without sacrificing speed. They let a single function accept String, &str, PathBuf, &Path, Vec<T>, and any custom type that can cheaply lend a reference to the desired target. The conversion costs nothing at runtime — it is just a pointer reinterpretation or field access — and it never fails.
The key insight is separation of concerns: the function declares what it needs (a &Path or &str), and the caller supplies whatever type it happens to have. The trait bridges the two with zero ceremony.
Together with traits like Borrow, BorrowMut, From, and Into, they form the core conversion vocabulary of Rust, and mastering them is a major step toward writing library-quality generic code.