AsRef, AsMut e Borrow Traits em Rust

Guia completo sobre AsRef, AsMut e Borrow em Rust: parâmetros genéricos, AsRef Path, Borrow para HashMap e quando usar cada um.

O que são AsRef, AsMut e Borrow?

Esses três traits lidam com conversão de referências — transformar uma referência de um tipo em uma referência de outro tipo, sem cópia. Embora pareçam similares, cada um tem um propósito distinto:

  • AsRef<T>: “posso ser visto como uma referência a T”. Usado para tornar funções genéricas que aceitam diferentes tipos de entrada. Exemplo clássico: aceitar tanto String quanto &str, ou tanto PathBuf quanto &Path.

  • AsMut<T>: versão mutável de AsRef. “Posso ser visto como uma referência mutável a T”.

  • Borrow<T>: similar a AsRef, mas com um contrato adicional: o tipo emprestado deve ter o mesmo comportamento de Hash, Eq e Ord que o tipo original. Essencial para chaves de HashMap e HashSet.

A regra prática: use AsRef para funções genéricas comuns, use Borrow para containers que dependem de hash e igualdade.


Definição dos Traits

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

// std::convert::AsMut
pub trait AsMut<T: ?Sized> {
    fn as_mut(&mut self) -> &mut T;
}

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

Diferenças-chave

AspectoAsRefBorrow
PropósitoConversão genérica de referênciaEmpréstimo com contrato de equivalência
Contrato Hash/EqNenhumObrigatório: hash e eq devem ser consistentes
Uso principalParâmetros de funçãoChaves de HashMap/HashSet
Implementação blanketT: AsRef<T> para todo TT: Borrow<T> para todo T

Como Implementar

AsRef manual

struct CaminhoConfig {
    base: String,
    arquivo: String,
}

impl AsRef<str> for CaminhoConfig {
    fn as_ref(&self) -> &str {
        &self.arquivo
    }
}

impl AsRef<std::path::Path> for CaminhoConfig {
    fn as_ref(&self) -> &std::path::Path {
        std::path::Path::new(&self.arquivo)
    }
}

fn imprimir_caminho(caminho: impl AsRef<std::path::Path>) {
    println!("Caminho: {}", caminho.as_ref().display());
}

fn main() {
    let config = CaminhoConfig {
        base: String::from("/etc"),
        arquivo: String::from("/etc/app/config.toml"),
    };

    imprimir_caminho(&config);
    imprimir_caminho("/usr/local/bin");
    imprimir_caminho(String::from("/home/user"));
}

Borrow manual

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

#[derive(Debug)]
struct EmailNormalizado {
    original: String,
    normalizado: String,
}

impl EmailNormalizado {
    fn novo(email: &str) -> Self {
        EmailNormalizado {
            original: email.to_string(),
            normalizado: email.to_lowercase(),
        }
    }
}

// Hash usa o valor normalizado
impl Hash for EmailNormalizado {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.normalizado.hash(state);
    }
}

// Eq usa o valor normalizado
impl PartialEq for EmailNormalizado {
    fn eq(&self, other: &Self) -> bool {
        self.normalizado == other.normalizado
    }
}

impl Eq for EmailNormalizado {}

// Borrow<str> permite buscar no HashMap com &str
impl Borrow<str> for EmailNormalizado {
    fn borrow(&self) -> &str {
        &self.normalizado
    }
}

fn main() {
    use std::collections::HashSet;

    let mut emails = HashSet::new();
    emails.insert(EmailNormalizado::novo("Ana@Exemplo.COM"));
    emails.insert(EmailNormalizado::novo("bruno@exemplo.com"));

    // Busca com &str (graças a Borrow<str>)
    // Precisamos buscar com o valor normalizado
    println!("Contém ana@exemplo.com? {}", emails.contains("ana@exemplo.com"));
    println!("Contém bruno@exemplo.com? {}", emails.contains("bruno@exemplo.com"));
}

Exemplos Práticos

Exemplo 1: AsRef para APIs flexíveis

fn contar_palavras(texto: impl AsRef<str>) -> usize {
    texto.as_ref().split_whitespace().count()
}

fn contem_palavra(texto: impl AsRef<str>, palavra: impl AsRef<str>) -> bool {
    let texto = texto.as_ref().to_lowercase();
    let palavra = palavra.as_ref().to_lowercase();
    texto.contains(&palavra)
}

fn main() {
    // Funciona com &str
    println!("{}", contar_palavras("Rust é incrível")); // 3

    // Funciona com String
    let frase = String::from("A linguagem Rust é segura e rápida");
    println!("{}", contar_palavras(&frase)); // 7
    println!("{}", contar_palavras(frase.clone())); // 7

    // Funciona com diferentes combinações
    println!("{}", contem_palavra("Rust é incrível", "RUST")); // true
    println!("{}", contem_palavra(frase, String::from("segura"))); // true
}

Exemplo 2: AsRef para operações de arquivo

use std::path::Path;
use std::fs;

fn arquivo_existe(caminho: impl AsRef<Path>) -> bool {
    caminho.as_ref().exists()
}

fn extensao(caminho: impl AsRef<Path>) -> Option<String> {
    caminho
        .as_ref()
        .extension()
        .and_then(|ext| ext.to_str())
        .map(|s| s.to_string())
}

fn listar_info(caminho: impl AsRef<Path>) {
    let p = caminho.as_ref();
    println!("Caminho: {}", p.display());
    println!("Existe: {}", p.exists());
    println!("É arquivo: {}", p.is_file());
    println!("Extensão: {:?}", extensao(p));
}

