Conversões: From, Into, TryFrom
Nesta aula, você aprenderá a realizar conversões de tipos em Rust de forma idiomática e segura, utilizando as traits From, Into, TryFrom, TryInto, AsRef e AsMut. Exploraremos como essas traits facilitam a conversão entre tipos, com exemplos práticos e boas práticas.
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
Fromem vez deInto, pois isso dá a você a implementação deIntogratuitamente. - Use
TryFromeTryIntopara conversões que podem falhar, e trate os erros de forma explícita. - Use
AsRefeAsMutpara conversões de referência que não devem falhar e que não consomem o valor original. - Evite implementar
FromouTryFrompara 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
- Documentação oficial da trait From
- Documentação oficial da trait Into
- Documentação oficial da trait TryFrom
- Documentação oficial da trait TryInto
- Documentação oficial da trait AsRef
- Documentação oficial da trait AsMut
- The Rust Book: Advanced Traits
Exercícios
- Exercício 1: Implemente
From<u32> for MeuTipo, ondeMeuTipoé uma struct com um campovalor: u32. Use.into()para converter umu32emMeuTipo. - Exercício 2: Crie uma conversão de
i64parai32usandoTryFrom. Trate o caso em que o valor está fora do intervalo e retorne um erro personalizado. - Exercício 3: Escreva uma função genérica que aceite qualquer tipo que implemente
AsRef<str>e imprima o comprimento da string. Teste comStringe&str. - Exercício 4: Implemente
From<MeuTipo> for Stringpara uma structMeuTipo { valor: u32 }, convertendo o valor em uma string. Use.into()para obter a string. - 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 umVec<u8>e umaString.
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);
}
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),
}
}
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);
}
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);
}
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.