From e Into Traits em Rust

Guia completo sobre os traits From e Into em Rust: conversão de tipos, implementação blanket, tratamento de erros e exemplos práticos.

O que são From e Into?

Os traits From e Into formam o sistema de conversão de tipos do Rust. Eles permitem transformar um valor de um tipo em outro de forma segura, explícita e idiomática.

  • From<T>: define como criar um tipo a partir de outro tipo T.
  • Into<U>: define como converter um tipo para outro tipo U.

A beleza desse sistema é que implementar FromInto de graça. Graças a uma implementação blanket na biblioteca padrão, se você implementa From<A> for B, automaticamente A ganha Into<B>. Por isso, a convenção é sempre implementar From, nunca Into diretamente.


Definição dos Traits

// Definido em std::convert
pub trait From<T>: Sized {
    fn from(value: T) -> Self;
}

pub trait Into<T>: Sized {
    fn into(self) -> T;
}

A implementação blanket que conecta os dois:

// Na biblioteca padrão (simplificado)
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

Isso significa: para qualquer tipo T, se U implementa From<T>, então T automaticamente implementa Into<U>.


Como Implementar

Implementação de From

From não pode ser derivado — você sempre implementa manualmente:

struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Self {
        Celsius((f.0 - 32.0) * 5.0 / 9.0)
    }
}

fn main() {
    // Usando From::from explicitamente
    let fahr = Fahrenheit::from(Celsius(100.0));
    println!("100°C = {}°F", fahr.0); // 212°F

    // Usando .into() (funciona por causa da blanket impl)
    let celsius: Celsius = Fahrenheit(32.0).into();
    println!("32°F = {}°C", celsius.0); // 0°C
}

From na biblioteca padrão

Rust já fornece dezenas de implementações de From:

fn main() {
    // String::from(&str)
    let s = String::from("olá mundo");

    // Vec<u8> from String
    let bytes: Vec<u8> = Vec::from(s.clone());

    // String from Vec<u8> (pode falhar — use String::from_utf8)
    // Mas &str from &[u8] que é válido UTF-8 usa from_utf8_unchecked

    // i64 from i32 (conversão sem perda)
    let grande: i64 = i64::from(42i32);

    // Box<str> from String
    let boxed: Box<str> = Box::from(s);

    println!("{}", boxed);
}

Exemplos Práticos

Exemplo 1: Conversão de tipos de domínio

#[derive(Debug)]
struct Email(String);

#[derive(Debug)]
struct Usuario {
    nome: String,
    email: Email,
}

impl From<String> for Email {
    fn from(s: String) -> Self {
        Email(s)
    }
}

impl From<&str> for Email {
    fn from(s: &str) -> Self {
        Email(s.to_string())
    }
}

fn main() {
    // From::from
    let email1 = Email::from("ana@exemplo.com");

    // .into() — o compilador infere o tipo alvo
    let email2: Email = String::from("bruno@exemplo.com").into();

    let usuario = Usuario {
        nome: String::from("Ana"),
        email: email1,
    };

    println!("{:?}", usuario);
}

Exemplo 2: From para conversão de erros

Um dos usos mais poderosos de From é na conversão de erros com o operador ?:

use std::fmt;
use std::num::ParseIntError;
use std::io;

#[derive(Debug)]
enum AppErro {
    Io(io::Error),
    Parse(ParseIntError),
    Validacao(String),
}

impl fmt::Display for AppErro {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppErro::Io(e) => write!(f, "Erro de I/O: {}", e),
            AppErro::Parse(e) => write!(f, "Erro de parse: {}", e),
            AppErro::Validacao(msg) => write!(f, "Validação: {}", msg),
        }
    }
}

impl From<io::Error> for AppErro {
    fn from(e: io::Error) -> Self {
        AppErro::Io(e)
    }
}

impl From<ParseIntError> for AppErro {
    fn from(e: ParseIntError) -> Self {
        AppErro::Parse(e)
    }
}