fn main() {
    // Aceita &str
    println!("Existe? {}", arquivo_existe("/etc/hosts"));

    // Aceita String
    let caminho = String::from("/tmp/teste.txt");
    listar_info(&caminho);

    // Aceita &Path
    let path = Path::new("/usr/bin/rustc");
    listar_info(path);

    // Aceita PathBuf
    let pathbuf = std::path::PathBuf::from("/home");
    listar_info(&pathbuf);
}

Exemplo 3: Borrow em HashMap

use std::collections::HashMap;
use std::borrow::Borrow;

fn main() {
    let mut capitais: HashMap<String, String> = HashMap::new();

    capitais.insert(
        String::from("Brasil"),
        String::from("Brasília"),
    );
    capitais.insert(
        String::from("Argentina"),
        String::from("Buenos Aires"),
    );
    capitais.insert(
        String::from("Chile"),
        String::from("Santiago"),
    );

    // HashMap::get aceita qualquer tipo que implemente Borrow<str>
    // String implementa Borrow<str>, então podemos buscar com &str
    if let Some(capital) = capitais.get("Brasil") {
        println!("Capital do Brasil: {}", capital);
    }

    // Também funciona com String
    let pais = String::from("Argentina");
    if let Some(capital) = capitais.get(&pais) {
        println!("Capital da Argentina: {}", capital);
    }

    // contains_key também usa Borrow
    println!("Tem Chile? {}", capitais.contains_key("Chile"));

    // remove também usa Borrow
    capitais.remove("Chile");
    println!("Após remover Chile: {:?}", capitais.keys().collect::<Vec<_>>());
}

Exemplo 4: AsMut para buffers

fn preencher_zeros(buffer: &mut impl AsMut<[u8]>) {
    let slice = buffer.as_mut();
    for byte in slice.iter_mut() {
        *byte = 0;
    }
}

fn capitalizar_ascii(texto: &mut impl AsMut<[u8]>) {
    let bytes = texto.as_mut();
    if let Some(primeiro) = bytes.first_mut() {
        if primeiro.is_ascii_lowercase() {
            *primeiro = primeiro.to_ascii_uppercase();
        }
    }
}

fn main() {
    // Com Vec<u8>
    let mut buffer = vec![1u8, 2, 3, 4, 5];
    preencher_zeros(&mut buffer);
    println!("{:?}", buffer); // [0, 0, 0, 0, 0]

    // Com array
    let mut arr = [10u8, 20, 30];
    preencher_zeros(&mut arr);
    println!("{:?}", arr); // [0, 0, 0]

    // Com Vec<u8> como texto
    let mut texto = b"rust".to_vec();
    capitalizar_ascii(&mut texto);
    println!("{}", String::from_utf8_lossy(&texto)); // Rust
}

Exemplo 5: Quando usar AsRef vs Borrow vs Deref

use std::borrow::Borrow;
use std::path::Path;

// AsRef: para conversões genéricas em parâmetros de função
fn processar_texto(input: impl AsRef<str>) {
    let s: &str = input.as_ref();
    println!("Processando: '{}'", s);
}

// Borrow: para containers que dependem de Hash/Eq
fn buscar_em_mapa<K, V>(mapa: &std::collections::HashMap<K, V>, chave: &str) -> Option<&V>
where
    K: std::hash::Hash + Eq + Borrow<str>,
{
    mapa.get(chave)
}

// Deref: acontece automaticamente via coercion
fn tamanho(s: &str) -> usize {
    s.len()
}

fn main() {
    let string = String::from("exemplo");

    // AsRef: chamada explícita com as_ref()
    processar_texto(&string);
    processar_texto("literal");

    // Borrow: usado internamente por HashMap
    let mut mapa = std::collections::HashMap::new();
    mapa.insert(String::from("chave"), 42);
    println!("{:?}", buscar_em_mapa(&mapa, "chave"));

    // Deref: coerção automática &String → &str
    println!("Tamanho: {}", tamanho(&string));
}

Quando Usar Cada Um?

Use AsRef<T> quando:

  • Sua função precisa de uma referência a T e você quer aceitar vários tipos de entrada
  • Não há requisito de consistência entre Hash/Eq
  • Cenário típico: AsRef<str>, AsRef<Path>, AsRef<[u8]>

Use Borrow<T> quando:

  • Você está implementando um container ou coleção
  • A busca/comparação precisa ser consistente com Hash e Eq
  • Cenário típico: chaves de HashMap, elementos de HashSet

Use Deref quando:

  • Você está criando um smart pointer ou tipo wrapper
  • Quer que todos os métodos do tipo interno estejam disponíveis
  • Cenário típico: Box<T>, Rc<T>, Arc<T>, newtypes

Padrões e Boas Práticas

  1. Prefira AsRef<str> a &String: Funções que aceitam impl AsRef<str> são mais genéricas e idiomáticas que funções com &String.

  2. O contrato de Borrow é sério: Se a.borrow() == b.borrow(), então hash(a) == hash(b) deve ser verdadeiro. Violar isso quebra HashMap/HashSet.

  3. AsRef é transitivo na stdlib: String: AsRef<str>, str: AsRef<Path>, mas String não implementa AsRef<Path> diretamente — use conversão explícita quando necessário.

  4. Não implemente AsRef e Deref para o mesmo Target: Isso pode causar ambiguidade. Escolha um: Deref para smart pointers, AsRef para conversões genéricas.

  5. impl AsRef<str> vs &str: Para funções simples, &str é mais direto e legível. Use impl AsRef<str> apenas quando realmente precisa aceitar múltiplos tipos.

  6. Borrow em APIs de coleção: Se você está criando uma coleção que faz lookup por chave, use Borrow no método de busca, como a stdlib faz com HashMap::get.


Veja Também