Em Rust, a conversão entre tipos é uma operação comum, mas que precisa ser feita de forma segura e explícita. Diferente de linguagens com conversões implícitas automáticas, Rust exige que você indique claramente quando deseja converter um valor de um tipo para outro. Para isso, a linguagem oferece um conjunto de traits poderosas que padronizam essas operações: From, Into, TryFrom e TryInto. Além delas, as traits AsRef e AsMut permitem conversões de referência que preservam a semântica de empréstimo.

Nesta aula, vamos mergulhar nessas traits, entender suas diferenças, quando usar cada uma e como implementá-las nos seus próprios tipos. Veremos exemplos práticos que vão desde conversões simples entre tipos numéricos até conversões que podem falhar, como parsing de strings para números. Ao final, você terá uma compreensão sólida de como realizar conversões idiomáticas e seguras em Rust.

From e Into

A trait From<T> define como criar um valor de um tipo a partir de outro tipo T. É a trait central para conversões que são sempre bem-sucedidas e não podem falhar. Quando você implementa From<T> para um tipo U, automaticamente ganha a implementação de Into<U> para T, graças a uma implementação genérica fornecida pela biblioteca padrão.

A trait Into<T> é a recíproca: ela permite converter um valor em outro tipo. Na prática, você geralmente implementa From e usa Into como uma interface conveniente. Por exemplo, a conversão de &str para String é feita via From:

let s = String::from("hello");
let s2: String = "world".into();

No código acima, String::from usa a implementação de From<&str> para String, e "world".into() usa a implementação de Into<String> que é gerada automaticamente. Essa relação é muito útil porque você pode escrever funções genéricas que aceitam qualquer tipo que possa ser convertido em String:

fn concat<T: Into<String>>(a: T, b: T) -> String {
    a.into() + &b.into()
}

Ao implementar From para seus próprios tipos, você define uma conversão clara e reutilizável. Por exemplo, vamos criar um tipo Ponto e permitir a conversão de uma tupla (i32, i32):

#[derive(Debug, PartialEq)]
struct Ponto {
    x: i32,
    y: i32,
}

impl From<(i32, i32)> for Ponto {
    fn from((x, y): (i32, i32)) -> Self {
        Ponto { x, y }
    }
}

fn main() {
    let p = Ponto::from((3, 4));
    let q: Ponto = (5, 6).into();
    println!("{:?} {:?}", p, q);
}

É importante notar que From é usado para conversões que são sempre válidas. Se a conversão pode falhar, você deve usar TryFrom, como veremos a seguir.

TryFrom e TryInto

Nem toda conversão é infalível. Por exemplo, converter um i64 para i32 pode falhar se o valor for grande demais. Para esses casos, Rust fornece as traits TryFrom e TryInto, que retornam um Result permitindo tratar o erro.

A trait TryFrom<T> tem um método try_from que retorna Result<Self, Self::Error>. O tipo de erro é definido como um tipo associado. Da mesma forma que From gera Into, TryFrom gera TryInto automaticamente.

Um exemplo clássico é a conversão de i64 para i32:

let valor_grande: i64 = 1_000_000_000_000;
let valor_pequeno = i32::try_from(valor_grande);

match valor_pequeno {
    Ok(v) => println!("Convertido: {}", v),
    Err(e) => println!("Erro: {}", e),
}

No código acima, a conversão falha porque o valor excede o intervalo de i32. O erro retornado é do tipo TryFromIntError, que implementa Display e Debug.

Você pode implementar TryFrom para seus próprios tipos. Por exemplo, vamos criar um tipo Idade que só aceita valores entre 0 e 150:

#[derive(Debug, PartialEq)]
struct Idade(u8);

impl TryFrom<u16> for Idade {
    type Error = String;

    fn try_from(valor: u16) -> Result<Self, Self::Error> {
        if valor <= 150 {
            Ok(Idade(valor as u8))
        } else {
            Err(format!("Idade inválida: {}", valor))
        }
    }
}

fn main() {
    let idade = Idade::try_from(25);
    let idade_invalida = Idade::try_from(200);
    println!("{:?} {:?}", idade, idade_invalida);
}

É importante escolher o tipo de erro apropriado. Em código de produção, você provavelmente usará um tipo de erro mais estruturado, como um enum, em vez de String.

AsRef e AsMut

As traits AsRef<T> e AsMut<T> permitem converter uma referência a um tipo em uma referência a outro tipo. Elas são usadas para abstrair sobre tipos que podem ser emprestados como outro tipo. Por exemplo, String implementa AsRef<str>, e Vec<T> implementa AsRef<[T]>.

A trait AsRef tem o método as_ref, que retorna uma referência imutável &T. Já AsMut tem o método as_mut, que retorna uma referência mutável &mut T.

Um uso comum é escrever funções que aceitam tanto &str quanto &String como argumento, usando um parâmetro genérico com AsRef<str>:

fn conta_letras<S: AsRef<str>>(s: S) -> usize {
    s.as_ref().chars().count()
}

fn main() {
    let texto = String::from("hello");
    println!("{}", conta_letras(&texto));
    println!("{}", conta_letras("world"));
}

No exemplo, a função conta_letras aceita qualquer tipo que possa ser emprestado como &str. Isso torna a função mais flexível e evita a necessidade de converter explicitamente.

Você pode implementar AsRef para seus próprios tipos. Por exemplo, se você tem um tipo Nome que contém uma String, pode implementar AsRef<str> para permitir o acesso direto à string:

struct Nome(String);

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

