wandres.dev
DEREF, DROP Y OPERADORES · traits que dan superpoderes

AsRef, Borrow y las conversiones ligeras: cuándo cada una

`From` e `Into` convierten valores y pueden asignar memoria. Pero muchas veces solo quieres ver un valor como otro a través de una referencia, sin coste: un `&str` desde un `String`, un `&Path` desde un `PathBuf`. Dos traits lo hacen con firmas casi idénticas, `AsRef` y `Borrow`, y la diferencia no es el código sino una ley: la que hace correcto consultar un `HashMap<String, V>` con un `&str`.

⏱ 19 min

From e Into convierten valores que poseen su contenido y, al hacerlo, pueden asignar memoria. Pero con enorme frecuencia no quieres poseer nada: solo ver un valor como si fuera de otro tipo, a través de una referencia y sin coste —un &str a partir de un String, un &Path de un PathBuf, un &[T] de un Vec<T>—. Dos traits cubren ese hueco con firmas casi idénticas: AsRef y Borrow. Lo que los separa no es el código, que es el mismo, sino una ley. Y esa ley es exactamente lo que vuelve correcto consultar un HashMap<String, V> con un &str, sin asignar ni una copia.

🎯 Al terminar esta lección sabrás
  • Usar AsRef<T> para aceptar argumentos flexibles por conversión de referencia barata.
  • Entender Borrow<T> y su ley de consistencia con Eq, Hash y Ord.
  • Explicar por qué un HashMap<String, V> se puede consultar con &str.
  • Elegir entre From/Into, AsRef, Borrow y ToOwned según el caso.

AsRef: ver un valor como otro, sin coste

AsRef<T> ofrece un único método que convierte una referencia en otra, barata y sin asignar:

pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

Su uso estelar es en firmas genéricas, para aceptar cualquier cosa que se pueda ver como el tipo que necesitas. La biblioteca lo emplea por todas partes: File::open recibe impl AsRef<Path>, y por eso le puedes pasar un &str, un String o un PathBuf indistintamente.

use std::path::Path;

fn componentes(ruta: impl AsRef<Path>) -> usize {
    ruta.as_ref().components().count()
}

fn main() {
    componentes("/etc/hosts");                     // &str
    componentes(String::from("/var/log"));         // String
    componentes(std::path::PathBuf::from("/tmp")); // PathBuf
    // los tres valen: los tres son AsRef<Path>
}

String: AsRef<str>, String: AsRef<[u8]>, Vec<T>: AsRef<[T]>, PathBuf: AsRef<Path>: la red de conversiones ligeras ya está tejida por ti.

Borrow: como AsRef, pero con una ley

Borrow<T> tiene una firma que, letra por letra, es la misma que la de AsRef:

pub trait Borrow<Borrowed: ?Sized> {
    fn borrow(&self) -> &Borrowed;
}

La diferencia no se ve en el tipo, sino en el contrato. Borrow exige que el valor prestado sea equivalente al dueño: que hashee igual, se compare igual y se ordene igual. AsRef no promete nada de eso; solo te da una vista barata. Por eso existe la implementación general Borrow<T> for T —todo valor se presta a sí mismo—, pensada para código genérico que quiere aceptar tanto un dueño como un préstamo con garantías de consistencia.

Por qué un HashMap de claves String se consulta con str

Aquí la ley cobra sentido práctico. La firma de get en los mapas es genérica sobre lo que le pasas a buscar:

// La firma que lo permite (esquematizada):
// fn get<Q>(&self, clave: &Q) -> Option<&V>
// where K: Borrow<Q>, Q: Hash + Eq + ?Sized

use std::collections::HashMap;

fn main() {
    let mut m: HashMap<String, i32> = HashMap::new();
    m.insert(String::from("uno"), 1);

    let v = m.get("uno");   // consulta con &str, no con &String
    assert_eq!(v, Some(&1));
}

Como String: Borrow<str> y la ley garantiza que un String y su &str producen el mismo hash y la misma igualdad, buscar con "uno" encuentra la clave guardada como String sin construir un String temporal. Si esto se apoyara en AsRef en vez de Borrow, nada impediría una conversión que cambiara el hash, y el mapa podría no encontrar una clave que “es igual” —un bug silencioso que la ley de Borrow diseña para que no exista—.

El mapa de las conversiones ligeras

Cuatro familias, dos ejes: ¿posees o prestas?, y ¿hay una ley o solo comodidad?

🏗️

From / Into

Valor a valor, puede asignar memoria. La conversión que posee el resultado. La viste en el nivel 16.

👓

AsRef / AsMut

Referencia a referencia, barata. Para firmas flexibles como impl AsRef<Path>. Sin ley sobre Eq ni Hash.