// Agora o operador ? converte automaticamente!
fn ler_numero_do_arquivo(caminho: &str) -> Result<i32, AppErro> {
    let conteudo = std::fs::read_to_string(caminho)?;  // io::Error → AppErro
    let numero: i32 = conteudo.trim().parse()?;         // ParseIntError → AppErro
    if numero < 0 {
        return Err(AppErro::Validacao("Número negativo".into()));
    }
    Ok(numero)
}

fn main() {
    match ler_numero_do_arquivo("numero.txt") {
        Ok(n) => println!("Número: {}", n),
        Err(e) => eprintln!("Erro: {}", e),
    }
}

Exemplo 3: Into em parâmetros de função genérica

Usar Into<T> como bound em parâmetros torna a API mais flexível:

#[derive(Debug)]
struct Mensagem {
    texto: String,
}

impl Mensagem {
    // Aceita qualquer coisa que possa virar String
    fn nova(texto: impl Into<String>) -> Self {
        Mensagem {
            texto: texto.into(),
        }
    }
}

fn saudacao(nome: impl Into<String>) -> String {
    format!("Olá, {}!", nome.into())
}

fn main() {
    // Funciona com &str
    let msg1 = Mensagem::nova("olá");

    // Funciona com String
    let msg2 = Mensagem::nova(String::from("mundo"));

    println!("{:?}", msg1);
    println!("{:?}", msg2);

    // A função aceita ambos os tipos
    println!("{}", saudacao("Ana"));
    println!("{}", saudacao(String::from("Bruno")));
}

Exemplo 4: Construindo tipos a partir de tuplas

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

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

impl From<[f64; 2]> for Ponto {
    fn from(arr: [f64; 2]) -> Self {
        Ponto { x: arr[0], y: arr[1] }
    }
}

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

fn main() {
    let p1 = Ponto::from((3.0, 4.0));
    let p2: Ponto = [1.0, 2.0].into();

    println!("{:?}", p1); // Ponto { x: 3.0, y: 4.0 }
    println!("{:?}", p2); // Ponto { x: 1.0, y: 2.0 }

    let tupla: (f64, f64) = p1.into();
    println!("{:?}", tupla); // (3.0, 4.0)
}

Exemplo 5: TryFrom para conversões que podem falhar

Quando a conversão pode falhar, use TryFrom em vez de From:

use std::convert::TryFrom;

#[derive(Debug)]
struct Porcentagem(u8);

impl TryFrom<i32> for Porcentagem {
    type Error = String;

    fn try_from(valor: i32) -> Result<Self, Self::Error> {
        if valor < 0 || valor > 100 {
            Err(format!("Valor {} fora do intervalo 0-100", valor))
        } else {
            Ok(Porcentagem(valor as u8))
        }
    }
}

fn main() {
    let p1 = Porcentagem::try_from(85);
    let p2 = Porcentagem::try_from(150);

    println!("{:?}", p1); // Ok(Porcentagem(85))
    println!("{:?}", p2); // Err("Valor 150 fora do intervalo 0-100")

    // Também funciona com .try_into()
    let p3: Result<Porcentagem, _> = 50i32.try_into();
    println!("{:?}", p3); // Ok(Porcentagem(50))
}

Padrões e Boas Práticas

  1. Implemente From, não Into: Sempre implemente From<T> for U. Você ganha Into<U> for T automaticamente via blanket impl.

  2. Use Into<T> em parâmetros: Quando uma função aceita um tipo, use impl Into<String> ou impl Into<T> para tornar a API mais ergonômica.

  3. Conversões de erro com From: Implemente From<SubErro> for MeuErro para que o operador ? converta erros automaticamente.

  4. From deve ser infalível: Se a conversão pode falhar, use TryFrom em vez de From. From é um contrato de que a conversão sempre terá sucesso.

  5. Não implemente From para si mesmo: From<T> for T (identidade) já é implementado na biblioteca padrão.

  6. String::from vs .to_string(): Ambos convertem &str para String. String::from("texto") usa From, enquanto "texto".to_string() usa Display. Ambos são idiomáticos.

  7. Conversões numéricas: Para conversões entre tipos numéricos sem perda (como i32 para i64), From já está implementado. Para conversões com possível perda, use as ou TryFrom.


Veja Também