Em Rust, a conversão entre tipos é uma operação comum, mas que deve ser feita de forma segura e explícita. As traits From, Into, TryFrom, TryInto, AsRef e AsMut fornecem mecanismos poderosos para definir e utilizar conversões de maneira idiomática. Nesta aula, vamos explorar cada uma delas, entender suas diferenças e aplicações, e ver exemplos práticos de como usá-las em seus projetos.

Dominar essas traits é essencial para escrever código limpo e reutilizável, especialmente em bibliotecas e APIs, onde a conversão de tipos é frequentemente necessária. Vamos começar com as traits mais básicas e avançar para as mais complexas, sempre com foco em boas práticas e segurança.

From e Into

A trait From é usada para definir uma conversão infalível de um tipo para outro. Se você implementar From<T> for U, estará dizendo que é sempre possível converter um valor do tipo T em um valor do tipo U, sem risco de erro. Por exemplo, a implementação From<&str> for String permite converter uma string literal em uma String.

A trait Into é o inverso: se você implementar From<T> for U, automaticamente obtém Into<U> for T (porque a implementação padrão de Into chama From). Isso significa que você pode usar .into() para converter valores, o que torna o código mais conciso e expressivo.

// Exemplo de uso de From e Into
fn main() {
    let s: String = String::from("hello");
    let s2: String = "world".into();
    println!("{}, {}", s, s2);
}

É importante notar que a implementação de From deve ser infalível, ou seja, a conversão nunca deve falhar. Se houver possibilidade de erro, use TryFrom e TryInto, que veremos a seguir.

TryFrom e TryInto

Quando a conversão pode falhar, usamos TryFrom e TryInto. Essas traits retornam um Result, permitindo que o erro seja tratado de forma adequada. Por exemplo, converter um i64 para um i32 pode falhar se o valor for grande demais. A implementação de TryFrom deve retornar Ok(valor) em caso de sucesso e Err(erro) em caso de falha.

use std::convert::TryFrom;

fn main() {
    let x: i64 = 1000;
    let y = i32::try_from(x);
    match y {
        Ok(valor) => println!("Valor convertido: {}", valor),
        Err(e) => println!("Erro: {}", e),
    }
}

Assim como Into, TryInto é automaticamente implementado quando você implementa TryFrom. Isso permite usar .try_into() para converter tipos de forma segura.

AsRef e AsMut

As traits AsRef e AsMut são usadas para conversões de referência. Elas permitem converter uma referência de um tipo em uma referência de outro tipo, mantendo a mesma semântica de empréstimo. Por exemplo, String implementa AsRef<str>, o que permite passar uma &String onde se espera um &str.

Essas traits são especialmente úteis para escrever funções genéricas que aceitam diferentes tipos que podem ser convertidos para um tipo comum. Por exemplo, uma função que aceita AsRef<str> pode receber tanto &String quanto &str.

fn print_len(s: impl AsRef<str>) {
    let s = s.as_ref();
    println!("Length: {}", s.len());
}

fn main() {
    let s1 = String::from("hello");
    let s2 = "world";
    print_len(&s1);
    print_len(s2);
}

Enquanto AsRef fornece uma referência imutável, AsMut fornece uma referência mutável. Por exemplo, Vec<T> implementa AsMut<[T]>, permitindo obter um slice mutável do vetor.

Idiomático

Usar essas traits de forma idiomática é uma parte importante do estilo Rust. Aqui estão algumas boas práticas:

  • Prefira implementar From em vez de Into, pois isso dá a você a implementação de Into gratuitamente.
  • Use TryFrom e TryInto para conversões que podem falhar, e trate os erros de forma explícita.
  • Use AsRef e AsMut para conversões de referência que não devem falhar e que não consomem o valor original.
  • Evite implementar From ou TryFrom para tipos que não são logicamente conversíveis; isso pode levar a confusão.
  • Use .into() e .try_into() para tornar o código mais limpo, mas sempre com clareza sobre o tipo de destino.

