Conversões em Rust: From, Into, TryFrom, FromStr e AsRef
Programas convertem coisas o tempo todo. O texto de um formulário vira um número. Uma tupla com três números vira uma Color. Um tipo de erro vira outro. O Rust tem uma pequena família de traits (características) da biblioteca padrão para isso e, depois que você as conhece, passa a vê-las em todo lugar no código de verdade.
Pense nessas traits como os adaptadores de tomada que você leva numa viagem ao exterior. Cada um serve para um trabalho. Alguns sempre encaixam (From). Alguns podem não encaixar, e avisam você (TryFrom). Alguns leem uma etiqueta e montam a coisa (FromStr). E alguns simplesmente deixam você ligar o que tiver na mão (AsRef). Esta página passa por cada um, com calma. Você já conheceu o as na página de Conversão de tipos e deu uma primeira olhada no From na página de Traits comuns, então vamos partir daí.
Por que não usar só o as?
Seção intitulada “Por que não usar só o as?”O as é rápido, mas tem dois limites. Ele só funciona entre tipos simples embutidos na linguagem, como números, char e bool. E ele nunca falha: se um valor não cabe, ele te entrega outro número em silêncio.
fn main() { let items_in_cart: i32 = 300;
// `as` espreme 300 dentro de um u8 e erra sem avisar let squeezed = items_in_cart as u8; println!("com as: {squeezed}");
// `try_from` percebe o problema e te avisa let checked = u8::try_from(items_in_cart); println!("com try_from: {:?}", checked);
// `from` só existe quando a conversão nunca perde dados let widened = i64::from(items_in_cart); println!("com from: {widened}");}Saída:
com as: 44com try_from: Err(TryFromIntError(PosOverflow))com from: 300Esse 44 é o tipo de bug que fica escondido por meses. As traits de conversão resolvem isso de três formas:
- Elas são honestas.
Fromsó é implementada quando nada pode dar errado. Quando algo pode dar errado, você usaTryFrom, que retorna umResult. - Elas funcionam com os seus próprios tipos. Você não pode escrever
(255, 0, 0) as Color, mas pode implementarFrom<(u8, u8, u8)> for Color. - O resto do código entende essas traits. O operador
?, o.parse()e muitas funções de bibliotecas são construídos em cima delas.
Então uma boa regra prática: deixe o as para truques rápidos e seguros com números, e use as traits em todo o resto.
From e Into: conversões que sempre funcionam
Seção intitulada “From e Into: conversões que sempre funcionam”From diz “eu sei me construir a partir daquele outro tipo”. Você já usou. Muitos tipos da biblioteca padrão implementam essa trait:
let greeting = String::from("olá"); // &str -> Stringlet big = i64::from(42_i32); // i32 -> i64, sempre cabelet precise = f64::from(7_u8); // u8 -> f64, sempre exatolet scores = Vec::from([90, 85, 77]); // array -> Vec<i32>Repare no que está faltando: não existe i32::from(algum_i64). Um i64 pode não caber em um i32, então o Rust não finge que sempre funciona. Vamos voltar a isso na seção sobre TryFrom.
Implementando From para o seu próprio tipo
Seção intitulada “Implementando From para o seu próprio tipo”Aqui uma Color pode ser construída a partir de uma tupla com os valores de vermelho, verde e azul:
#[derive(Debug)]struct Color { red: u8, green: u8, blue: u8,}
impl From<(u8, u8, u8)> for Color { fn from((red, green, blue): (u8, u8, u8)) -> Self { Color { red, green, blue } }}
fn main() { let orange = Color::from((255, 136, 0)); println!("{:?}", orange);
// Nunca escrevemos `Into`, mas ganhamos de graça: let sky: Color = (135, 206, 235).into(); println!("céu: r={} g={} b={}", sky.red, sky.green, sky.blue);}Saída:
Color { red: 255, green: 136, blue: 0 }céu: r=135 g=206 b=235Esta é a parte boa. A biblioteca padrão diz: “se B implementa From<A>, então A implementa Into<B> automaticamente”. Então um único impl From te dá dois jeitos de converter:
Color::from(tuple)se lê como “construa umaColora partir disto”.tuple.into()se lê como “transforme isto no que for preciso aqui”.
Por que escrevemos let sky: Color? Porque o .into() não diz em quê transformar. Em teoria, uma tupla poderia ser convertida em vários tipos. A anotação diz ao Rust qual você quer. Se o Rust já consegue ver o tipo de destino, por exemplo pelo parâmetro de uma função, ela não é necessária.
Você também vai ver bastante o .into() em parâmetros de funções. Uma função como fn new(title: impl Into<String>) aceita tanto um &str quanto uma String, e chama title.into() por dentro. Ali o Rust sabe o destino, então nenhuma anotação é necessária. Você vai experimentar esse padrão no exercício 2 da Prática.
From no tratamento de erros: como o ? converte erros
Seção intitulada “From no tratamento de erros: como o ? converte erros”Este é um lugar onde o From trabalha bastante por você, sem fazer barulho. Quando o ? encontra um Err, ele não só o retorna. Antes, ele chama From::from no erro, para transformá-lo no tipo de erro que a sua função retorna.
Então, se a sua função retorna o seu próprio AppError, e AppError implementa From<ParseIntError>, o ? consegue transformar sozinho um erro de leitura de número em um AppError:
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { MissingEquals, BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
// Lê uma linha de configuração como "max_users = 50"fn read_setting(line: &str) -> Result<(String, u32), AppError> { let (key, value) = line.split_once('=').ok_or(AppError::MissingEquals)?; let number = value.trim().parse::<u32>()?; // ParseIntError -> AppError Ok((key.trim().to_string(), number))}
fn main() { println!("{:?}", read_setting("max_users = 50")); println!("{:?}", read_setting("max_users = muitos")); println!("{:?}", read_setting("max_users 50"));}Saída:
Ok(("max_users", 50))Err(BadNumber(ParseIntError { kind: InvalidDigit }))Err(MissingEquals)Sem o impl From, aquele segundo ? não compilaria, porque o Rust não saberia transformar um ParseIntError em um AppError. Com ele, o corpo da sua função fica limpo. A página de Erros personalizados se aprofunda no design de tipos de erro, e a página de anyhow e thiserror mostra crates que escrevem esses impl From por você.
TryFrom e TryInto: conversões que podem falhar
Seção intitulada “TryFrom e TryInto: conversões que podem falhar”Algumas conversões só funcionam às vezes. 300 não cabe em um u8. -5 não é uma porcentagem válida. Para esses casos, o Rust tem o TryFrom. Ele funciona como o From, mas retorna um Result, então você é obrigado a lidar com o caso do “não”. Isso é bom: a falha não consegue passar escondida.
Você já viu a versão embutida no começo desta página: u8::try_from(300_i32) devolve um Err em vez de um número errado. E, assim como o From vem com o Into, o TryFrom vem com o TryInto, então let small: Result<u8, _> = big.try_into(); também funciona. O TryInto está no prelude desde a edição 2021, então você não precisa importar nada.
Implementando TryFrom para o seu próprio tipo
Seção intitulada “Implementando TryFrom para o seu próprio tipo”Vamos criar um tipo Percentage que só pode guardar valores de 0 a 100. Se toda Percentage for criada por meio do try_from, então toda Percentage do seu programa é válida. Você confere uma vez, na porta de entrada, e nunca mais.
#[derive(Debug)]struct Percentage(u8);
#[derive(Debug)]struct OutOfRange(i32);
impl TryFrom<i32> for Percentage { type Error = OutOfRange;
fn try_from(value: i32) -> Result<Self, Self::Error> { if (0..=100).contains(&value) { Ok(Percentage(value as u8)) // seguro: acabamos de conferir o intervalo } else { Err(OutOfRange(value)) } }}
fn main() { println!("{:?}", Percentage::try_from(85)); println!("{:?}", Percentage::try_from(150));
let battery: Result<Percentage, _> = (-5).try_into(); println!("{:?}", battery);}Saída:
Ok(Percentage(85))Err(OutOfRange(150))Err(OutOfRange(-5))A linha type Error = OutOfRange; diz ao Rust que tipo de erro essa conversão retorna. Quem escolhe é você. Aqui ele carrega o valor inválido, então quem receber o erro pode mostrar uma mensagem útil.
FromStr e .parse(): construindo valores a partir de texto
Seção intitulada “FromStr e .parse(): construindo valores a partir de texto”Texto é a coisa que você mais vai converter. Arquivos, argumentos de linha de comando e formulários web te entregam strings. A trait FromStr descreve “como construir este tipo a partir de um &str”, e o .parse() é o jeito amigável de chamá-la.
Quando você escreve "42".parse::<i32>(), o Rust procura a implementação de FromStr do i32 e a executa. A parte ::<i32> (o pessoal chama de “turbofish”) diz ao Rust qual tipo você quer. Uma anotação de tipo na variável também funciona.
let age = "42".parse::<i32>(); // Ok(42)let price: Result<f64, _> = "4.99".parse(); // Ok(4.99)let broken = "quarenta".parse::<i32>(); // Err(ParseIntError { kind: InvalidDigit })Cada chamada retorna um Result, porque o texto sempre pode estar errado.
Implementando FromStr para o seu próprio tipo
Seção intitulada “Implementando FromStr para o seu próprio tipo”Imagine uma loja de molduras onde os clientes digitam tamanhos como "30x40". Vamos ensinar o Rust a ler isso:
use std::str::FromStr;
#[derive(Debug)]struct Size { width: u32, height: u32,}
#[derive(Debug)]enum SizeError { MissingX, BadNumber,}
impl FromStr for Size { type Err = SizeError;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (w, h) = text.split_once('x').ok_or(SizeError::MissingX)?; let width = w.trim().parse().map_err(|_| SizeError::BadNumber)?; let height = h.trim().parse().map_err(|_| SizeError::BadNumber)?; Ok(Size { width, height }) }}
fn main() { println!("{:?}", "30x40".parse::<Size>());
println!("{:?}", "30 por 40".parse::<Size>()); println!("{:?}", "30xgrande".parse::<Size>());}Saída:
Ok(Size { width: 30, height: 40 })Err(MissingX)Err(BadNumber)Dois detalhes pequenos. Primeiro, o FromStr chama o seu tipo de erro de Err, e não de Error como o TryFrom faz. É uma esquisitice antiga de nomenclatura, então não se preocupe se você confundir os dois no começo; o compilador vai te lembrar. Segundo, você precisa de use std::str::FromStr; para implementar a trait, mas não para chamar o .parse().
O .map_err(|_| SizeError::BadNumber) troca o erro da leitura do número pelo nosso próprio erro. É uma alternativa prática a escrever um impl From quando você só precisa disso em um lugar.
AsRef: aceite qualquer coisa que possa te emprestar uma visão
Seção intitulada “AsRef: aceite qualquer coisa que possa te emprestar uma visão”As traits que vimos até aqui criam um valor novo. O AsRef é diferente. Ele não constrói nada. Ele só diz “eu consigo te dar, sem custo, uma visão emprestada de mim mesmo como aquele tipo”.
Por que você ia querer isso? Digamos que você escreva uma função que conta palavras. Se ela recebe &str, quem tem uma String precisa escrever &name. Se ela recebe String, quem tem um &str precisa alocar uma String nova. Com impl AsRef<str>, tudo isso simplesmente funciona:
fn word_count(text: impl AsRef<str>) -> usize { text.as_ref().split_whitespace().count()}
fn main() { let note = String::from("comprar leite e ovos");
println!("{}", word_count("olá mundo rust")); // &str println!("{}", word_count(¬e)); // &String println!("{}", word_count(note)); // String}Saída:
344Dentro da função, text.as_ref() te dá um &str simples, seja lá o que quem chamou tenha passado.
AsRef na biblioteca padrão: caminhos de arquivo
Seção intitulada “AsRef na biblioteca padrão: caminhos de arquivo”Você provavelmente já usou AsRef sem perceber. As funções de arquivo em std::fs são assim:
pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>É por isso que fs::read_to_string("notas.txt"), fs::read_to_string(my_string) e fs::read_to_string(my_path_buf) funcionam. Você pode usar o mesmo truque no seu código. Este exemplo só olha para os nomes dos caminhos, então não mexe em nenhum arquivo de verdade:
use std::path::{Path, PathBuf};
fn extension_of(path: impl AsRef<Path>) -> String { match path.as_ref().extension() { Some(ext) => ext.to_string_lossy().to_string(), None => String::from("(nenhuma)"), }}
fn main() { println!("{}", extension_of("notas.txt")); println!("{}", extension_of(String::from("ferias.jpeg"))); println!("{}", extension_of(PathBuf::from("backups/2024/site.tar.gz"))); println!("{}", extension_of("README"));}Saída:
txtjpeggz(nenhuma)O AsMut segue a mesma ideia, mas empresta uma visão mutável. Por exemplo, tanto um Vec quanto um array podem se emprestar como um slice (fatia) mutável:
fn reset_all(scores: &mut impl AsMut<[u32]>) { for score in scores.as_mut() { *score = 0; }}
// As duas chamadas funcionam:// reset_all(&mut vec![12, 7, 30]);// reset_all(&mut [3, 1, 4]);Você vai usar o AsMut bem menos do que o AsRef. Basta reconhecê-lo quando aparecer.
Qual devo usar?
Seção intitulada “Qual devo usar?”Quando estiver em dúvida, esta tabela é um bom ponto de partida:
| Você quer… | Use | Exemplo |
|---|---|---|
| Converter rapidamente entre tipos numéricos, aceitando truncamento | as |
x as f64 |
| Converter quando nunca pode falhar | From |
i64::from(n), String::from("oi") |
| O mesmo, quando o tipo de destino já é conhecido | Into |
let s: String = "oi".into(); |
| Converter quando pode falhar | TryFrom / TryInto |
u8::try_from(300) |
| Construir um valor a partir de texto | FromStr / .parse() |
"42".parse::<i32>() |
| Aceitar, sem custo, várias entradas que parecem string ou caminho | AsRef |
fn f(p: impl AsRef<Path>) |
| Transformar um valor em texto para pessoas lerem | Display → .to_string() |
42.to_string() |
A última linha vem de graça: implemente Display (veja Traits comuns) e todo valor do seu tipo ganha um método .to_string().
Erros comuns
Seção intitulada “Erros comuns”Chamar .into() quando o Rust não consegue descobrir o tipo de destino.
fn main() { let name = "Ferris".into(); println!("{name}");}error[E0283]: type annotations needed --> src/main.rs:2:9 |2 | let name = "Ferris".into(); | ^^^^ ---- type must be known at this point | = note: the type must implement `From<&str>` = note: required for `&str` to implement `Into<_>`help: consider giving `name` an explicit type |2 | let name: /* Type */ = "Ferris".into(); | ++++++++++++O compilador diz que precisa de uma anotação de tipo. Um &str pode virar uma String, uma Box<str>, um Vec<u8> e mais. O Rust não vai chutar. Coloque o tipo: let name: String = "Ferris".into();, ou escreva simplesmente String::from("Ferris").
Implementar Into em vez de From. Parece que deveria dar no mesmo, então é uma armadilha fácil:
use std::num::ParseIntError;
#[derive(Debug)]struct AppError(String);
impl Into<AppError> for ParseIntError { fn into(self) -> AppError { AppError(self.to_string()) }}
fn read_port(text: &str) -> Result<u16, AppError> { let port = text.parse::<u16>()?; Ok(port)}error[E0277]: `?` couldn't convert the error to `AppError` --> src/main.rs:13:35 |12 | fn read_port(text: &str) -> Result<u16, AppError> { | --------------------- expected `AppError` because of this13 | let port = text.parse::<u16>()?; | --------------^ the trait `From<ParseIntError>` is not implemented for `AppError` | = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` traitO erro diz que o ? não conseguiu converter o erro em AppError, porque From<ParseIntError> não está implementado. O presente grátis só vale em um sentido: implementar From te dá Into, mas implementar Into não te dá From. E o ?, junto com muito código de bibliotecas, pede From. Então implemente sempre o From e deixe o Into vir sozinho. (O Clippy, o linter do Rust, até avisa sobre esse caso.)
Prática
Seção intitulada “Prática”1. Lendo um horário
Seção intitulada “1. Lendo um horário”Crie uma struct Time com hour: u8 e minute: u8. Implemente FromStr para que "09:45".parse::<Time>() funcione. Retorne um erro se não houver :, se uma das partes não for um número, ou se a hora passar de 23 ou o minuto passar de 59. Teste com "09:45", "25:00" e "almoço". Dica: split_once(':') te dá as duas metades. Uma String simples serve como tipo de erro.
Ver solução
use std::str::FromStr;
#[derive(Debug)]struct Time { hour: u8, minute: u8,}
impl FromStr for Time { type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (h, m) = text.split_once(':').ok_or("falta o ':'")?; let hour: u8 = h.parse().map_err(|_| "a hora não é um número")?; let minute: u8 = m.parse().map_err(|_| "o minuto não é um número")?;
if hour > 23 || minute > 59 { return Err(format!("{text} não é um horário de verdade")); } Ok(Time { hour, minute }) }}
fn main() { for input in ["09:45", "25:00", "almoço"] { match input.parse::<Time>() { Ok(t) => println!("{input} -> {}h {}min", t.hour, t.minute), Err(e) => println!("{input} -> erro: {e}"), } }}Saída:
09:45 -> 9h 45min25:00 -> erro: 25:00 não é um horário de verdadealmoço -> erro: falta o ':'O ? depois de .ok_or("falta o ':'") funciona porque String implementa From<&str>. É o mesmo truque do From da seção de tratamento de erros, só que desta vez com um tipo da biblioteca padrão.
2. Um timer de cozinha
Seção intitulada “2. Um timer de cozinha”Crie uma struct Timer com minutes: u32 e seconds: u32. Implemente From<u32> para que um total de segundos vire um Timer (150 vira 2 minutos e 30 segundos). Depois escreva fn start(timer: impl Into<Timer>), que imprime o tempo, e chame-a com 150 e com Timer { minutes: 1, seconds: 5 }. Dica: os minutos são total / 60 e o resto é total % 60. Todo tipo já se converte em si mesmo, então passar um Timer também funciona.
Ver solução
struct Timer { minutes: u32, seconds: u32,}
impl From<u32> for Timer { fn from(total: u32) -> Self { Timer { minutes: total / 60, seconds: total % 60 } }}
fn start(timer: impl Into<Timer>) { let t = timer.into(); println!("Timer ajustado para {}:{:02}", t.minutes, t.seconds);}
fn main() { start(150_u32); start(Timer { minutes: 1, seconds: 5 });}Saída:
Timer ajustado para 2:30Timer ajustado para 1:05Só escrevemos From<u32>, mas a start aceita tanto um número quanto um Timer. O número funciona por causa do nosso From, e o Timer funciona porque a biblioteca padrão diz que todo tipo se converte em si mesmo.
- O
asserve para conversões rápidas entre números e pode perder dados em silêncio. As traits de conversão são explícitas, seguras e funcionam com os seus próprios tipos. - Implemente
Frompara conversões que sempre funcionam, e você ganhaIntode graça. O.into()precisa saber o tipo de destino. - O operador
?chama oFrompara converter tipos de erro, entãoimpl From<OutroErro> for MeuErrodeixa o tratamento de erros curto. - Use
TryFrom/TryIntoquando uma conversão pode falhar. Elas retornam umResult. - Implemente
FromStrpara que.parse::<SeuTipo>()funcione com texto. - Use
impl AsRef<str>ouimpl AsRef<Path>para aceitar vários tipos de entrada sem custo, sem copiar nada.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Você implementa From<Celsius> for Fahrenheit. O que mais você ganha automaticamente?
2. Qual chamada é o jeito seguro de transformar em u8 um i32 que pode valer 300?
3. O que você implementa para que "30x40".parse::<Size>() funcione?
4. Por que let name = "Ferris".into(); não compila?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”std::convert— documentação da biblioteca padrão (em inglês): a casa deFrom,Into,TryFrom,TryInto,AsRefeAsMut, com um guia curto sobre quando implementar cada uma.FromStr— documentação da biblioteca padrão (em inglês): a trait por trás do.parse(), com um exemplo completo de implementação.AsRef— documentação da biblioteca padrão (em inglês): como oAsRefdifere doBorrow, e por que ele é usado para conversões baratas de referências.- Conversion — Rust by Example (em inglês): capítulos curtos e executáveis sobre
From/Into,TryFrom/TryIntoe conversões de e para strings. - Conversions — Rust API Guidelines (em inglês): os conselhos da comunidade sobre quais traits de conversão os seus próprios tipos devem implementar.