⚖️

Borrow / BorrowMut

Referencia a referencia con ley: el prestado hashea e iguala como el dueño. Para claves de mapas.

📦

ToOwned

Referencia a valor: generaliza Clone. De str a String, de [T] a Vec<T>. Es el motor de Cow.

ToOwned es la dirección inversa de Borrow —de préstamo a valor poseído— y sostiene a Cow<'_, B> (clone on write), que presta mientras puede y solo asigna cuando de verdad tiene que modificar:

use std::borrow::Cow;

fn normaliza(entrada: &str) -> Cow<'_, str> {
    if entrada.contains(' ') {
        Cow::Owned(entrada.replace(' ', "_"))  // asigna solo si hace falta
    } else {
        Cow::Borrowed(entrada)                 // sin coste: presta
    }
}
flowchart TB
Q[Que conviertes] --> A[Valor a valor]
Q --> B[Referencia a referencia]
Q --> C[Referencia a valor]
A --> A1[From e Into puede asignar]
B --> B1[Necesitas la ley de igualdad]
B1 -->|si claves de mapa| BR[Borrow]
B1 -->|no solo ergonomia| AR[AsRef]
C --> C1[ToOwned generaliza Clone]
style BR fill:#a6e3a1,color:#11111b
style AR fill:#89b4fa,color:#11111b
style A1 fill:#cba6f7,color:#11111b
style C1 fill:#f9e2af,color:#11111b
Elegir el trait de conversión es elegir una promesa, no una firma

El detalle que casi nadie subraya es que AsRef y Borrow tienen la misma firma —un método que toma &self y devuelve &T— y sin embargo son traits distintos, con implementaciones distintas y usos que no se solapan. Si el tipo fuera lo único que importara, sobraría uno de los dos. Lo que los diferencia vive fuera del sistema de tipos, en el contrato que cada uno impone: AsRef promete solo baratura —conviérteme en esta otra vista sin coste—, mientras que Borrow promete además equivalencia —y la vista que te doy hashea, iguala y ordena exactamente como yo—. Esa segunda promesa es la que hace posible una de las comodidades más usadas de Rust: guardar un mapa con claves String y consultarlo con &str sin asignar nada, con la certeza de que “uno” encontrará la entrada guardada como String::from("uno"). Cámbialo por AsRef y la certeza desaparece, porque AsRef no jura que la conversión preserve el hash; podrías construir una tabla en la que una clave “igual” jamás se encuentre. Rust, fiel a su método, no deja esa invariante a la buena fe: la sube al sistema de tipos exigiendo K: Borrow<Q> justo en la firma de get, de modo que solo los tipos que han prometido la equivalencia pueden usarse como sonda. Es el mismo patrón que viste con Eq frente a PartialEq: un trait marcador que no añade métodos, sino que porta una ley que el compilador no puede demostrar pero sí puede exigir que declares. La familia de conversiones ligeras no es un surtido de utilidades intercambiables, sino una retícula en la que cada trait codifica una promesa precisa —posees o prestas, cuesta o es gratis, hay ley o solo conveniencia— y elegir bien es, antes que una cuestión de firmas, una cuestión de qué te comprometes a garantizar.

📝
Lo esencial de las conversiones ligeras

AsRef<T> da una vista barata de referencia a referencia, ideal para firmas como impl AsRef<Path>, sin ninguna ley. Borrow<T> tiene la misma firma pero añade la ley de que el prestado hashea, iguala y ordena como el dueño; por eso HashMap<String, V> se consulta con &str gracias a K: Borrow<Q>. ToOwned es la dirección inversa —de referencia a valor poseído—, generaliza Clone y sostiene a Cow. Frente a From/Into, que poseen y pueden asignar, estos traits solo ven: elige Borrow cuando necesites la equivalencia, AsRef cuando solo busques ergonomía.

⚔️ Convierte ligero, con la promesa correcta
  1. Escribe fn cuenta(ruta: impl AsRef<Path>) -> usize y llámala con un &str, un String y un PathBuf. Explica por qué las tres compilan.
  2. Construye un HashMap<String, i32>, inserta una clave y consúltala con un &str. Explica qué implementación de Borrow hace que funcione sin asignar.
  3. Razona por qué get pide K: Borrow<Q> y no K: AsRef<Q>. ¿Qué bug silencioso previene la ley de Borrow?
  4. Usa ToOwned para convertir un &str en String y un &[i32] en Vec<i32>. Relaciónalo con Clone.
  5. Escribe una función que devuelva Cow<'_, str> y asigne solo cuando modifique la entrada. Explica en qué caso presta y en cuál posee.