Implementing Borrow for Partial Views
Implementing Borrow for one convenient field is wrong unless that field has exactly the same equality, ordering, and hashing behavior as the whole value.
The mistake
Developers often see that Borrow<str> enables HashMap<String, V> lookup by &str and try to copy it for a richer struct.
For example, a User { id, name } might implement Borrow<str> by returning name.
That is only valid if the user’s identity is exactly the same as the name string.
If equality or hashing includes id, then a borrowed str is not an equivalent key.
Why it happens
The method signature of Borrow looks like AsRef for Flexible Arguments.
Both return a borrowed view.
The difference is the semantic contract: Borrow is used by collections that require equivalent Hash, Eq, and Ord behavior.
The compiler cannot verify this contract. Bad implementations can lead to failed lookups and confusing collection behavior.
HashMap stores entries according to the hash of the stored key.
Borrowed lookup hashes the borrowed query.
If Hash for the stored key includes id but Hash for the borrowed str includes only name, lookup starts in the wrong place.
That mismatch is a logic error in the key type’s API, not a bug in HashMap.
Use a dedicated key type when the lookup identity is a field.
For example, store HashMap<UserName, User> if names are the lookup key, or store HashMap<UserId, User> if IDs are the lookup key.
Example
#[derive(Debug, Hash, PartialEq, Eq)]
struct User {
id: u64,
name: String,
}
impl AsRef<str> for User {
fn as_ref(&self) -> &str {
&self.name
}
}
fn greeting(name: impl AsRef<str>) -> String {
format!("hello, {}", name.as_ref())
}
fn main() {
let user = User { id: 1, name: String::from("Ferris") };
assert_eq!(greeting(user), "hello, Ferris");
}Bad example
This implementation compiles but violates the Borrow contract because User equality and hashing include id.
use std::borrow::Borrow;
use std::collections::HashMap;
#[derive(Debug, Hash, PartialEq, Eq)]
struct User {
id: u64,
name: String,
}
impl Borrow<str> for User {
fn borrow(&self) -> &str {
&self.name
}
}
fn main() {
let mut users = HashMap::new();
users.insert(User { id: 1, name: String::from("Ferris") }, "admin");
assert_eq!(users.get("Ferris"), None);
}The correct design is to key the map by String, UserName, or UserId, then store the full User as the value if needed.
Common errors
There is usually no compiler error for this antipattern. The common symptom is a test failure or production miss:
assertion `left == right` failed
left: None
right: Some(...)Fix the key model instead of trying to tweak hash implementations until one lookup passes. The owned key and borrowed key must describe the same identity.
Best practice
- ✅ Use
AsRefwhen exposing a field or convenient borrowed representation. - ✅ Implement Borrow for Equivalent Keys only when the borrowed view is the whole logical key.
- ✅ Test custom map-key lookup behavior if you implement
Borrowmanually. - ✅ Prefer transparent Newtype Pattern wrappers for custom borrowed key implementations.
- ✅ Choose the map key type from the lookup operation you actually need most often.
- ✅ Use
AsRefor a named accessor for display names, slugs, and other partial views.
Pitfalls
- ⚠️ Do not implement
Borrow<str>for a multi-field type whose equality includes more than that string. - ⚠️ Do not assume successful compilation means the
Borrowcontract is valid. - ⚠️ Do not use
Borrowsimply to make a function argument flexible. - ⚠️ Do not make
Hashignore fields thatEqconsiders; equal keys must imply equal hashes, and mismatched semantics break collections. - ⚠️ Do not use interior mutability to change the borrowed identity of a key while it is stored in a map.
See also
Borrow for Equivalent Keys · AsRef for Flexible Arguments · Conversion Traits · HashMap · Newtype Pattern · Stringly-Typed Code · Hash and Eq Contracts · Collections & Strings · Idioms & API Design