fn main() {
    let nome = Nome(String::from("Alice"));
    let s: &str = nome.as_ref();
    println!("{}", s);
}

Note que AsRef é para conversões de referência baratas e sem falhas. Para conversões que alocam memória ou podem falhar, prefira From ou TryFrom.

Idiomático

Em Rust, o uso correto dessas traits é parte do estilo idiomático. Aqui estão algumas diretrizes:

  • Prefira implementar From em vez de Into, pois a primeira fornece a segunda automaticamente e é mais intuitiva.
  • Use TryFrom para conversões que podem falhar, e defina um tipo de erro claro.
  • Use AsRef e AsMut para conversões de referência que são baratas e não falham, especialmente para aceitar diferentes tipos de strings ou slices em funções.
  • Evite implementar From para conversões que podem perder dados ou falhar; nesses casos, use TryFrom.
  • Para conversões entre tipos numéricos, use o operador as apenas quando a perda de dados for aceitável e você tiver certeza do intervalo; caso contrário, prefira TryFrom.
  • Quando você precisa converter entre tipos que são conceitualmente iguais, como String para &str, use as_str() ou as_ref() em vez de From.

Um padrão comum é implementar From para seus tipos de erro, permitindo usar o operador ? para propagar erros de forma concisa. Por exemplo:

#[derive(Debug)]
enum Erro {
    Conversao(String),
}

impl From<std::num::ParseIntError> for Erro {
    fn from(e: std::num::ParseIntError) -> Self {
        Erro::Conversao(e.to_string())
    }
}

fn parse_num(s: &str) -> Result<i32, Erro> {
    let n = s.parse::<i32>()?;
    Ok(n)
}

Esse padrão permite que o operador ? converta automaticamente o erro de ParseIntError para o nosso tipo Erro.

Referências

Exercícios

  1. Crie um tipo Temperatura que armazene um valor em Celsius (f64). Implemente From<f64> para criar uma temperatura a partir de um valor em Celsius. Depois, use Into para converter um valor em Celsius para Temperatura.

    ✓ Resposta:
    struct Temperatura {
        celsius: f64,
    }
    
    impl From<f64> for Temperatura {
        fn from(celsius: f64) -> Self {
            Temperatura { celsius }
        }
    }
    
    fn main() {
        let t = Temperatura::from(25.0);
        let t2: Temperatura = 30.0.into();
        println!("{:?} {:?}", t.celsius, t2.celsius);
    }
    
  2. Implemente TryFrom<u32> para um tipo Nota que só aceita valores entre 0 e 10 (inclusive). Use um erro do tipo String para descrever o problema. Teste com valores válidos e inválidos.

    ✓ Resposta:
    struct Nota(u8);
    
    impl TryFrom<u32> for Nota {
        type Error = String;
    
        fn try_from(valor: u32) -> Result<Self, Self::Error> {
            if valor <= 10 {
                Ok(Nota(valor as u8))
            } else {
                Err(format!("Nota inválida: {}", valor))
            }
        }
    }
    
    fn main() {
        let nota = Nota::try_from(8);
        let nota_invalida = Nota::try_from(11);
        println!("{:?} {:?}", nota, nota_invalida);
    }
    
  3. Escreva uma função genérica imprimir que aceite qualquer tipo que implemente AsRef<str> e imprima o conteúdo como string. Teste com String, &str e um tipo customizado que implemente AsRef<str>.

    ✓ Resposta:
    fn imprimir<S: AsRef<str>>(s: S) {
        println!("{}", s.as_ref());
    }
    
    struct Nome(String);
    
    impl AsRef<str> for Nome {
        fn as_ref(&self) -> &str {
            &self.0
        }
    }
    
    fn main() {
        imprimir(String::from("Olá"));
        imprimir("Mundo");
        imprimir(Nome(String::from("Rust")));
    }
    
  4. Implemente AsMut<[u8]> para um tipo Buffer que encapsule um Vec<u8>. Use as_mut para modificar o conteúdo do buffer diretamente.

    ✓ Resposta:
    struct Buffer {
        dados: Vec<u8>,
    }
    
    impl AsMut<[u8]> for Buffer {
        fn as_mut(&mut self) -> &mut [u8] {
            &mut self.dados
        }
    }
    
    fn main() {
        let mut buf = Buffer { dados: vec![1, 2, 3] };
        let slice: &mut [u8] = buf.as_mut();
        slice[0] = 99;
        println!("{:?}", buf.dados);
    }
    
  5. Explique a diferença entre From e AsRef. Dê um exemplo onde cada um é mais apropriado.

    ✓ Resposta:

    From é usado para conversões que criam um novo valor (por exemplo, converter &str em String, que aloca memória). Já AsRef é usado para conversões de referência que não alocam e apenas emprestam o valor original (por exemplo, obter &str a partir de String). Use From quando você precisa de um valor próprio (dono) e AsRef quando você só precisa de uma referência para leitura.

Boas Práticas e Observações Finais

Ao trabalhar com conversões em Rust, lembre-se de que a clareza e a segurança são fundamentais. Prefira sempre as traits da biblioteca padrão em vez de criar suas próprias funções de conversão. Isso torna o código mais previsível e integrado ao ecossistema.

Outra boa prática é implementar From para seus tipos de erro, facilitando a propagação com o operador ?. Isso reduz a verbosidade e mantém o fluxo de erro consistente.

Por fim, não abuse de as para conversões numéricas. Use TryFrom sempre que houver risco de perda de dados, e documente claramente quando usar as for aceitável (por exemplo, quando você tem garantias de intervalo).