Uma técnica comum é usar Into em parâmetros de função para aceitar diferentes tipos que podem ser convertidos, tornando a API mais flexível. Por exemplo, uma função que aceita impl Into<String> pode receber tanto String quanto &str.

fn greet(name: impl Into<String>) {
    let name = name.into();
    println!("Hello, {}!", name);
}

fn main() {
    greet("Alice");
    greet(String::from("Bob"));
}

Referências

Exercícios

  1. Exercício 1: Implemente From<u32> for MeuTipo, onde MeuTipo é uma struct com um campo valor: u32. Use .into() para converter um u32 em MeuTipo.
  2. ✓ Resposta:
    struct MeuTipo { valor: u32 }
    
    impl From<u32> for MeuTipo {
        fn from(v: u32) -> Self {
            MeuTipo { valor: v }
        }
    }
    
    fn main() {
        let mt: MeuTipo = 42.into();
        println!("Valor: {}", mt.valor);
    }
    
  3. Exercício 2: Crie uma conversão de i64 para i32 usando TryFrom. Trate o caso em que o valor está fora do intervalo e retorne um erro personalizado.
  4. ✓ Resposta:
    use std::convert::TryFrom;
    
    #[derive(Debug)]
    struct ErroConversao;
    
    impl TryFrom<i64> for i32 {
        type Error = ErroConversao;
    
        fn try_from(value: i64) -> Result<Self, Self::Error> {
            if value >= i32::MIN as i64 && value <= i32::MAX as i64 {
                Ok(value as i32)
            } else {
                Err(ErroConversao)
            }
        }
    }
    
    fn main() {
        let x: i64 = 1000;
        match i32::try_from(x) {
            Ok(v) => println!("Convertido: {}", v),
            Err(e) => println!("Erro: {:?}", e),
        }
    }
    
  5. Exercício 3: Escreva uma função genérica que aceite qualquer tipo que implemente AsRef<str> e imprima o comprimento da string. Teste com String e &str.
  6. ✓ Resposta:
    fn print_len(s: impl AsRef<str>) {
        let s = s.as_ref();
        println!("Length: {}", s.len());
    }
    
    fn main() {
        let s1 = String::from("hello");
        let s2 = "world";
        print_len(&s1);
        print_len(s2);
    }
    
  7. Exercício 4: Implemente From<MeuTipo> for String para uma struct MeuTipo { valor: u32 }, convertendo o valor em uma string. Use .into() para obter a string.
  8. ✓ Resposta:
    struct MeuTipo { valor: u32 }
    
    impl From<MeuTipo> for String {
        fn from(mt: MeuTipo) -> Self {
            format!("{}", mt.valor)
        }
    }
    
    fn main() {
        let mt = MeuTipo { valor: 10 };
        let s: String = mt.into();
        println!("String: {}", s);
    }
    
  9. Exercício 5: Crie uma função que aceite um parâmetro impl Into<Vec<u8>> e imprima os bytes. Use .into() para converter a entrada. Teste com um Vec<u8> e uma String.
  10. ✓ Resposta:
    fn print_bytes(data: impl Into<Vec<u8>>) {
        let bytes = data.into();
        println!("Bytes: {:?}", bytes);
    }
    
    fn main() {
        let v = vec![1, 2, 3];
        print_bytes(v);
        let s = String::from("abc");
        print_bytes(s);
    }
    

Boas Práticas e Observações Finais

Ao implementar conversões, sempre considere a semântica e a segurança. Use From e Into para conversões que são sempre válidas e TryFrom/TryInto para aquelas que podem falhar. Lembre-se de que a implementação de From fornece Into automaticamente, então prefira implementar From quando possível.

Além disso, AsRef e AsMut são úteis para abstrair referências, mas use-os com moderação para não criar APIs confusas. Sempre documente suas conversões e explique por que elas são seguras, especialmente quando usar TryFrom.

Com essas ferramentas, você pode escrever código Rust mais flexível e expressivo, mantendo a segurança de tipos que a linguagem oferece.