Pular para o conteúdo

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í.

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: 44
com try_from: Err(TryFromIntError(PosOverflow))
com from: 300
▶ Experimente você mesmo

Esse 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. From só é implementada quando nada pode dar errado. Quando algo pode dar errado, você usa TryFrom, que retorna um Result.
  • Elas funcionam com os seus próprios tipos. Você não pode escrever (255, 0, 0) as Color, mas pode implementar From<(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 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 -> String
let big = i64::from(42_i32); // i32 -> i64, sempre cabe
let precise = f64::from(7_u8); // u8 -> f64, sempre exato
let 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.

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=235
▶ Experimente você mesmo

Esta é 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 uma Color a 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)
▶ Experimente você mesmo

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ê.

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.

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))
▶ Experimente você mesmo

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.

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)
▶ Experimente você mesmo

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(&note)); // &String
println!("{}", word_count(note)); // String
}

Saída:

3
4
4
▶ Experimente você mesmo

Dentro da função, text.as_ref() te dá um &str simples, seja lá o que quem chamou tenha passado.

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:

txt
jpeg
gz
(nenhuma)
▶ Experimente você mesmo

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.

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().

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 this
13 | 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` trait

O 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.)

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 45min
25:00 -> erro: 25:00 não é um horário de verdade
almoço -> erro: falta o ':'
▶ Experimente você mesmo

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.

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:30
Timer ajustado para 1:05
▶ Experimente você mesmo

Só 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 as serve 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 From para conversões que sempre funcionam, e você ganha Into de graça. O .into() precisa saber o tipo de destino.
  • O operador ? chama o From para converter tipos de erro, então impl From<OutroErro> for MeuErro deixa o tratamento de erros curto.
  • Use TryFrom / TryInto quando uma conversão pode falhar. Elas retornam um Result.
  • Implemente FromStr para que .parse::<SeuTipo>() funcione com texto.
  • Use impl AsRef<str> ou impl AsRef<Path> para aceitar vários tipos de entrada sem custo, sem copiar nada.

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