Skip to content

Instantly share code, notes, and snippets.

@rohithreddykota
Last active January 29, 2025 20:31
Show Gist options
  • Save rohithreddykota/5e07005cf308d411734cde6f6ef907e1 to your computer and use it in GitHub Desktop.
Save rohithreddykota/5e07005cf308d411734cde6f6ef907e1 to your computer and use it in GitHub Desktop.

What Are Borrow and BorrowMut?

  • Borrow<T>: Allows an owned type to be borrowed as some other type T, typically used in HashMap and BTreeMap lookups. This trait bridges the gap between the stored key type and the query key type.
  • BorrowMut<T>: The mutable counterpart of Borrow<T>, allowing a mutable borrow of type T. It’s less commonly used for key lookups but follows the same pattern.

When a HashMap<K, V> needs to look up a key Q (a different type than K), Rust checks if K: Borrow<Q> is implemented. If so, Rust internally converts the stored key K into a borrowed Q and compares it to your query Q. This process bypasses the need to allocate or convert Q into a K.


Why Custom Structs?

Sometimes you have a complex struct with multiple fields, but lookups only depend on a subset of those fields. For instance, if you have a User struct with many fields, you might only want to look up by email or username. Using Borrow allows you to:

  1. Store the full User struct in the HashMap.
  2. Query the map using just an &str (or another subset type).
  3. Avoid expensive conversions or cloning.

Example: User Struct with Subset Lookup

Struct Definition

use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::borrow::Borrow;

#[derive(Debug, Clone)]
struct User {
    username: String,
    email: String,
    age: u32,
}

// For hashing and equality, we decide what it means for two Users to be "the same".
// Here, we'll assume "username" uniquely identifies a user in our system.
impl PartialEq for User {
    fn eq(&self, other: &Self) -> bool {
        self.username == other.username
    }
}

impl Eq for User {}

// Consistency is crucial: the hash must be derived from the same fields used for equality.
impl Hash for User {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.username.hash(state);
    }
}

We’re treating username as a unique key. The PartialEq and Hash implementations rely exclusively on username. This ensures the internal HashMap logic remains consistent—the same fields used for hashing and equality must match those used when we implement Borrow.

Implement Borrow<str>

We want to be able to call map.get("alice") where "alice" is an &str, but our map is keyed by User. To do this, we implement Borrow<str> on User:

impl Borrow<str> for User {
    fn borrow(&self) -> &str {
        &self.username
    }
}

Now, HashMap<User, V> can be queried with &str because it knows how to borrow a str out of a User.

Putting It All Together

fn main() {
    // 1. Create a HashMap<User, i32>
    let mut users: HashMap<User, i32> = HashMap::new();

    // 2. Insert some users
    users.insert(
        User {
            username: "alice".to_string(),
            email: "[email protected]".to_string(),
            age: 30,
        },
        100,
    );
    users.insert(
        User {
            username: "bob".to_string(),
            email: "[email protected]".to_string(),
            age: 25,
        },
        200,
    );

    // 3. Lookup by &str, thanks to our Borrow<str> implementation
    let query: &str = "alice";
    if let Some(points) = users.get(query) {
        println!("Found user 'alice' with points: {}", points);
    } else {
        println!("User 'alice' not found");
    }

    // 4. Debug print to confirm we have valid data
    println!("{:?}", users);
}

Key Points:

  • The HashMap is keyed by User.
  • We only store the full user once.
  • We can look up that user by just "alice" (&str).
  • No extra String creation or User creation is necessary during the lookup.

Using BorrowMut

BorrowMut<T> is similar, except it provides a mutable reference of type T. While it’s not commonly used for key lookups, you might see something like:

impl BorrowMut<str> for User {
    fn borrow_mut(&mut self) -> &mut str {
        &mut self.username
    }
}

This would allow certain containers or APIs (though not standard HashMap lookups) to mutably borrow part of the User as a mutable str. In practice, mutable borrows of partial data structures are more specialized and less common for standard dictionary or map lookups.


Common Pitfalls and Best Practices

  1. Consistency in Hash and PartialEq: You must ensure the fields used in eq match those used in hash and the same fields used in borrow(). Mismatches can cause logical errors, collisions, or missing items.
  2. Do Not Over-Engineer: If your struct is trivially keyed (like a single String), you might just store String directly. Borrow is helpful when you have many fields but only one or two that matter for lookups.
  3. Borrow vs. AsRef: Both provide references, but Borrow<T> is specifically used by collection types for key lookups. AsRef<T> is more general-purpose and does not automatically work for map lookups.
  4. Borrow and Lifetimes: Make sure the fields you borrow exist for at least as long as needed. Typically, this is straightforward because the fields belong to the struct itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment