Programacao em Rust - Desenvolvi - Jim Blandy
Programacao em Rust - Desenvolvi - Jim Blandy
Desenvolvimento de sistemas
rápidos e seguros
segunda edição revisada – aborda rust edição
2021
Jim Blandy
Jason Orendorff
Leonora F. S. Tindall
Novatec
Authorized Portuguese translation of the English edition of Programming Rust, 2E ISBN
9781492052593 © 2021 Jim Blandy, Leonora F. S. Tindall, Jason Orendorff. This translation
is published and sold by permission of O'Reilly Media, Inc., the owner of all rights to publish
and sell the same.
Tradução em português autorizada da edição em inglês da obra Programming Rust, 2E
ISBN 9781492052593 © 2021 Jim Blandy, Leonora F. S. Tindall, Jason Orendorff. Esta
tradução é publicada e vendida com a permissão da O'Reilly Media, Inc., detentora de todos
os direitos para publicação e venda desta obra.
© Novatec Editora Ltda. [2023].
Todos os direitos reservados e protegidos pela Lei 9.610 de 19/02/1998. É proibida a
reprodução desta obra, mesmo parcial, por qualquer processo, sem prévia autorização, por
escrito, do autor e da Editora.
Editor: Rubens Prates BAR20230720
Tradução: Edson Furmankiewicz
Revisão da tradução: Nilo Ney Coutinho Menezes
Revisão gramatical: Tássia Carvalho
ISBN do impresso: 978-85-7522-860-9
ISBN do ebook: 978-85-7522-861-6
Histórico de impressões:
Agosto/2023 Primeira edição
Novatec Editora Ltda.
Rua Luís Antônio dos Santos 110
02460-000 – São Paulo, SP – Brasil
Tel.: +55 11 2959-6529
Email: [email protected]
Site: https://novatec.com.br
Twitter:twitter.com/novateceditora
Facebook: facebook.com/novatec
LinkedIn: linkedin.com/company/novatec-editora/
BAR20230720
Sumário
Prefácio
Capítulo 1 Os programadores de sistemas podem ter
coisas legais
O Rust carrega o peso para você
A programação paralela foi domada
E, mesmo assim, o Rust ainda é rápido
O Rust facilita a colaboração
Capítulo 2 Um passeio pelo Rust
rustup e Cargo
Funções no Rust
Escrevendo e executando testes unitários
Manipulando argumentos de linha de comando
Servindo páginas para a web
Concorrência
O que é realmente o conjunto de Mandelbrot
Analisando argumentos de linha de comando em pares
Mapeando pixels para números complexos
Plotando o conjunto
Gravando arquivos de imagem
Um programa de Mandelbrot concorrente
Executando o plotter de Mandelbrot
A segurança é invisível
Sistemas de arquivos e ferramentas de linha de comando
Interface de linha de comando
Lendo e gravando arquivos
Localizar e substituir
Capítulo 3 Tipos fundamentais
Tipos numéricos de tamanho fixo
Tipos inteiros
Aritmética: verificação, enquadramento, saturação e estouro
Tipos de ponto flutuante
Tipo bool
Caracteres
Tuplas
Tipos de ponteiro
Referências
Boxes
Ponteiros brutos
Arrays, vetores e fatias
Arrays
Vetores
Fatias
Tipos de string
Literais de string
Strings de bytes
Strings na memória
String
Usando strings
Outros tipos semelhantes a strings
Aliases de tipo
Além do básico
Capítulo 4 Posse e movimentos
Posse (ownership)
Movimentos
Mais operações que movem
Movimentos e fluxo de controle
Movimentos e conteúdo indexado
Tipos de cópia: A exceção aos movimentos
Rc e Arc: Posse compartilhada
Capítulo 5 Referências
Referências a valores
Trabalhando com referências
Referências Rust versus referências C++
Atribuição de referências
Referências a referências
Comparando referências
Referências nunca são nulas
Emprestando referências a expressões arbitrárias
Referências a fatias e objetos trait
Segurança de referência
Emprestando uma variável local
Recebendo referências como argumentos de função
Passando referências para funções
Retornando referências
Structs contendo referências
Parâmetros de tempo de vida distintos
Omitindo parâmetros de tempo de vida
Compartilhamento versus mutação
Pegando em armas contra um mar de objetos
Capítulo 6 Expressões
Uma linguagem de expressão
Precedência e associatividade
Blocos e pontos e vírgulas
Declarações
if e match
if let
Loops
Fluxo de controle em loops
Expressões return
Por que o Rust tem loops
Chamadas de função e método
Campos e elementos
Operadores de referência
Operadores aritméticos, de bit a bit, de comparação e lógicos
Atribuição
Coerções de tipo
Closures
A seguir
Capítulo 7 Tratamento de erros
Pânico
Desempilhamento
Abortando
Result
Capturando erros
Aliases do tipo Result
Imprimindo erros
Propagando erros
Trabalhando com vários tipos de erro
Lidando com erros que “não podem acontecer”
Ignorando erros
Tratando erros em main()
Declarando um tipo de erro personalizado
Por que Results?
Capítulo 8 Crates e módulos
Crates
Edições
Perfis de compilação
Módulos
Módulos aninhados
Módulos em arquivos separados
Caminhos e importações
Prelúdio padrão
Criando declarações use públicas
Criando campos struct públicos
Variáveis estáticas e constantes
Transformando um programa em uma biblioteca
Diretório src/bin
Atributos
Testes e documentação
Testes de integração
Documentação
Doc-Tests
Especificando dependências
Versões
Cargo.lock
Publicando crates em crates.io
Espaços de trabalho
Mais coisas legais
Capítulo 9 Structs
Structs de campo nomeado
Structs do tipo tupla
Structs do tipo unidade
Disposição de um struct na memória
Definindo métodos com impl
Passando Self como um Box, Rc ou Arc
Funções associadas a tipos
Consts associadas
Structs genéricos
Structs genéricos com parâmetros de tempo de vida
Structs genéricos com parâmetros constantes
Derivando traits comuns para tipos de struct
Mutabilidade interior
Capítulo 10 Enums e padrões
Enums
Enums com dados
Enums na memória
Estruturas de dados ricas usando enums
Enums genéricos
Padrões
Literais, variáveis e curingas em padrões
Padrões com tupla e struct
Padrões de array e fatia
Padrões de referência
Guardas de correspondência
Combinando múltiplas possibilidades
Vinculando com padrões @
Onde os padrões são permitidos
Preenchendo uma árvore binária
Visão geral
Capítulo 11 Traits e genéricos
Utilizando traits
Objetos trait
Funções genéricas e parâmetros de tipo
Qual utilizar
Definindo e implementando traits
Métodos padrão
Traits e tipos de outras pessoas
Self em traits
Subtraits
Funções associadas a tipos
Chamadas de método totalmente qualificadas
Traits que definem relacionamentos entre tipos
Tipos associados (ou como funcionam os iteradores)
Traits genéricos (ou como funciona a sobrecarga do operador)
impl Trait
Consts associadas
Engenharia reversa de limites
Traits como uma fundação
Capítulo 12 Sobrecarga de operador
Operadores aritméticos e de bit a bit
Operadores unários
Operadores binários
Operadores de atribuição composta
Comparações de equivalência
Comparações ordenadas
Index e IndexMut
Outros operadores
Capítulo 13 Traits utilitários
Drop
Sized
Clone
Copy
Deref e DerefMut
Default
AsRef e AsMut
Borrow e BorrowMut
From e Into
TryFrom e TryInto
ToOwned
Borrow e ToOwned em funcionamento: Cow (“clone on write”)
Capítulo 14 Closures
Capturando variáveis
Closures que pedem emprestado
Closures que roubam
Função e tipos de closure
Desempenho de closure
Closures e segurança
Closures que matam
FnOnce
FnMut
Copy e Clone para closures
Callbacks
Usando closures de forma eficaz
Capítulo 15 Iteradores
Traits Iterator e IntoIterator
Criando iteradores
Métodos iter e iter_mut
Implementações de IntoIterator
from_fn e sucessores
Métodos drain
Outras fontes do iterador
Adaptadores iteradores
map e filter
filter_map e flat_map
flatten
take e take_while
skip e skip_while
peekable
fuse
Iteradores reversíveis e rev
inspect
chain
enumerate
zip
by_ref
cloned, copied
cycle
Consumindo iteradores
Acumulação simples: contagem, soma, produto
max, min
max_by, min_by
max_by_key, min_by_key
Comparando sequências de itens
any e all
position, rposition e ExactSizeIterator
fold e rfold
try_fold e try_rfold
nth, nth_back
last
find, rfind e find_map
Construindo coleções: collect e FromIterator
Trait Extend
partition
for_each e try_for_each
Implementando seus próprios iteradores
Capítulo 16 Coleções
Visão geral
Vec<T>
Acessando elementos
Iteração
Crescendo e encolhendo vetores
Unindo
Dividindo
Permutando
Preenchimento
Ordenando e pesquisando
Comparando fatias
Elementos aleatórios
O Rust descarta erros de invalidação
VecDeque<T>
BinaryHeap<T>
HashMap<K, V> e BTreeMap<K, V>
Entradas
Iteração em mapas
HashSet<T> e BTreeSet<T>
Iteração em conjuntos
Quando valores iguais são diferentes
Operações sobre o conjunto inteiro
Hash
Usando um algoritmo de hash personalizado
Além das coleções padrão
Capítulo 17 Strings e texto
Princípios básicos de Unicode
ASCII, Latin-1 e Unicode
UTF-8
Direcionalidade do texto
Caracteres (char)
Classificando caracteres
Manipulando dígitos
Conversão de maiúsculas e minúsculas para caracteres
Conversões de e para inteiros
String e str
Criando valores string
Inspeção simples
Acrescentando e inserindo texto
Removendo e substituindo texto
Convenções para pesquisa e iteração
Padrões para pesquisar texto
Pesquisando e substituindo
Iterando por texto
Recortando uma string
Conversão de maiúsculas e minúsculas para strings
Convertendo outros tipos a partir de strings
Convertendo outros tipos em strings
Emprestando como outros tipos semelhantes a texto
Acessando texto como UTF-8
Produzindo texto a partir de dados UTF-8
Adiando a alocação
Strings como coleções genéricas
Formatando valores
Formatando valores de texto
Formatando números
Formatando outros tipos
Valores de formatação para depuração
Ponteiros de formatação para depuração
Referindo-se a argumentos por índice ou nome
Larguras dinâmicas e precisões
Formatando os próprios tipos
Usando a linguagem de formatação em seu próprio código
Expressões regulares
Uso básico de Regex
Construindo valores regex preguiçosamente (lazily)
Normalização
Formas de normalização
Crate de normalização Unicode
Capítulo 18 Entrada e saída
Leitores e escritores
Leitores
Leitores bufferizados
Lendo linhas
Coletando linhas
Escritores
Arquivos
Posicionando
Outros tipos de leitores e escritores
Dados binários, compactação e serialização
Arquivos e diretórios
OsStr e Path
Métodos Path e PathBuf
Funções de acesso ao sistema de arquivos
Lendo diretórios
Recursos específicos da plataforma
Redes
Capítulo 19 Concorrência
Paralelismo fork-join
spawn e join
Tratamento de erros entre threads
Compartilhamento de dados imutáveis entre threads
Rayon
Revisitando o conjunto de Mandelbrot
Canais
Enviando valores
Recebendo valores
Executando o pipeline
Recursos e desempenho do canal
Segurança de thread: Send e Sync
Utilizando pipes em quase qualquer iterador para um canal
Além de pipelines
Estado mutável compartilhado
O que é um mutex?
Mutex<T>
mut e Mutex
Por que mutexes nem sempre são uma boa ideia
Deadlock
Mutexes envenenados
Canais multiconsumidores usando mutexes
Bloqueios de leitura/escrita (RwLock<T>)
Variáveis de condição (Condvar)
Atômicos
Variáveis globais
Como é hackear código concorrente em Rust
Capítulo 20 Programação assíncrona
De síncrono a assíncrono
Futuros
Funções assíncronas e expressões await
Chamando funções assíncronas a partir do código síncrono:
block_on
Gerando tarefas assíncronas
Blocos assíncronos
Construindo funções assíncronas a partir de blocos assíncronos
Gerando tarefas assíncronas em um pool de threads
Mas seu futuro implementa Send?
Cálculos de longa duração: yield_now e spawn_blocking
Comparando projetos assíncronos
Um cliente HTTP assíncrono real
Um cliente e servidor assíncronos
Tipos de erro e resultado
O protocolo
Recebendo a entrada do usuário: Fluxos assíncronos
Enviando pacotes
Recebendo pacotes: mais fluxos assíncronos
A função main do cliente
A função main do servidor
Lidando com conexões de bate-papo: Mutexes assíncronos
A tabela de grupo: Mutexes síncronos
Grupos de bate-papo: canais de transmissão do tokio
Primitivas de futuro e executores: Quando vale a pena verificar um
futuro novamente?
Invocando wakers: spawn_blocking
Implementando block_on
Fixando
As duas fases da vida de um futuro
Ponteiros fixados
Trait Unpin
Quando o código assíncrono é útil?
Capítulo 21 Macros
Noções básicas de macro
Noções básicas de expansão de macro
Consequências não intencionais
Repetição
Macros internas
Depurando macros
Construindo a macro json!
Tipos de fragmento
Recursão em macros
Usando traits com macros
Escopo e higiene
Importando e exportando macros
Evitando erros de sintaxe durante a correspondência
Para além de macro_rules!
Capítulo 22 Código inseguro
Inseguro de quê?
Blocos inseguros
Exemplo: Um tipo de string ASCII eficiente
Funções inseguras
Bloco inseguro ou função insegura?
Comportamento indefinido
Traits inseguros
Ponteiros brutos
Desreferenciando ponteiros brutos com segurança
Exemplo: RefWithFlag
Ponteiros que podem ser nulos
Tamanhos e alinhamentos de tipos
Aritmética de ponteiro
Movendo dados para dentro e para fora da memória
Exemplo: GapBufferGenericName
Segurança contra pânico em código inseguro
Reinterpretando a memória com uniões
Correspondendo uniões
Pegando emprestadas uniões
Capítulo 23 Funções externas
Encontrando representações de dados comuns
Declarando funções e variáveis externas
Usando funções de bibliotecas
Uma interface bruta para libgit2
Uma interface segura para libgit2
Conclusão
Prefácio
Agradecimentos
O livro que você tem em mãos se beneficiou muito da atenção de
nossos revisores técnicos oficiais: Brian Anderson, Matt Brubeck, J.
David Eisenberg, Ryan Levick, Jack Moffitt, Carol Nichols e Erik
Nordin; e de nossos tradutores: Hidemoto Nakada ( 中 田 秀 基 )
(japonês), Sr. Songfeng Li (chinês simplificado) e Adam Bochenek e
Krzysztof Sawka (polonês).
Muitos outros revisores não oficiais leram os primeiros rascunhos e
forneceram feedback inestimável. Gostaríamos de agradecer a Eddy
Bruel, Nick Fitzgerald, Graydon Hoare, Michael Kelly, Jeffrey Lim,
Jakob Olesen, Gian-Carlo Pascutto, Larry Rabinowitz, Jaroslav Šnajdr,
Joe Walker e Yoshua Wuyts pelos comentários atenciosos. Jeff
Walden e Nicolas Pierron foram especialmente generosos com seu
tempo, revisando quase todo o livro. Como qualquer
empreendimento de programação, um livro de programação se
destaca quando conta com relatórios de bugs de qualidade.
Obrigado.
A Mozilla foi extremamente compreensiva com o trabalho de Jim e
Jason neste projeto, embora estivesse fora de nossas
responsabilidades oficiais e competisse com eles por nossa atenção.
Agradecemos aos gerentes de Jim e Jason: Dave Camp, Naveed
Ihsanullah, Tom Tromey e Joe Walker, pelo apoio. Eles têm uma
visão ampla do que é a Mozilla; esperamos que esses resultados
justifiquem a confiança que depositaram em nós.
Também gostaríamos de expressar nossa gratidão a todos na
O’Reilly que ajudaram a concretizar este projeto, especialmente
nossos surpreendentemente pacientes editores Jeff Bleiel e Brian
MacDonald, e nosso editor de aquisições Zan McQuade.
Acima de tudo, nossos sinceros agradecimentos a nossas famílias
pelo amor inabalável, entusiasmo e paciência.
1 capítulo
Os programadores de
sistemas podem ter coisas
legais
rustup e Cargo
A melhor maneira de instalar o Rust é utilizar rustup. Visite
https://rustup.rs e siga as instruções.
Você pode, alternativamente, ir para o site do Rust
(https://oreil.ly/4Q2FB) a fim de obter pacotes pré-compilados para
Linux, macOS e Windows. O Rust também está incluído em algumas
distribuições de sistemas operacionais. Preferimos rustup porque é
uma ferramenta para gerenciar instalações Rust, como RVM para
Ruby ou NVM para Node. Por exemplo, quando uma nova versão do
Rust for lançada, você poderá atualizar apenas digitando rustup update.
De qualquer forma, depois de concluir a instalação, você deverá ter
três novos comandos disponíveis em sua linha de comando:
$ cargo --version
cargo 1.56.0 (4ed5d137b 2021-10-04)
$ rustc --version
rustc 1.56.0 (09c42c458 2021-10-18)
$ rustdoc --version
rustdoc 1.56.0 (09c42c458 2021-10-18)
Aqui o caractere $ é o prompt de comando; no Windows, isso seria
C:\> ou algo semelhante. Executamos os três comandos que
instalamos, pedindo a cada um que informe sua versão. Examinando
cada um dos comandos, temos:
• cargo é o gerenciador de compilação, gerenciador de pacotes e
ferramenta de uso geral do Rust. Você pode utilizar o Cargo para
iniciar um novo projeto, criar e executar seu programa e gerenciar
quaisquer bibliotecas externas das quais seu código dependa.
• rustc é o compilador do Rust. Normalmente, deixamos o Cargo
invocar o compilador para nós, mas às vezes é útil executá-lo
diretamente.
• rustdoc é a ferramenta de documentação do Rust. Se você escrever
documentação na forma apropriada, em comentários no código-
fonte do seu programa, rustdoc pode construir HTML bem
formatado a partir deles. Como ocorre com rustc, geralmente
deixamos o Cargo executar rustdoc para nós.
Por conveniência, o Cargo pode criar um novo pacote Rust para nós,
com alguns metadados padrão organizados adequadamente:
$ cargo new hello
Created binary (application) `hello` package
Esse comando cria um novo diretório de pacotes chamado hello,
pronto para criar um executável de linha de comando.
Examinando o diretório de nível superior do pacote:
$ cd hello
$ ls -la
total 24
drwxrwxr-x. 4 jimb jimb 4096 Sep 22 21:09 .
drwx------. 62 jimb jimb 4096 Sep 22 21:09 ..
drwxrwxr-x. 6 jimb jimb 4096 Sep 22 21:09 .git
-rw-rw-r--. 1 jimb jimb 7 Sep 22 21:09 .gitignore
-rw-rw-r--. 1 jimb jimb 88 Sep 22 21:09 Cargo.toml
drwxrwxr-x. 2 jimb jimb 4096 Sep 22 21:09 src
Podemos ver que o Cargo criou um arquivo Cargo.toml a fim de
armazenar metadados para o pacote. No momento, esse arquivo
tem poucos dados:
[package]
name = "hello"
version = "0.1.0"
edition = "2021"
# Veja mais chaves e suas definições em
# https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
Se nosso programa tiver dependências de outras bibliotecas,
podemos registrá-las nesse arquivo e o Cargo se encarregará de
baixar, criar e atualizar essas bibliotecas para nós. Abordaremos o
arquivo Cargo.toml em detalhes no Capítulo 8.
O Cargo configurou nosso pacote para uso com o sistema de
controle de versão git, criando um subdiretório .git de metadados e
um arquivo .gitignore. Você pode configurar o Cargo para pular essa
etapa passando --vcs none para cargo new na linha de comando.
O subdiretório src contém o código-fonte Rust real:
$ cd src
$ ls -l
total 4
-rw-rw-r--. 1 jimb jimb 45 Sep 22 21:09 main.rs
Parece que o Cargo começou a escrever o programa para nós. O
arquivo main.rs contém o texto:
fn main() {
println!("Hello, world!");
}
No Rust, você nem precisa escrever o próprio programa “Hello,
World!”. E é esta a extensão do código boilerplate1 para um novo
programa Rust: dois arquivos, totalizando treze linhas.
Podemos invocar o comando cargo run de qualquer diretório no pacote
para construir e executar nosso programa:
$ cargo run
Compiling hello v0.1.0 (/home/jimb/rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `/home/jimb/rust/hello/target/debug/hello`
Hello, world!
Aqui, o Cargo invocou o compilador do Rust, rustc e, em seguida,
rodou o executável que foi gerado. O Cargo coloca o executável no
subdiretório target no topo do pacote:
$ ls -l ../target/debug
total 580
drwxrwxr-x. 2 jimb jimb 4096 Sep 22 21:37 build
drwxrwxr-x. 2 jimb jimb 4096 Sep 22 21:37 deps
drwxrwxr-x. 2 jimb jimb 4096 Sep 22 21:37 examples
-rwxrwxr-x. 1 jimb jimb 576632 Sep 22 21:37 hello
-rw-rw-r--. 1 jimb jimb 198 Sep 22 21:37 hello.d
drwxrwxr-x. 2 jimb jimb 68 Sep 22 21:37 incremental
$ ../target/debug/hello
Hello, world!
Quando terminarmos, o Cargo pode apagar os arquivos gerados
para nós:
$ cargo clean
$ ../target/debug/hello
bash: ../target/debug/hello: No such file or directory
Funções no Rust
A sintaxe do Rust é deliberadamente trivial. Se você estiver
familiarizado com C, C++, Java ou JavaScript, provavelmente poderá
se localizar na estrutura geral de um programa Rust. Eis uma função
que calcula o máximo divisor comum de dois números inteiros,
utilizando o algoritmo de Euclides (https://oreil.ly/DFpyb). Você pode
adicionar este código ao final de src/main.rs:
fn gcd(mut n: u64, mut m: u64) -> u64 {
assert!(n != 0 && m != 0);
while m != 0 {
if m < n {
let t = m;
m = n;
n = t;
}
m = m % n;
}
n
}
A palavra-chave fn (pronuncia-se “fun”) introduz uma função. Aqui,
estamos definindo uma função chamada gcd, que recebe dois
parâmetros, n e m, cada um dos quais é do tipo u64, um inteiro de
64 bits sem sinal. O token -> precede o tipo de retorno: nossa
função retorna um valor u64. O recuo de quatro espaços é o estilo
padrão do Rust.
Os nomes do tipo inteiro da linguagem Rust refletem seu tamanho e
sinal (com ou sem): i32 é um inteiro de 32 bits com sinal; u8 é um
inteiro de 8 bits sem sinal (usado para valores que cabem num
“byte”) e assim por diante. Os tipos isize e usize armazenam inteiros
com e sem sinal de um ponteiro, com tamanho de 32 bits nas
plataformas de 32 bits, e 64 bits nas plataformas de 64 bits. O Rust
também tem dois tipos de ponto flutuante, f32 e f64, que são os tipos
de ponto flutuante de precisão simples e dupla IEEE, como float e
double em C e C++.
Por padrão, uma vez que uma variável é inicializada, seu valor não
pode ser alterado, mas colocar a palavra-chave mut (pronuncia-se
“mute”, abreviação de mutable) antes dos parâmetros n e m permite
que o corpo da nossa função os modifique. Na prática, a maioria das
variáveis não é modificada; mas, naquelas que são, a palavra-chave
mut pode ser uma dica útil ao se ler o código.
O corpo da função começa com uma chamada à macro assert!,
verificando se nenhum dos argumentos é zero. O caractere ! marca
isso como uma invocação de macro, e não uma chamada de função.
Como ocorre com a macro assert em C e C++, assert! do Rust verifica
se o seu argumento é verdadeiro e, se não for, encerra o programa
com uma mensagem útil, incluindo o local de origem da falha
verificada; esse tipo de término abrupto é chamado de pânico. Ao
contrário de C e C++, nos quais as asserções podem ser ignoradas,
o Rust sempre verifica as asserções, independentemente de como o
programa foi compilado. Também há uma macro debug_assert!, cujas
asserções são ignoradas quando o programa é compilado para ter
maior velocidade.
O coração da nossa função é um loop while contendo uma instrução if
e uma atribuição. Ao contrário de C e C++, o Rust não requer
parênteses em torno das expressões condicionais, mas requer
chaves em torno das instruções que elas controlam.
Uma instrução let declara uma variável local, como t em nossa
função. Não precisamos escrever o tipo de t, desde que o Rust possa
inferi-lo a partir de como a variável é utilizada. Em nossa função, o
único tipo que funciona para t é u64, correspondendo a m e n. O Rust
apenas infere tipos dentro do corpo de uma função: você deve
escrever os tipos de parâmetro da função e seus valores de retorno,
como fizemos antes. Se quiséssemos explicitar o tipo de t,
poderíamos escrever:
let t: u64 = m;
O Rust tem uma instrução return, mas a função gcd não precisa de
uma. Se o corpo de uma função terminar com uma expressão que
não é seguida por um ponto e vírgula, esse é o valor de retorno da
função. Na verdade, qualquer bloco entre chaves pode funcionar
como uma expressão. Por exemplo, esta é uma expressão que
imprime uma mensagem e, em seguida, produz x.cos() como seu
valor:
{
println!("evaluating cos x");
x.cos()
}
É típico no Rust empregar essa forma para estabelecer o valor da
função quando esta não tiver valor de retorno, e usar instruções
return somente para retornos antecipados explícitos do meio de uma
função.
fn main() {
let server = HttpServer::new(|| {
App::new()
.route("/", web::get().to(get_index))
});
println!("Serving on http://localhost:3000...");
server
.bind("127.0.0.1:3000").expect("error binding server to address")
.run().expect("error running server");
}
fn get_index() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html")
.body(
r#"
<title>GCD Calculator</title>
<form action="/gcd" method="post">
<input type="text" name="n"/>
<input type="text" name="m"/>
<button type="submit">Compute GCD</button>
</form>
"#,
)
}
Começamos com uma instrução use para facilitar a obtenção de
algumas das definições do crate actix-web. Quando escrevemos use
actix_web::{...}, cada um dos nomes listados dentro das chaves torna-
se diretamente utilizável em nosso código; em vez de ter de digitar o
nome completo actix_web::HttpResponse cada vez que o utilizamos,
podemos simplesmente o referenciar como HttpResponse. (Vamos
chegar ao crate serde daqui a pouco.)
Nossa função main é simples: chama HttpServer::new para criar um
servidor que responda às solicitações de um único caminho, "/";
imprime uma mensagem lembrando-nos de como nos conectar a
ele; e, em seguida, define-o como escuta na porta TCP 3000 na
máquina local.
O argumento que passamos para HttpServer::new é a expressão closure
|| { App::new() ... }. Uma closure é um valor que pode ser chamado
como se fosse uma função. Essa closure não requer argumentos,
mas, se precisasse, seus nomes apareceriam entre barras
verticais ||. O { ... } é o corpo da closure. Quando iniciamos nosso
servidor, o Actix inicia um pool de threads para lidar com as
solicitações recebidas. Cada thread chama nossa closure para obter
uma nova cópia do valor App que informa como rotear e tratar
solicitações.
O closure chama App::new para criar um novo App vazio e, então,
chama seu método route para acrescentar uma única rota ao
caminho "/". O manipulador fornecido para essa rota,
web::get().to(get_index),
trata requisições HTTP GET chamando a função
get_index. O método route retorna o mesmo App que foi invocado, agora
aprimorado com a nova rota. Como não há ponto e vírgula no final
do corpo do closure, o App é o valor de retorno do closure, pronto
para o thread HttpServer utilizar.
A função get_index constrói um valor HttpResponse que representa a
resposta a uma solicitação HTTP GET /. HttpResponse::Ok() representa um
status HTTP 200 OK, indicando que a solicitação foi bem-sucedida.
Chamamos seus métodos content_type e body para preencher os
detalhes da resposta; cada chamada retorna o HttpResponse que
recebeu, com as modificações feitas. Por fim, o valor de retorno de
body serve como o valor de retorno de get_index.
Como o texto da resposta contém muitas aspas duplas, escrevemos
utilizando a sintaxe “raw string” (“string bruta”) do Rust: a letra r,
zero ou mais cerquilhas (ou seja, o caractere #), uma aspa dupla e,
em seguida, o conteúdo da string, finalizado por outra aspa dupla
seguida pelo mesmo número de cerquilhas. Qualquer caractere pode
ocorrer em uma string bruta sem escape, incluindo aspas duplas; na
verdade, nenhuma sequência de escape como \" é reconhecida.
Sempre podemos garantir que a string termine onde pretendemos,
utilizando mais cerquilhas em torno das aspas que sempre aparecem
no texto.
Tendo escrito main.rs, podemos utilizar o comando cargo run para
fazer tudo o que é necessário para executá-lo: buscar os crates
necessários, compilá-los, construir nosso próprio programa, vincular
tudo e iniciá-lo:
$ cargo run
Updating crates.io index
Downloading crates ...
Downloaded serde v1.0.100
Downloaded actix-web v1.0.8
Downloaded serde_derive v1.0.100
...
Compiling serde_json v1.0.40
Compiling actix-router v0.1.5
Compiling actix-http v0.2.10
Compiling awc v0.2.7
Compiling actix-web v1.0.8
Compiling gcd v0.1.0 (/home/jimb/rust/actix-gcd)
Finished dev [unoptimized + debuginfo] target(s) in 1m 24s
Running `/home/jimb/rust/actix-gcd/target/debug/actix-gcd`
Serving on http://localhost:3000...
Neste ponto, podemos visitar a URL fornecida em nosso navegador e
ver a página mostrada anteriormente na Figura 2.1.
Infelizmente, clicar em Compute GCD (Calcular MDC) não faz nada
além de levar nosso navegador para uma página em branco. Vamos
corrigir isso a seguir, adicionando outra rota ao nosso App para lidar
com a solicitação POST em nosso formulário.
Finalmente chegou a hora de utilizar o crate serde que listamos em
nosso arquivo Cargo.toml: ele fornece uma ferramenta útil que nos
ajudará a processar os dados do formulário. Primeiro, precisamos
adicionar a seguinte diretiva use na parte superior de src/main.rs:
use serde::Deserialize;
Os programadores Rust geralmente reúnem todas as suas
declarações use na parte superior do arquivo, mas isso não é
estritamente necessário: o Rust permite que as declarações ocorram
em qualquer ordem, desde que apareçam no nível apropriado de
indentação.
Em seguida, vamos definir um tipo de estrutura Rust que representa
os valores que esperamos de nosso formulário:
#[derive(Deserialize)]
struct GcdParameters {
n: u64,
m: u64,
}
Isso define um novo tipo chamado GcdParameters que tem dois
campos, n e m, cada um dos quais é um u64 – o tipo de argumento
que nossa função gcd espera.
A anotação acima da definição struct é um atributo, como o atributo #
[test] que utilizamos anteriormente para marcar as funções de teste.
Colocar um atributo #[derive(Deserialize)] acima de uma definição de tipo
instrui o crate serde a examinar o tipo quando o programa for
compilado e a gerar automaticamente um código para analisar um
valor desse tipo a partir de dados no formato que os formulários
HTML utilizam para solicitações POST. Na verdade, esse atributo é
suficiente para permitir que você analise um valor GcdParameters de
quase qualquer tipo de dados estruturados: JSON, YAML, TOML ou
qualquer um de vários outros formatos textuais e binários. O crate
serde também fornece um atributo Serialize que gera código para fazer
o inverso, pegar valores Rust e escrevê-los em um formato
estruturado.
Com essa definição, podemos escrever nossa função de tratamento
com bastante facilidade:
fn post_gcd(form: web::Form<GcdParameters>) -> HttpResponse {
if form.n == 0 || form.m == 0 {
return HttpResponse::BadRequest()
.content_type("text/html")
.body("Computing the GCD with zero is boring.");
}
let response =
format!("The greatest common divisor of the numbers {} and {} \
is <b>{}</b>\n",
form.n, form.m, gcd(form.n, form.m));
HttpResponse::Ok()
.content_type("text/html")
.body(response)
}
Para que uma função sirva como um manipulador de solicitações do
Actix, todos os seus argumentos devem ter tipos que o Actix saiba
como extrair de uma solicitação HTTP. Nossa função post_gcd recebe
um argumento, form, cujo tipo é web::Form<GcdParameters>. O Actix sabe
como extrair um valor de qualquer tipo web::Form<T> de uma
solicitação HTTP se, e somente se, T puder ser desserializado a partir
dos dados POST do formulário HTML. Como colocamos o atributo #
[derive(Deserialize)] em nossa definição de tipo GcdParameters, o Actix pode
desserializá-lo a partir dos dados do formulário, portanto as funções
manipuladoras de solicitação podem esperar um valor
web::Form<GcdParameters> como parâmetro. Essas relações entre tipos e
funções são todas trabalhadas em tempo de compilação; se
escrevermos uma função manipuladora com um tipo de argumento
que o Actix não saiba manipular, o compilador Rust nos informará
sobre nosso erro imediatamente.
Examinando post_gcd, a função primeiro retorna um erro HTTP 400 BAD
REQUEST se um dos parâmetros for zero, pois nossa função gcd gera
um pânico caso sejam. Em seguida, a função constrói uma resposta
para a solicitação utilizando a macro format!. A macro format! é como a
macro println!, exceto que, em vez de escrever o texto na saída
padrão, ela o retorna como uma string. Uma vez obtido o texto da
resposta, post_gcd empacota-o em uma resposta HTTP 200 OK, define
seu tipo de conteúdo e o retorna para ser entregue ao remetente.
Também temos de registrar post_gcd como o manipulador do
formulário. Agora vamos substituir nossa função main por esta
versão:
fn main() {
let server = HttpServer::new(|| {
App::new()
.route("/", web::get().to(get_index))
.route("/gcd", web::post().to(post_gcd))
});
println!("Serving on http://localhost:3000...");
server
.bind("127.0.0.1:3000").expect("error binding server to address")
.run().expect("error running server");
}
A única mudança aqui é que adicionamos outra chamada para route,
estabelecendo web::post().to(post_gcd) como o manipulador para o
caminho "/gcd".
A última peça restante é a função gcd que escrevemos
anteriormente, e que fica no arquivo actix-gcd/src/main.rs. Com isso
no lugar, você pode interromper qualquer servidor que tenha
deixado em execução, recompilar e reiniciar o programa:
$ cargo run
Compiling actix-gcd v0.1.0 (/home/jimb/rust/actix-gcd)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/actix-gcd`
Serving on http://localhost:3000...
Desta vez, visitando http://localhost:3000, inserindo alguns números
e clicando no botão Compute GCD, você deve ver alguns resultados
(Figura 2.2).
Concorrência
Um dos grandes pontos fortes do Rust é o suporte à programação
concorrente. As mesmas regras que garantem que os programas
Rust estejam livres de erros de memória também garantem que os
threads possam compartilhar memória apenas de maneiras que
evitem corridas de dados (data races). Por exemplo:
• Se você utilizar um mutex para coordenar threads que fazem
alterações em uma estrutura de dados compartilhada, o Rust
garante que você não possa acessar os dados, exceto quando
estiver segurando o bloqueio, e libera o bloqueio automaticamente
quando você terminar. Em C e C++, o relacionamento entre um
mutex e os dados que ele protege é deixado para os comentários.
• Se você deseja compartilhar dados somente leitura entre vários
threads, o Rust garante que você não possa modificar os dados
acidentalmente. Em C e C++, o sistema de tipos pode ajudar com
isso, mas é fácil errar.
• Se você transferir a posse de uma estrutura de dados de um
thread para outro, o Rust garante que você realmente cedeu todo
o acesso a ele. Em C e C++, cabe a você verificar se nada no
thread de envio terá acesso aos dados novamente. Se você não
acertar, os efeitos podem depender do que está no cache do
processador e de quantas gravações na memória você fez
recentemente. Não que sejamos pessimistas.
Nesta seção, vamos guiá-lo pelo processo de escrever seu segundo
programa multithread.
Você já escreveu o primeiro: o framework web Actix utilizado para
implementar o servidor do máximo divisor comum usa um pool de
threads para executar funções manipuladoras de solicitação. Se o
servidor receber requisições simultâneas, ele pode executar as
funções get_index e post_gcd em vários segmentos ao mesmo tempo.
Isso pode ser um pouco chocante, já que certamente não tínhamos
a concorrência em mente quando escrevemos essas funções. Mas
Rust garante que isso é seguro, não importa o quão elaborado seu
servidor seja: se o seu programa compilar, ele estará livre de
corridas de dados (data races). Todas as funções do Rust são
thread-safe.
O programa desta seção plota o conjunto de Mandelbrot, um fractal
gerado pela iteração de uma função simples em números complexos.
Plotar o conjunto de Mandelbrot é frequentemente chamado de
algoritmo embaraçosamente paralelo, porque o padrão de
comunicação entre os threads é muito simples; abordaremos
padrões mais complexos no Capítulo 19, mas essa tarefa demonstra
alguns dos fundamentos.
Para começar, criaremos um novo projeto Rust:
$ cargo new mandelbrot
Created binary (application) `mandelbrot` package
$ cd mandelbrot
Todo o código entrará mandelbrot/src/main.rs, e adicionaremos
algumas dependências a mandelbrot/Cargo.toml.
Antes de entrarmos na implementação concorrente de Mandelbrot,
precisamos descrever a computação que vamos realizar.
[dependencies]
num = "0.4"
Agora podemos escrever a penúltima versão do nosso loop:
use num::Complex;
fn complex_square_add_loop(c: Complex<f64>) {
let mut z = Complex { re: 0.0, im: 0.0 };
loop {
z = z * z + c;
}
}
É costume utilizar z para números complexos, então renomeamos
nossa variável de loop. A expressão Complex { re: 0.0, im: 0.0 } é a
maneira como escrevemos o zero complexo utilizando o tipo Complex
do crates num. Complex é um tipo de estrutura do Rust (ou struct),
assim definido:
struct Complex<T> {
/// Parte real do número complexo
re: T,
/// Parte imaginária do número complexo
im: T,
}
O código anterior define uma estrutura chamada Complex, com dois
campos, re e im. Complex é uma estrutura genérica: você pode ler o
<T> após o nome do tipo como “para qualquer tipo T”. Por exemplo,
Complex<f64> é um número complexo cujos campos re e im são valores
f64, Complex<f32> utilizaria floats de 32 bits e assim por diante. Dada
essa definição, uma expressão como Complex { re: 0.24, im: 0.3 } produz
um valor Complex com seu campo re inicializado em 0.24, e seu
campo im inicializado em 0.3.
O crate num arranja para *, + e outros operadores aritméticos para
trabalhar valores Complex, então o restante da função funciona
exatamente como a versão anterior, exceto que opera em pontos no
plano complexo, não apenas em pontos ao longo da reta numérica
real. Explicaremos no Capítulo 12 como você pode fazer os
operadores do Rust funcionarem com seus próprios tipos.
Finalmente, chegamos ao destino de nossa excursão em matemática
pura. O conjunto de Mandelbrot é definido como o conjunto dos
números complexos c para o qual z não tende ao infinito. Nosso loop
quadrado simples original era previsível o suficiente: qualquer
número maior que 1 ou menor que -1 desaparece. Adicionar um + c
a cada iteração torna o comportamento um pouco mais difícil de
antecipar: como dissemos anteriormente, os valores de c maiores
que 0,25 ou menores que -2 fazem z tender ao infinito. Mas expandir
o jogo para números complexos produz padrões verdadeiramente
bizarros e elegantes, que é o que queremos plotar.
Como um número complexo c tem componentes reais e imaginários
c.re e c.im, vamos tratá-los como as coordenadas x e y de um ponto
no plano cartesiano e pintar o ponto de preto se c estiver no
conjunto de Mandelbrot, ou uma cor mais clara, caso contrário.
Portanto, para cada pixel em nossa imagem, devemos executar o
loop anterior no ponto correspondente no plano complexo, ver se ele
escapa para o infinito ou orbita em torno da origem para sempre, e
colori-lo de acordo.
O loop infinito demora um pouco para rodar, mas há dois truques
para os impacientes. Primeiro, se desistirmos de executar o loop
indefinidamente e apenas tentarmos um número limitado de
iterações, veremos que ainda obteremos uma aproximação decente
do conjunto. Saber de quantas iterações precisamos vai depender de
quão precisamente queremos plotar o limite. Em segundo lugar, foi
demonstrado que, se z alguma vez deixar o círculo de raio 2
centrado na origem, ele definitivamente sumirá no infinito em algum
momento, longe da origem. Então, eis a versão final do nosso loop e
o coração do nosso programa:
use num::Complex;
None
}
Essa função pega o número complexo c que queremos testar para
saber se pertence ao conjunto de Mandelbrot e um limite para o
número de iterações a serem tentadas antes de desistir (limit) e
declarar que c provavelmente é um membro.
O valor de retorno da função é um Option<usize>. A biblioteca padrão
do Rust define o tipo Option da seguinte forma:
enum Option<T> {
None,
Some(T),
}
é um tipo enumerado, muitas vezes chamado de enum, porque
Option
sua definição enumera diversas variantes que um valor desse tipo
poderia assumir: para qualquer tipo T, um valor do tipo Option<T> ou
é Some(v), em que v é um valor do tipo T, ou é None, indicando que
nenhum valor do tipo T está disponível. Como o tipo Complex que
discutimos anteriormente, Option é um tipo genérico: você pode
utilizar Option<T> para representar um valor opcional de qualquer
tipo T que quiser.
No nosso caso, escape_time retorna um Option<usize> para indicar se c
está no conjunto de Mandelbrot – e, se não estiver, quantas vezes
tivemos de iterar para descobrir isso. Se c não está no conjunto,
escape_time retorna Some(i), em que i é o número da iteração em que z
deixou o círculo de raio 2. Caso contrário, c está aparentemente no
conjunto, e escape_time retorna None.
for i in 0..limit {
Os exemplos anteriores mostraram loops for iterando sobre
argumentos de linha de comando e elementos vetoriais; esse loop for
simplesmente itera sobre o intervalo de inteiros começando com 0 e
indo até (mas não incluindo) limit.
A chamada de método z.norm_sqr() retorna o quadrado da distância de
z em relação à origem. Para decidir se z saiu do círculo de raio 2, em
vez de calcular uma raiz quadrada, apenas comparamos a distância
ao quadrado com 4,0, que é mais rápido.
Você deve ter notado que utilizamos /// para marcar as linhas de
comentário acima da definição da função; os comentários acima dos
membros da estrutura Complex também começam com ///. Esses são
comentários da documentação; a rustdoc utilitário sabe como analisá-
los, com o código que eles descrevem, e produzir documentação
online. A documentação da biblioteca padrão do Rust está escrita
dessa forma. Descrevemos os comentários da documentação em
detalhes no Capítulo 8.
O restante do programa se preocupa em decidir qual parte do
conjunto plotar e em qual resolução, e então em distribuir o trabalho
por vários threads para acelerar o cálculo.
/// Analise a string `s` como um par de coordenadas, como `"400x600"` ou `"1.0,0.5"`.
///
/// Especificamente, `s` deve ter a forma <left><sep><right>, onde <sep> é
/// o caractere dado pelo argumento `separator`, e <left> e <right> são
/// strings que podem ser analisadas por `T::from_str`. `separator`
/// deve ser um caractere ASCII.
///
/// Se `s` tiver a forma adequada, retorna `Some<(x, y)>`.
/// Se não analisar corretamente, retorna `None`.
fn parse_pair<T: FromStr>(s: &str, separator: char) -> Option<(T, T)> {
match s.find(separator) {
None => None,
Some(index) => {
match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) {
(Ok(l), Ok(r)) => Some((l, r)),
_ => None
}
}
}
}
#[test]
fn test_parse_pair() {
assert_eq!(parse_pair::<i32>("", ','), None);
assert_eq!(parse_pair::<i32>("10,", ','), None);
assert_eq!(parse_pair::<i32>(",10", ','), None);
assert_eq!(parse_pair::<i32>("10,20", ','), Some((10, 20)));
assert_eq!(parse_pair::<i32>("10,20xy", ','), None);
assert_eq!(parse_pair::<f64>("0.5x", 'x'), None);
assert_eq!(parse_pair::<f64>("0.5x1.5", 'x'), Some((0.5, 1.5)));
}
A definição de parse_pair é uma função genérica:
fn parse_pair<T: FromStr>(s: &str, separator: char) -> Option<(T, T)> {
Você pode ler a cláusula <T: FromStr> em voz alta, como “Para
qualquer tipo T que implementa o trait FromStr...”. Isso efetivamente
nos permite definir uma família inteira de funções de uma só vez:
parse_pair::<i32> é uma função que analisa pares de i32 valores,
parse_pair::<f64> analisa pares de valores de ponto flutuante e assim
por diante. Isso é muito parecido com um modelo de função em
C++. Um programador Rust chamaria T um parâmetro de tipo de
parse_pair. Quando você usa uma função genérica, o Rust geralmente
consegue inferir parâmetros de tipo para você, e não é necessário
escrevê-los como fizemos no código de teste.
Nosso tipo de retorno é Option<(T, T)>: ou None ou um valor
Some((v1, v2)), em que (v1, v2) é uma tupla de dois valores, ambos do
tipo T. A função parse_pair não usa uma instrução de retorno explícita,
então seu valor de retorno é o valor da última (e única) expressão
em seu corpo:
match s.find(separator) {
None => None,
Some(index) => {
...
}
}
O método find do tipo String procura na string um caractere que
corresponda a separator. Se find retornar None, o que significa que o
caractere separador não ocorre na string, toda a expressão match é
avaliada como None, indicando que a análise falhou. Caso contrário,
assumimos que index seja a posição do separador na string.
match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) {
(Ok(l), Ok(r)) => Some((l, r)),
_ => None
}
Isso começa a mostrar o poder da expressão match. O argumento
para a correspondência é esta expressão de tupla:
(T::from_str(&s[..index]), T::from_str(&s[index + 1..]))
As expressões &s[..index] e &s[index + 1..] são fatias da string,
precedendo e sucedendo o separador. A função associada from_str do
parâmetro de tipo T pega cada um deles e tenta analisá-los como
um valor do tipo T, produzindo uma tupla de resultados. Isto é o que
nós comparamos:
(Ok(l), Ok(r)) => Some((l, r)),
Esse padrão corresponde apenas se ambos os elementos da tupla
forem variantes Ok do tipo Result, indicando que ambas as análises
foram bem-sucedidas. Se sim, Some((l, r)) é o valor da expressão de
correspondência e, portanto, o valor de retorno da função.
_ => None
O padrão curinga _ corresponde a qualquer coisa e ignora seu valor.
Se chegamos a este ponto, então parse_pair falhou e, portanto,
avaliamos como None, novamente fornecendo o valor de retorno da
função.
Agora que temos parse_pair, é fácil escrever uma função para analisar
um par de coordenadas de ponto flutuante e retorná-las como um
valor Complex<f64>:
/// Analisa um par de números de ponto flutuante separados
/// por uma vírgula como um número complexo
fn parse_complex(s: &str) -> Option<Complex<f64>> {
match parse_pair(s, ',') {
Some((re, im)) => Some(Complex { re, im }),
None => None
}
}
#[test]
fn test_parse_complex() {
assert_eq!(parse_complex("1.25,-0.0625"),
Some(Complex { re: 1.25, im: -0.0625 }));
assert_eq!(parse_complex(",-0.0625"), None);
}
A função parse_complex chama parse_pair, constrói um valor Complex se as
coordenadas foram analisadas com sucesso e passa as falhas para
seu chamador.
Se você leu atentamente, deve ter notado que utilizamos uma
notação abreviada para construir o valor Complex. É comum inicializar
os campos de uma struct com variáveis de mesmo nome; então, em
vez de forçá-lo a escrever Complex { re: re, im: im }, o Rust permite que
você simplesmente escreva Complex { re, im }. Isso é modelado em
notações semelhantes em JavaScript e Haskell.
#[test]
fn test_pixel_to_point() {
assert_eq!(pixel_to_point((100, 200), (25, 175),
Complex { re: -1.0, im: 1.0 },
Complex { re: 1.0, im: -1.0 }),
Complex { re: -0.5, im: -0.75 });
}
A Figura 2.4 ilustra o cálculo que pixel_to_point realiza.
O código de pixel_to_point é simplesmente cálculo, então não vamos
explicá-lo em detalhes. Contudo, há alguns aspectos a apontar.
Expressões com essa forma referenciam elementos de tupla:
pixel.0
Isso referencia o primeiro elemento da tupla pixel.
pixel.0 as f64
Essa é a sintaxe do Rust para uma conversão de tipo: isso converte
pixel.0 em um valor f64. Ao contrário de C e C++, Rust geralmente se
recusa a converter entre tipos numéricos implicitamente; você deve
escrever as conversões necessárias. Isso pode ser tedioso, mas ser
explícito sobre quais conversões ocorrem e quando ocorrem é
surpreendentemente útil. As conversões implícitas de inteiros
parecem bastante inocentes, mas historicamente elas têm sido uma
fonte frequente de bugs e falhas de segurança em código C e C++
do mundo real.
Plotando o conjunto
Para plotar o conjunto de Mandelbrot, para cada pixel da imagem,
simplesmente aplicamos escape_time para o ponto correspondente no
plano complexo e pintamos o pixel dependendo do resultado:
/// Renderiza um retângulo do conjunto de Mandelbrot em um buffer de pixels.
/// O argumento `bounds` dá a largura e a altura dos `pixels` do buffer,
/// que contém um pixel em tons de cinza por byte.
/// Os argumentos `upper_left` e `lower_right` especificam
/// pontos no plano complexo correspondente aos cantos
/// superior esquerdo e inferior direito do buffer de pixels.
fn render(pixels: &mut [u8],
bounds: (usize, usize),
upper_left: Complex<f64>,
lower_right: Complex<f64>)
{
assert!(pixels.len() == bounds.0 * bounds.1);
Ok(())
}
A operação dessa função é bastante direta: ela abre um arquivo e
tenta gravar a imagem nele. Passamos ao codificador os dados de
pixel reais de pixels, e sua largura e altura de bounds, e então um
argumento final que diz como interpretar os bytes em pixels: o valor
de ColorType::Gray(8) indica que cada byte é um valor de escala de cinza
de oito bits.
Isso é simples e direto. O que é interessante sobre essa função é
como ela lida quando algo dá errado. Se encontrarmos um erro,
precisamos reportá-lo ao nosso chamador. Como mencionamos
antes, funções que podem falhar no Rust devem retornar um valor
Result, que é Ok(s) em caso de sucesso, em que s é o valor de sucesso,
ou Err(e) em caso de falha, em que e é um código de erro. Então,
quais são os tipos de sucesso e erro de write_image?
Quando tudo corre bem, nossa função write_image não tem valor útil
para retornar; ela gravou tudo interessante no arquivo. Portanto, seu
tipo de sucesso é o tipo unit(), assim chamado porque tem apenas
um valor, também escrito (). O tipo unit é semelhante a void em C e
C++.
Quando ocorre um erro, é porque ou File::create não foi capaz de criar
o arquivo ou encoder.encode não foi capaz de gravar a imagem nele; a
operação de E/S retornou um código de erro. O tipo de retorno de
File::create é Result<std::fs::File, std::io::Error>, enquanto o de encoder.encode é
Result<(), std::io::Error>, então ambos compartilham o mesmo tipo de
erro, std::io::Error. Faz sentido nossa função write_image fazer o mesmo.
Em ambos os casos, a falha deve resultar em um retorno imediato,
repassando o valor std::io::Error descrevendo o que está errado.
Então, para lidar corretamente com o resultado File::create, precisamos
fazer o valor de retorno corresponder com a instrução match, da
seguinte forma:
let output = match File::create(filename) {
Ok(f) => f,
Err(e) => {
return Err(e);
}
};
Em caso de sucesso, faça com que output seja o File dentro no valor
Ok. Em caso de falha, repasse o erro ao nosso próprio chamador.
2
if args.len() != 5 {
eprintln!("Usage: {} FILE PIXELS UPPERLEFT LOWERRIGHT",
args[0]);
eprintln!("Example: {} mandel.png 1000x750 -1.20,0.35 -1,0.20",
args[0]);
std::process::exit(1);
}
{
let bands: Vec<&mut [u8]> =
pixels.chunks_mut(rows_per_band * bounds.0).collect();
crossbeam::scope(|spawner| {
for (i, band) in bands.into_iter().enumerate() {
let top = rows_per_band * i;
let height = band.len() / bounds.0;
let band_bounds = (bounds.0, height);
let band_upper_left =
pixel_to_point(bounds, (0, top), upper_left, lower_right);
let band_lower_right =
pixel_to_point(bounds, (bounds.0, top + height),
upper_left, lower_right);
spawner.spawn(move |_| {
render(band, band_bounds, band_upper_left, band_lower_right);
});
}
}).unwrap();
}
Dividindo isso da maneira usual:
let threads = 8;
let rows_per_band = bounds.1 / threads + 1;
[dependencies]
num = "0.4"
image = "0.13"
crossbeam = "0.8"
Com isso pronto, podemos construir e executar o programa:
$ cargo build --release
Updating crates.io index
Compiling crossbeam-queue v0.3.2
Compiling crossbeam v0.8.1
Compiling mandelbrot v0.1.0 ($RUSTBOOK/mandelbrot)
Finished release [optimized] target(s) in #.## secs
$ time target/release/mandelbrot mandel.png 4000x3000 -1.20,0.35 -1,0.20
real 0m1.436s
user 0m4.922s
sys 0m0.011s
Aqui, utilizamos time novamente para ver quanto tempo o programa
demorou para ser executado; observe que, embora ainda
gastássemos quase cinco segundos de tempo do processador, o
tempo real decorrido foi de apenas cerca de 1,5 segundo. Você pode
verificar se uma parte desse tempo é gasta gravando o arquivo de
imagem comentando o código que faz isso e medindo novamente.
No laptop em que esse código foi testado, a versão concorrente
reduz o tempo de cálculo de Mandelbrot propriamente dito por um
fator de quase quatro. Mostraremos como melhorar
substancialmente isso no Capítulo 19.
Como antes, esse programa terá criado um arquivo chamado
mandel.png. Com essa versão mais rápida, você pode explorar mais
facilmente o conjunto de Mandelbrot alterando os argumentos da
linha de comando ao seu gosto.
A segurança é invisível
No final, o programa paralelo com o qual terminamos não é
substancialmente diferente do que poderíamos escrever em qualquer
outra linguagem: distribuímos partes do buffer de pixels entre os
processadores, deixamos cada um trabalhar em sua parte
separadamente e, quando terminamos tudo, apresentamos o
resultado. Então, o que há de tão especial no suporte à concorrência
do Rust?
O que não mostramos aqui são todos os programas Rust que não
podemos escrever. O código que examinamos neste capítulo
particiona o buffer entre os threads corretamente, mas há muitas
pequenas variações nesse código que não (e, portanto, introduzem
corridas de dados); nenhuma dessas variações passará nas
verificações estáticas do compilador Rust. Um compilador C ou C++
o ajudará alegremente a explorar o vasto espaço de programas com
corridas de dados sutis; o Rust avisa, desde o início, quando algo
pode dar errado.
Nos capítulos 4 e 5, descreveremos as regras do Rust para
segurança de memória. O Capítulo 19 explica como essas regras
também garantem a higiene de concorrência adequada.
[dependencies]
text-colorizer = "1"
regex = "1"
Os crates do Rust que atingiram a versão 1.0, como estes, seguem as
regras de “versionamento semântico”: até que o número da versão
principal 1 mude, as versões mais recentes devem sempre ser
extensões compatíveis de seus predecessores. Portanto, se
testarmos nosso programa em relação à versão 1.2 de algum crate,
ainda deve funcionar com versões 1.3, 1.4, e assim por diante; mas a
versão 2.0 poderia introduzir mudanças incompatíveis. Quando
simplesmente solicitamos a versão "1" de uma crate em um arquivo
Cargo.toml, o Cargo utilizará a versão mais recente disponível do
crate antes de 2.0.
fn print_usage() {
eprintln!("{} - change occurrences of one string into another",
"quickreplace".green());
eprintln!("Usage: quickreplace <target> <replacement> <INPUT> <OUTPUT>");
}
Apenas adicionar .green() ao final de uma string literal produz uma
string envolvida pelos códigos de escape ANSI apropriados para
exibir como verde em um emulador de terminal. Essa string é então
interpolada no restante da mensagem antes de ser impressa.
Agora podemos coletar e processar os argumentos do programa:
use std::env;
fn parse_args() -> Arguments {
let args: Vec<String> = env::args().skip(1).collect();
if args.len() != 4 {
print_usage();
eprintln!("{} wrong number of arguments: expected 4, got {}.",
"Error:".red().bold(), args.len());
std::process::exit(1);
}
Arguments {
target: args[0].clone(),
replacement: args[1].clone(),
filename: args[2].clone(),
output: args[3].clone()
}
}
Para obter os argumentos que o usuário inseriu, utilizamos o mesmo
iterador args utilizado nos exemplos anteriores. .skip(1) ignora o
primeiro valor do iterador (o nome do programa que está sendo
executado) para que o resultado tenha apenas os argumentos da
linha de comando.
O método collect() produz um Vec de argumentos. Em seguida,
verificamos se o número correto está presente e, caso contrário,
imprimimos uma mensagem e saímos com um código de erro. Além
disso, colorimos novamente parte da mensagem e utilizamos .bold()
para colocar o texto em negrito. Se o número certo de argumentos
estiver presente, nós os colocamos em um struct Arguments e o
retornamos.
Então vamos acrescentar uma função main que apenas chama
parse_args e imprime a saída:
fn main() {
let args = parse_args();
println!("{:?}", args);
}
Neste ponto, podemos executar o programa e ver que ele apresenta
a mensagem de erro correta:
$ cargo run
Updating crates.io index
Compiling libc v0.2.82
Compiling lazy_static v1.4.0
Compiling memchr v2.3.4
Compiling regex-syntax v0.6.22
Compiling thread_local v1.1.0
Compiling aho-corasick v0.7.15
Compiling atty v0.2.14
Compiling text-colorizer v1.0.0
Compiling regex v1.4.3
Compiling quickreplace v0.1.0 (/home/jimb/quickreplace)
Finished dev [unoptimized + debuginfo] target(s) in 6.98s
Running `target/debug/quickreplace`
quickreplace - change occurrences of one string into another
Usage: quickreplace <target> <replacement> <INPUT> <OUTPUT>
Error: wrong number of arguments: expected 4, got 0
Se você der alguns argumentos ao programa, ele imprimirá uma
representação do struct Arguments:
$ cargo run "find" "replace" file output
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/quickreplace find replace file output`
Arguments { target: "find", replacement: "replace", filename: "file", output: "output" }
Este é um bom começo! Os argumentos são escolhidos
corretamente e colocados nas partes corretas do struct Arguments.
Localizar e substituir
O toque final para este programa é implementar sua funcionalidade
real: localizar e substituir. Para isso, utilizaremos o crate regex, que
compila e executa expressões regulares. Ele fornece uma estrutura
chamada Regex, que representa uma expressão regular compilada.
Regex tem um método replace_all, que faz exatamente o que diz:
procura em uma string todas as correspondências da expressão
regular e substitui cada uma por uma determinada string substituta.
Podemos extrair essa lógica em uma função:
use regex::Regex;
fn replace(target: &str, replacement: &str, text: &str)
-> Result<String, regex::Error>
{
let regex = Regex::new(target)?;
Ok(regex.replace_all(text, replacement).to_string())
}
Observe o tipo de retorno dessa função. Assim como as funções de
biblioteca padrão que utilizamos anteriormente, replace retorna um
Result, desta vez com um tipo de erro fornecido pelo crate regex.
Regex::new compila o regex fornecido pelo usuário e pode falhar se
receber uma string inválida. Como no programa de Mandelbrot,
utilizamos ? para encurtar o processo caso Regex::new falhe, mas
nesse caso a função retorna um tipo de erro específico do crate regex.
Depois que o regex é compilado, seu método replace_all substitui
quaisquer correspondências em text pela string replacement fornecida.
Se replace_all encontrar correspondências, ele retornará uma nova
String com essas correspondências substituídas pelo texto que lhe
fornecemos. Por outro lado, replace_all retorna um ponteiro para o
texto original, evitando alocação de memória e cópia desnecessárias.
Nesse caso, porém, sempre queremos uma cópia independente,
então utilizamos o método to_string para obter uma String em ambos os
casos e retornar essa string envolvida em Result::Ok, como nas outras
funções.
Agora, é hora de incorporar a nova função em nosso código main:
fn main() {
let args = parse_args();
$ cat test-modified.txt
Hello, Rust
E, naturalmente, o tratamento de erros também está em vigor,
relatando os erros de maneira elegante ao usuário:
$ cargo run "[[a-z]" "0" test.txt test-modified.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/quickreplace '[[a-z]' 0 test.txt test-modified.txt`
Error: failed to replace text: Syntax(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~
regex parse error:
[[a-z]
^
error: unclosed character class
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~
)
É claro, há muitos recursos faltando nessa demonstração simples,
mas os fundamentos foram apresentados. Você viu como ler e
gravar arquivos, propagar e exibir erros e colorir a saída para
melhorar a experiência do usuário no terminal.
Nos próximos capítulos serão exploradas técnicas mais avançadas
para desenvolvimento de aplicativos, desde coleções de dados e
programação funcional com iteradores até técnicas de programação
assíncrona para concorrência extremamente eficiente, mas primeiro
você precisará da base sólida do próximo capítulo sobre os tipos de
dados fundamentais do Rust.
1 N.R.: Boilerplates são seções de código que fornecem uma estrutura básica (esqueleto),
que pode ser utilizada repetidamente com pouca ou nenhuma alteração, sendo um ponto
de partida, a partir do qual você faz o resto.
2 N.R.: Em caso de sucesso, o valor da variável output será igual a f passado no valor
retornado por File::create, ou seja, Ok(f) quando o arquivo é aberto corretamente. Em
caso de falha, (File::create retorna Err(e)) retorna o erro para a função que chamou
write_file.
3 A crate num_cpus fornece uma função que retorna o número de CPUs disponíveis no
sistema atual.
3
capítulo
Tipos fundamentais
Tipos inteiros
Os tipos inteiros sem sinal do Rust utilizam um intervalo completo
para representar valores positivos e zero (Tabela 3.3).
Tabela 3.3: Tipos inteiros sem sinal do Rust
Tipo Intervalo
u8 0 a 28–1 (0 a 255)
Tipo Intervalo
u16 0 a 216−1 (0 a 65.535)
u32 0 a 232−1 (0 a 4.294.967.295)
u64 0 a 264−1 (0 a 18.446.744.073.709.551.615 ou 18
quintilhões)
u12 0 a 2128−1 (0 a aproximadamente de 3,4✕1038)
8
usiz 0 a 232−1 ou 264−1
e
O Rust utiliza o tipo u8 para valores de byte. Por exemplo, ler dados
de um arquivo binário ou soquete gera um fluxo de valores u8.
Ao contrário do C e C++, o Rust trata caracteres como distintos dos
tipos numéricos: um char não é um u8, nem é um u32 (embora tenha
32 bits de comprimento). Descrevemos o tipo char do Rust em
“Caracteres”, na página 84.
Os tipos usize e isize são análogos a size_t e ptrdiff_t em C e C++. Sua
precisão corresponde ao tamanho do espaço de endereço na
máquina de destino: eles têm 32 bits em arquiteturas de 32 bits e
64 bits em arquiteturas de 64 bits. O Rust requer que os índices do
array sejam valores usize. Valores que representam os tamanhos de
arrays, vetores ou contagens do número de elementos em alguma
estrutura de dados geralmente também têm o tipo usize.
Literais de inteiro no Rust podem receber um sufixo indicando seu
tipo: 42u8 é um valor u8 e 1729isize é um isize. Se um literal de inteiro
não possui um sufixo de tipo, o Rust adia a determinação do tipo até
encontrar o valor que está sendo utilizado de uma maneira que o
resolva: armazenado em uma variável de um tipo específico,
passado para uma função que espera um tipo específico, comparado
com outro valor de um tipo específico, ou algo assim. No final, se
múltiplos tipos puderem funcionar, o Rust assume o padrão de i32 se
isso estiver entre as possibilidades. Caso contrário, o Rust relata a
ambiguidade como um erro.
Os prefixos 0x, 0o e 0b designam literais hexadecimais, octais e
binários.
Para tornar os números longos mais legíveis, você pode inserir
sublinhados entre os dígitos. Por exemplo, você pode escrever o
maior valor u32 como 4_294_967_295. A colocação exata dos
sublinhados não é significativa, portanto você pode dividir números
hexadecimais ou binários em grupos de quatro dígitos em vez de
três, como em 0xffff_ffff, ou definir o sufixo de tipo dos dígitos, como
em 127_u8. Alguns exemplos de literais de inteiro são ilustrados na
Tabela 3.5.
Tabela 3.5: Exemplos de literais de inteiro
Valor
Literal Tipo
decimal
116i8 i8 116
0xcafeu32 u32 51966
0b0010_101 Inferid 42
0 o
0o106 Inferid 70
o
Tipo bool
Tipo booleano do Rust, bool, tem os dois valores usuais para esses
tipos, true e false. Operadores de comparação como == e < produzem
resultados bool: o valor de 2 < 5 é true.
Muitas linguagens são indulgentes com o uso de valores de outros
tipos em contextos que exigem um valor booleano: C e C++
convertem implicitamente caracteres, inteiros, números de ponto
flutuante e ponteiros em valores booleanos, para que possam ser
utilizados diretamente como a condição em uma declaração if ou
while.O Python permite strings, listas, dicionários e até conjuntos em
contextos booleanos, tratando esses valores como verdadeiros se
não estiverem vazios. O Rust, porém, é muito rígido: estruturas de
controle como if e while exigem que suas condições sejam expressões
bool, assim como os operadores lógicos que curto-circuitam && e ||.
2
Caracteres
O tipo de caractere char do Rust representa um único caractere
Unicode, como um valor de 32 bits.
O Rust utiliza o tipo char para caracteres únicos isoladamente, mas
utiliza a codificação UTF-8 para strings e fluxos (streams) de texto.
Então, uma String representa o texto como uma sequência de bytes
UTF-8, não como um array de caracteres.
Literais de caractere são caracteres entre aspas simples, como '8' ou
'!'. Você pode utilizar toda a extensão do Unicode: ' 錆 ' é um char
literal representando o kanji japonês para sabi (rust).
Assim como os literais de byte, as barras invertidas são necessárias
para alguns caracteres (Tabela 3.10).
Tabela 3.10: Caracteres que exigem escapes de barra invertida
Literal de caractere
Caractere
Rust
Aspas simples, ' '\''
Barra invertida, '\\'
\
Literal de caractere
Caractere
Rust
Nova linha '\n'
Retorno de '\r'
carro
Tab '\t'
Tuplas
Uma tupla é um par, ou tripla, quádrupla, quíntupla etc. (portanto,
n-tupla, enupla, ou tupla), dos valores de tipos variados. Você pode
escrever uma tupla como uma sequência de elementos, separados
por vírgulas e entre parênteses. Por exemplo, ("Brazil", 1985) é uma
tupla cujo primeiro elemento é uma string alocada estaticamente e
cujo segundo é um inteiro; seu tipo é (&str, i32). Dado um valor de
tupla t, você pode acessar seus elementos como t.0, t.1 e assim por
diante.
Até certo ponto, as tuplas se parecem com arrays: ambos os tipos
representam uma sequência ordenada de valores. Muitas linguagens
de programação fundem ou combinam os dois conceitos, mas, no
Rust, eles são completamente distintos. Por um lado, cada elemento
de uma tupla pode ter um tipo diferente, enquanto os elementos de
um array devem ser todos do mesmo tipo. Além disso, tuplas
permitem apenas constantes como índices, como t.4. Você não pode
escrever t.i ou t[i] para obter o i-ésimo elemento.
O código Rust geralmente utiliza tipos de tupla para retornar
múltiplos valores de uma função. Por exemplo, o método split_at em
fatias de string, que divide uma string em duas metades e retorna
ambas, é declarado assim:
fn split_at(&self, mid: usize) -> (&str, &str);
O tipo de retorno (&str, &str) é uma tupla de duas fatias de string. Você
pode utilizar a sintaxe de correspondência de padrões para atribuir
cada elemento do valor de retorno a uma variável diferente:
let text = "I see the eigenvalue in thine eye";
let (head, tail) = text.split_at(21);
assert_eq!(head, "I see the eigenvalue ");
assert_eq!(tail, "in thine eye");
Isso é mais legível do que o equivalente:
let text = "I see the eigenvalue in thine eye";
let temp = text.split_at(21);
let head = temp.0;
let tail = temp.1;
assert_eq!(head, "I see the eigenvalue ");
assert_eq!(tail, "in thine eye");
Também veremos tuplas utilizadas como uma espécie de struct sem
drama. Por exemplo, no programa Mandelbrot no Capítulo 2,
precisamos passar a largura e a altura da imagem para as funções
que plotam e a escrevem em disco. Poderíamos declarar um struct
com os membros width e height, mas essa é uma notação bastante
trabalhosa para algo tão óbvio, assim simplesmente utilizamos uma
tupla:
/// Grava o buffer `pixels`, cujas dimensões são dadas por `bounds`,
/// no arquivo chamado `filename`.
fn write_image(filename: &str, pixels: &[u8], bounds: (usize, usize))
-> Result<(), std::io::Error>
{ ... }
O tipo do parâmetro bounds é (usize, usize), uma tupla de dois valores
usize. Poderíamos muito bem escrever explicitamente os parâmetros
width e separados, e, de qualquer maneira, o código de máquina
height
seria quase o mesmo. É uma questão de clareza. Pensamos no
tamanho como um valor, não dois, e utilizar uma tupla permite
escrever o que temos em mente.
O outro tipo de tupla comumente utilizado é a tupla zero (). Isso é
tradicionalmente chamado de tipo unidade porque tem apenas um
valor, também escrito (). O Rust utiliza o tipo unidade onde não há
valor significativo a executar, mas mesmo assim o contexto requer
alguma espécie de tipo.
Por exemplo, uma função que não retorna nenhum valor tem um
tipo de retorno de (). A função std::mem::swap da biblioteca padrão não
tem um valor de retorno significativo; apenas troca os valores de
seus dois argumentos. A declaração para std::mem::swap é lida como:
fn swap<T>(x: &mut T, y: &mut T);
O <T> significa que swap é genérico: você pode usá-lo em referências
a valores de qualquer tipo T. Mas a assinatura omite completamente
o tipo de retorno de swap, que é uma abreviação para retornar o tipo
de unidade:
fn swap<T>(x: &mut T, y: &mut T) -> ();
Da mesma forma, o write_image de exemplo que mencionamos antes
tem um tipo de retorno de Result<(), std::io::Error>, o que significa que a
função retorna um valor std::io::Error se algo der errado, mas não
retorna nenhum valor em caso de sucesso.
Se quiser, você pode incluir uma vírgula após o último elemento de
uma tupla: os tipos (&str, i32,) e (&str, i32) são equivalentes, assim como
as expressões ("Brazil", 1985,) e ("Brazil", 1985). O Rust permite
consistentemente uma vírgula extra em todos os lugares onde
vírgulas são utilizadas: argumentos de função, arrays, definições de
struct e enum etc. Isso pode parecer estranho para os leitores
humanos, mas pode facilitar a leitura das diferenças quando as
entradas são adicionadas e removidas no final de uma lista.
Para fins de consistência, existem até tuplas que contêm um único
valor. O literal ("lonely hearts",) é uma tupla contendo uma única string;
seu tipo é (&str,). Aqui, a vírgula após o valor é necessária para
distinguir a tupla singleton de uma expressão entre parênteses
simples.
Tipos de ponteiro
O Rust tem vários tipos que representam endereços de memória.
Essa é uma grande diferença entre o Rust e a maioria das
linguagens com coleta de lixo (garbage collection). No Java, se
class Rectangle contém um campo Vector2D upperLeft;, então upperLeft é uma
referência a outro objeto Vector2D criado separadamente. Objetos
nunca contêm fisicamente outros objetos em Java.
O Rust é diferente. A linguagem é projetada para ajudar a manter o
mínimo de alocações. Valores agrupados por padrão. O valor ((0, 0),
(1440, 900)) é armazenado como quatro inteiros adjacentes. Se você o
armazenar em uma variável local, você terá uma variável local com
quatro inteiros de largura. Nada é alocado no heap.
Isso é ótimo para eficiência de memória, mas, como consequência,
quando um programa Rust precisa de valores para apontar para
outros valores, ele deve utilizar tipos de ponteiro explicitamente. A
boa notícia é que os tipos de ponteiro utilizados no Rust seguro4
(safe) são restritos para eliminar o comportamento indefinido,
portanto os ponteiros são muito mais fáceis de utilizar corretamente
no Rust do que no C++.
Discutiremos três tipos de ponteiro aqui: referências, boxes e
ponteiros não seguros (unsafe).
Referências
Um valor do tipo &String (pronuncia-se “ref String”) é uma referência
a um valor String, um &i32 é uma referência a um i32 e assim por
diante.
É mais fácil começar pensando em referências como o tipo de
ponteiro básico do Rust. Em tempo de execução, uma referência a
um i32 é uma única palavra de máquina contendo o endereço do i32,
que pode estar no stack (pilha) ou no heap. A expressão &x produz
uma referência a x; na terminologia do Rust, dizemos que empresta
uma referência a x. Dada uma referência r, a expressão *r referencia
o valor para o qual r aponta. Estes são muito parecidos com os
operadores & e * em C e C++. E como um ponteiro C, uma
referência não libera automaticamente nenhum recurso quando sai
do escopo.
Ao contrário dos ponteiros C, porém, as referências do Rust nunca
são nulas: simplesmente não há como produzir uma referência nula
em Rust seguro. E, diferentemente do C, o Rust rastreia a posse e o
tempo de vida dos valores, portanto erros como ponteiros perdidos,
liberações duplas e invalidação de ponteiro são descartados em
tempo de compilação.
As referências do Rust têm duas versões:
&T
Uma referência imutável e compartilhada. Você pode ter muitas
referências compartilhadas a um determinado valor em um dado
momento, mas elas são somente leitura: modificar o valor para o
qual elas apontam é proibido, como acontece com const T* em C.
&mut T
Uma referência mutável e exclusiva. Você pode ler e modificar o
valor para o qual aponta, como com um T* em C. Mas, enquanto a
referência existir, você não pode ter nenhuma outra referência de
qualquer tipo a esse valor. Na verdade, a única maneira de acessar
o valor é por meio da referência mutável.
O Rust utiliza essa dicotomia entre referências compartilhadas e
mutáveis para impor uma regra de “único escritor ou vários leitores”:
você pode ler e escrever/gravar o valor ou pode ser compartilhado
por qualquer número de leitores, mas nunca os dois ao mesmo
tempo. Essa separação, imposta por verificações em tempo de
compilação, é fundamental para as garantias de segurança do Rust.
O Capítulo 5 explica as regras do Rust para uso seguro de
referências.
Boxes
A maneira mais simples de alocar um valor no heap é utilizar
Box::new:
let t = (12, "eggs");
let b = Box::new(t); // aloca uma tupla no heap
O tipo de t é (i32, &str), então o tipo de b é Box<(i32, &str)>. A chamada
para Box::new aloca memória suficiente para conter a tupla no heap.
Quando b sai do escopo, a memória é liberada imediatamente, a
menos que b tenha sido movido – sendo retornado, por exemplo. Os
movimentos são essenciais para a maneira como o Rust lida com os
valores alocados em heap; explicamos tudo isso em detalhes no
Capítulo 4.
Ponteiros brutos
O Rust também tem os tipos de ponteiro bruto *mut T e *const T. Na
verdade, ponteiros brutos são como ponteiros em C++. Utilizar um
ponteiro bruto não é seguro, porque o Rust não faz nenhum esforço
para rastrear aquilo para o que ele aponta. Por exemplo, os
ponteiros brutos podem ser nulos ou podem apontar para a memória
que foi liberada ou que agora contém um valor de um tipo diferente.
Todos os erros clássicos de ponteiro do C++ são oferecidos para sua
diversão.
Entretanto, você só pode desreferenciar ponteiros brutos dentro de
um bloco unsafe (inseguro). Um bloco unsafe é o mecanismo de
inclusão do Rust para recursos avançados de linguagem cuja
segurança depende de você. Se seu código não tiver blocos unsafe
(ou se aqueles que eles têm estão escritos corretamente), então as
garantias de segurança que enfatizamos ao longo deste livro
continuam válidas. Para mais detalhes, ver o Capítulo 22.
Arrays
Existem várias maneiras de escrever valores de array. A mais simples
é escrever uma série de valores entre colchetes:
let lazy_caterer: [u32; 6] = [1, 2, 4, 7, 11, 16];
let taxonomy = ["Animalia", "Arthropoda", "Insecta"];
assert_eq!(lazy_caterer[3], 7);
assert_eq!(taxonomy.len(), 3);
Para o caso comum de um longo array preenchido com algum valor,
você pode escrever [V; N], em que V é o valor que cada elemento
deve ter e N é o comprimento. Por exemplo, [true; 10000] é um array
de 10.000 elementos bool, todos definidos como true:
let mut sieve = [true; 10000];
for i in 2..100 {
if sieve[i] {
let mut j = i * i;
while j < 10000 {
sieve[j] = false;
j += i;
}
}
}
assert!(sieve[211]);
assert!(!sieve[9876]);
Você verá essa sintaxe utilizada para buffers de tamanho fixo:
[0u8; 1024] pode ser um buffer de um Kbyte, preenchido com zeros. O
Rust não possui notação para um array não inicializado. (Em geral, o
Rust assegura que o código nunca possa acessar qualquer tipo de
valor não inicializado.)
O comprimento de um array é parte de seu tipo e é fixo em tempo
de compilação. Se n é uma variável, você não pode escrever [true; n]
para obter um array de n elementos. Quando você precisa de um
array cujo comprimento varia em tempo de execução (e você
geralmente precisa), utilize um vetor.
Os métodos úteis que você quer ver em arrays – iterar por
elementos, pesquisar, ordenar, preencher, filtrar e assim por diante –
são todos fornecidos como métodos em fatias, não em arrays. Mas o
Rust converte implicitamente uma referência a um array em uma
fatia ao pesquisar métodos, para que você possa chamar qualquer
método de fatia sobre um array diretamente:
let mut chaos = [3, 5, 4, 1, 2];
chaos.sort();
assert_eq!(chaos, [1, 2, 3, 4, 5]);
Aqui o método sort é realmente definido em fatias, mas como ele
utiliza o operando por referência, o Rust produz implicitamente uma
fatia &mut [i32] que referencia todo o array e passa isso para sort
trabalhar. Na verdade, o método len que mencionamos anteriormente
também é um método de fatia. Discutimos fatias em mais detalhes
em “Fatias”, na página 96.
Vetores
Um vetor Vec<T> é um array redimensionável de elementos do tipo T,
alocado no heap.
Existem várias maneiras de criar vetores. A mais simples é utilizar a
macro vec!, que fornece uma sintaxe para vetores que se parece
muito com um literal de array:
let mut primes = vec![2, 3, 5, 7];
assert_eq!(primes.iter().product::<i32>(), 210);
Mas é claro, isso é um vetor, não um array, assim podemos adicionar
elementos a ele dinamicamente:
primes.push(11);
primes.push(13);
assert_eq!(primes.iter().product::<i32>(), 30030);
Você também pode construir um vetor repetindo um determinado
valor um certo número de vezes, novamente utilizando uma sintaxe
que imita literais de array:
fn new_pixel_buffer(rows: usize, cols: usize) -> Vec<u8> {
vec![0; rows * cols]
}
A macro vec! é equivalente a chamar Vec::new para criar um novo vetor
vazio e, em seguida, inserir os elementos nele, o que é outro idioma:
let mut pal = Vec::new();
pal.push("step");
pal.push("on");
pal.push("no");
pal.push("pets");
assert_eq!(pal, vec!["step", "on", "no", "pets"]);
Outra possibilidade é construir um vetor a partir dos valores gerados
por um iterador:
let v: Vec<i32> = (0..5).collect();
assert_eq!(v, [0, 1, 2, 3, 4]);
Muitas vezes, você precisará fornecer o tipo ao utilizar collect (como
fizemos aqui), porque pode construir muitos tipos diferentes de
coleções, não apenas vetores. Especificando o tipo de v, tornamos
inequívoco o tipo de coleção que queremos.
Como com arrays, você pode utilizar métodos de fatia em vetores:
// Um palíndromo!
let mut palindrome = vec!["a man", "a plan", "a canal", "panama"];
palindrome.reverse();
// Razoável, mas decepcionante:
assert_eq!(palindrome, vec!["panama", "a canal", "a plan", "a man"]);
Aqui o método reverse é na verdade definido em fatias, mas a
chamada implicitamente empresta uma fatia &mut [&str] do vetor e
invoca reverse nisso.
Vec é um tipo essencial para o Rust – é utilizado em quase todos os
lugares onde uma lista de tamanho dinâmico é necessária –, assim
existem muitos outros métodos que constroem novos vetores ou
estendem os existentes. Vamos abordá-los no Capítulo 16.
Um Vec<T> consiste em três valores: um ponteiro para o buffer
alocado no heap para os elementos, que é criado e possuído Vec<T>;
o número de elementos que o buffer tem capacidade de armazenar;
e o número que realmente contém agora (em outras palavras, seu
comprimento). Quando o buffer alcançou sua capacidade, adicionar
outro elemento ao vetor envolve alocar um buffer maior, copiar o
conteúdo atual para ele, atualizar o ponteiro do vetor e a capacidade
para descrever o novo buffer e, finalmente, liberar o antigo.
Se você souber antecipadamente o número de elementos de que um
vetor precisará, em vez de Vec::new você pode chamar Vec::with_capacity
para criar um vetor com um buffer grande o suficiente para conter
todos eles, desde o início; então, você pode adicionar os elementos
ao vetor, um de cada vez, sem causar nenhuma realocação. A macro
vec! utiliza um truque como esse, pois sabe quantos elementos o
vetor final terá. Observe que isso apenas estabelece o tamanho
inicial do vetor; se você exceder a estimativa, o vetor simplesmente
aumentará o armazenamento como de costume.
Muitas funções de biblioteca buscam a oportunidade de utilizar
Vec::with_capacity em vez de Vec::new. Por exemplo, no exemplo de collect,
o iterador 0..5 sabe de antemão que vai gerar cinco valores e a
função collect aproveita isso para pré-alocar o vetor que retorna com
a capacidade correta. Veremos como isso funciona no Capítulo 15.
Assim como o método len de um vetor retorna o número de
elementos que ele contém agora, o método capacity retorna o número
de elementos que poderia conter sem realocação:
let mut v = Vec::with_capacity(2);
assert_eq!(v.len(), 0);
assert_eq!(v.capacity(), 2);
v.push(1);
v.push(2);
assert_eq!(v.len(), 2);
assert_eq!(v.capacity(), 2);
v.push(3);
assert_eq!(v.len(), 3);
// Normalmente imprime "capacity is now 4":
println!("capacity is now {}", v.capacity());
Não é garantido que a capacidade impressa no final seja
exatamente 4, mas será pelo menos 3, pois o vetor contém três
valores.
Você pode inserir e remover elementos onde quiser em um vetor,
embora essas operações desbloqueiem todos os elementos após a
posição afetada para a frente ou para trás; portanto, podem ser
lentas se o vetor for longo:
let mut v = vec![10, 20, 30, 40, 50];
// Faça o elemento no índice 3 ser 35
v.insert(3, 35);
assert_eq!(v, [10, 20, 30, 35, 40, 50]);
// Remova o elemento no índice 1
v.remove(1);
assert_eq!(v, [10, 30, 35, 40, 50]);
Você pode utilizar o método pop para remover o último elemento e
retorná-lo. Mais precisamente, extrair um valor de um Vec<T> retorna
um Option<T>: None se o vetor já estava vazio, ou Some(v) se o último
elemento era v:
let mut v = vec!["Snow Puff", "Glass Gem"];
assert_eq!(v.pop(), Some("Glass Gem"));
assert_eq!(v.pop(), Some("Snow Puff"));
assert_eq!(v.pop(), None);
Você pode utilizar um loop for para iterar por um vetor:
// Obtenha nossos argumentos de linha de comando como um vetor de Strings
let languages: Vec<String> = std::env::args().skip(1).collect();
for l in languages {
println!("{}: {}", l,
if l.len() % 2 == 0 {
"functional"
} else {
"imperative"
});
}
Executar esse programa com uma lista de linguagens de
programação é esclarecedor:
$ cargo run Lisp Scheme C C++ Fortran
Compiling proglangs v0.1.0 (/home/jimb/rust/proglangs)
Finished dev [unoptimized + debuginfo] target(s) in 0.36s
Running `target/debug/proglangs Lisp Scheme C C++ Fortran`
Lisp: functional
Scheme: functional
C: imperative
C++: imperative
Fortran: imperative
$
Por fim, uma definição satisfatória para o termo linguagem funcional.
Apesar de seu papel fundamental, Vec é um tipo comum definido no
Rust, não integrado na linguagem. Abordaremos as técnicas
necessárias para implementar esses tipos no Capítulo 22.
Fatias
Uma fatia (slice), escrita [T] sem especificar o comprimento, é uma
região de um array ou vetor. Como uma fatia pode ter qualquer
comprimento, as fatias não podem ser armazenadas diretamente em
variáveis ou passadas como argumentos de função. As fatias são
sempre passadas por referência.
Uma referência a uma fatia é uma ponteiro gordo (fat pointer): um
valor de duas palavras que compreende um ponteiro para o primeiro
elemento da fatia e o número de elementos na fatia.
Suponha que você execute o seguinte código:
let v: Vec<f64> = vec![0.0, 0.707, 1.0, 0.707];
let a: [f64; 4] = [0.0, -0.707, -1.0, -0.707];
let sv: &[f64] = &v;
let sa: &[f64] = &a;
Nas duas últimas linhas, o Rust converte automaticamente a
referência &Vec<f64> e a referência &[f64; 4] em referências de fatia
que apontam diretamente para os dados.
No final, a memória se parece com a Figura 3.2.
Tipos de string
Os programadores familiarizados com C++ lembrarão que existem
dois tipos de string na linguagem. Os literais de string têm o tipo de
ponteiro const char *. A biblioteca padrão também oferece uma classe,
std::string, para criar strings dinamicamente em tempo de execução.
O Rust tem um design semelhante. Nesta seção, mostraremos todas
as maneiras de escrever literais de strings e, em seguida,
apresentaremos os dois tipos string do Rust. Fornecemos mais
detalhes sobre strings e manipulação de texto no Capítulo 17.
Literais de string
Literais de string são colocados entre aspas duplas. Eles utilizam as
mesmas sequências de escape de barra invertida como literais char:
let speech = "\"Ouch!\" said the well.\n";
Em literais de string, ao contrário de literais char, aspas simples não
precisam de escape de barra invertida, e aspas duplas precisam.
Uma string pode abranger várias linhas:
println!("In the room the women come and go,
Singing of Mount Abora");
O caractere de nova linha nesse literal de string é incluído na string
e, portanto, na saída. Assim como os espaços no início da segunda
linha.
Se uma linha de uma string terminar com uma barra invertida, o
caractere de nova linha e o espaço em branco inicial na próxima
linha serão dropados:
println!("It was a bright, cold day in April, and \
there were four of us—\
more or less.");
Isso imprime uma única linha de texto. A string contém um espaço
único entre “and” e “there” porque há um espaço antes da barra
invertida no programa, e nenhum espaço entre o travessão e
“more”.
Em alguns casos, a necessidade de dobrar cada barra invertida em
uma string é um incômodo. (Os exemplos clássicos são expressões
regulares e caminhos (paths) do Windows.) Para esses casos, o Rust
oferece strings brutas. Uma string bruta é marcada com a letra
minúscula r. Todas as barras invertidas e caracteres de espaço em
branco dentro de uma string bruta são incluídos textualmente na
string. Nenhuma sequência de escape é reconhecida:
let default_win_install_path = r"C:\Program Files\Gorillas";
Strings de bytes
Um literal de string com o prefixo b é uma string de bytes. Essa
string é uma fatia dos valores u8 – ou seja, bytes – em vez de texto
Unicode:
let method = b"GET";
assert_eq!(method, &[b'G', b'E', b'T']);
O tipo de method é &[u8; 3]: é uma referência a um array de três bytes.
Ele não possui nenhum dos métodos de string que discutiremos a
seguir. A coisa mais parecida com uma string é a sintaxe que
utilizamos para escrevê-la.
Strings de byte podem utilizar todas as outras sintaxes de string que
mostramos: elas podem abranger várias linhas, utilizar sequências
de escape e usar barras invertidas para unir linhas. Strings de bytes
brutas começam com br".
Strings de bytes não podem conter caracteres Unicode arbitrários.
Elas devem se contentar com sequências de escape ASCII e \xHH.
Strings na memória
Strings do Rust são sequências de caracteres Unicode, mas não são
armazenadas na memória como arrays de chars. Em vez disso, são
armazenadas utilizando UTF-8, uma codificação de tamanho
variável. Cada caractere ASCII em uma string é armazenado em um
byte. Outros caracteres ocupam vários bytes.
A Figura 3.3 mostra os valores String e &str criados pelo seguinte
código:
let noodles = "noodles".to_string();
let oodles = &noodles[1..];
let poodles = "ಠ_ಠ";
Um String tem um buffer redimensionável contendo texto UTF-8. O
buffer é alocado no heap, portanto ele pode redimensionar seu
buffer conforme necessário ou solicitado. No exemplo, noodles é uma
String que possui um buffer de oito bytes, dos quais sete estão em
uso. Você pode pensar em uma String como um Vec<u8> que
garantidamente contém UTF-8 bem formado; na verdade, é assim
que String é implementada.
Um &str (pronuncia-se “stir” ou “string slice”) é uma referência a uma
string de texto UTF-8 possuída por outra pessoa: ele “pega
emprestado” o texto. No exemplo, oodles é um &str referindo-se aos
últimos seis bytes do texto pertencente a noodles, portanto representa
o texto “oodles”. Como outras referências a fatias, um &str é um
ponteiro gordo, contendo o endereço dos dados reais e seu
comprimento. Você pode pensar em um &str como sendo nada mais
do que um &[u8] que é garantido conter UTF-8 bem formado.
String
&str se parece muito com &[T]: um ponteiro gordo para alguns dados.
String é análogo a Vec<T>, conforme descrito na Tabela 3.11.
Usando strings
Strings suportam os operadores == e !=. Duas strings são iguais se
contiverem os mesmos caracteres na mesma ordem
(independentemente de apontarem para a mesma posição na
memória):
assert!("ONE".to_lowercase() == "one");
Strings também suportam os operadores de comparação <, <=, > e
>=, bem como muitos métodos e funções úteis que você pode
encontrar na documentação on-line em “str (tipo primitivo)” ou no
módulo “std::str” (ou apenas passe para o Capítulo 17). Eis alguns
exemplos:
assert!("peanut".contains("nut"));
assert_eq!("ಠ_ಠ".replace("ಠ", "■"), "■_■");
assert_eq!(" clean\n".trim(), "clean");
Além do básico
Tipos são uma parte central do Rust. Continuaremos a discutir tipos
e apresentar novos ao longo do livro. Em particular, os tipos do Rust
definidos pelo usuário fornecem à linguagem boa parte de seu
poder, porque é onde os métodos são definidos. Existem três tipos
de tipos definidos pelo usuário e vamos abordá-los em três capítulos
sucessivos: structs no Capítulo 9, enums no Capítulo 10 e traits no
Capítulo 11.
Funções e closures têm seus próprios tipos, abordados no
Capítulo 14. E os tipos que compõem a biblioteca padrão são
abordados ao longo do livro. Por exemplo, o Capítulo 16 apresenta
os tipos de coleção padrão.
Mas tudo isso terá de esperar. Antes de prosseguirmos, é hora de
abordar os conceitos que estão no centro das regras de segurança
do Rust.
1 N.R: Aqui enquadrar está sendo usado no sentido de “wraps around”, pois quando um
valor é maior do que o tipo pode representar, apenas uma parte do valor é mantida,
perdendo-se o restante, como um limite de tamanho, daí enquadramento. Em Rust,
quando isso acontece, o valor resultante é o valor do módulo do valor correto com o
máximo que pode ser presentado pelo tipo. Por exemplo, 1000 enquadrado em u8 seria
representado como 1000 % 256 (2^8) = 232.
2 N.R.: Um curto-circuito na avaliação de um operador booleano é uma otimização que não
executa o restante da expressão caso o resultado da primeira parte seja determinante.
Por exemplo, com &&, ambos os operandos têm de ser verdadeiros. Se o primeiro for
falso, o segundo nem é avaliado, pois o resultado do && será falso independentemente de
seu valor. No || o mesmo acontece quando um dos operandos é verdadeiro.
3 N.R.: Idioma falado na Índia: https://pt.wikipedia.org/wiki/L%C3%ADngua_canaresa
4 N.R.: Rust tem operações seguras (safe) e inseguras (unsafe). O programador pode
escolher ter partes inseguras (unsafe) no código, ou seja, cujo comportamento não pode
ser verificado pelo compilador Rust.
4capítulo
Posse e movimentos
Posse (ownership)
Se você leu muito código C ou C++, provavelmente já se deparou
com um comentário dizendo que uma instância de alguma classe
possui algum outro objeto para o qual ela aponta. Isso geralmente
significa que o objeto proprietário decide quando liberar o objeto
possuído: quando o proprietário é destruído, ele destrói suas posses
com ele.
Por exemplo, suponha que você escreva o seguinte código em C++:
std::string s = "frayed knot";
A string s geralmente é representada na memória como mostrado na
Figura 4.1.
Movimentos
No Rust, para a maioria dos tipos, operações como atribuir um valor
a uma variável, passá-lo para uma função ou retorná-lo de uma
função não copiam o valor: elas o movem. A origem cede a posse do
valor para o destino e torna-se não inicializada; o destino agora
controla o tempo de vida do valor. Os programas Rust constroem e
destroem estruturas complexas, um valor por vez, um movimento
por vez.
Você pode se surpreender com o fato de o Rust mudar o significado
de tais operações fundamentais; certamente a atribuição é algo que
deveria ser muito bem definido neste ponto da história. No entanto,
se você observar atentamente como as diferentes linguagens
escolheram lidar com as tarefas, verá que há uma variação
significativa de uma escola para outra. A comparação também
facilita a compreensão do significado e das consequências da
escolha do Rust.
Considere o seguinte código Python:
s = ['udon', 'ramen', 'soba']
t=s
u=s
Cada objeto Python carrega uma contagem de referência,
rastreando o número de valores que o estão referenciando
atualmente. Assim, após a atribuição de s, o estado do programa se
parece com a Figura 4.5 (observe que alguns campos foram
deixados de fora).
Figura 4.5: Como o Python representa uma lista de strings na
memória.
Desde que apenas s está apontando para a lista, a contagem de
referência da lista é 1; e como a lista é o único objeto apontando
para as strings, cada uma de suas contagens de referência também
é 1.
O que acontece quando o programa executa as atribuições para t e
u? O Python implementa a atribuição simplesmente fazendo com que
o destino aponte para o mesmo objeto que a origem e
incrementando a contagem de referência do objeto. Portanto, o
estado final do programa é algo como a Figura 4.6.
Figura 4.6: O resultado da atribuição de s tanto para t como para u
no Python.
O Python copiou o ponteiro de s para t e u e atualizou a contagem de
referência da lista para 3. A atribuição no Python é barata, mas,
como cria uma nova referência ao objeto, devemos manter
contagens de referência para saber quando podemos liberar o valor.
Agora considere o código C++ análogo:
using namespace std;
vector<string> s = { "udon", "ramen", "soba" };
vector<string> t = s;
vector<string> u = s;
O valor inicial de s se parece com a Figura 4.7 na memória.
O que acontece quando o programa atribui s para t e u? Atribuir um
std::vector produz uma cópia do vetor em C++; std::string se comporta
de forma semelhante. Portanto, quando o programa chega ao final
desse código, ele já alocou três vetores e nove strings (Figura 4.8).
Dependendo dos valores envolvidos, a atribuição em C++ pode
consumir quantidades sem controle de memória e tempo do
processador. A vantagem, porém, é que é fácil para o programa
decidir quando liberar toda essa memória: quando as variáveis saem
do escopo, tudo alocado aqui é liberado automaticamente.
for mut s in v {
s.push('!');
println!("{}", s);
}
Quando passamos o vetor para o loop diretamente, como em for ...
in v, isso move o vetor para fora de v, deixando v não inicializado. O
mecanismo interno do loop for toma posse do vetor e o disseca em
seus elementos. A cada iteração, o loop move outro elemento para a
variável s. Como agora s possui a string, podemos modificá-la no
corpo do loop antes de imprimi-la. E, como o próprio vetor não é
mais acessível para o código, nada pode observá-lo durante o loop
em algum estado parcialmente vazio.
Se você precisar mover um valor de um proprietário que o
compilador não pode rastrear, considere alterar o tipo do proprietário
para algo que possa rastrear dinamicamente se ele tem um valor ou
não. Por exemplo, eis uma variante do exemplo anterior:
struct Person { name: Option<String>, birth: i32 }
// O Rust pode inferir todos esses tipos; escritos aqui para maior clareza
let s: Rc<String> = Rc::new("shirataki".to_string());
let t: Rc<String> = s.clone();
let u: Rc<String> = s.clone();
Para qualquer tipo T, um valor Rc<T> é um ponteiro para um T
alocado no heap que teve uma contagem de referência afixada nele.
Clonar um valor Rc<T> não copia o T; em vez disso, simplesmente
cria outro ponteiro para ele e incrementa a contagem de referência.
Portanto, o código anterior produz a situação ilustrada na
Figura 4.12 na memória.
Referências a valores
Como exemplo, suponha que vamos construir uma tabela de artistas
da Renascença que pintaram assassinatos e as obras pelas quais são
conhecidos. A biblioteca padrão do Rust inclui um tipo de tabela
hash, ou tabela de dispersão, então podemos definir nosso tipo
assim:
use std::collections::HashMap;
type Table = HashMap<String, Vec<String>>;
Em outras palavras, essa é uma tabela hash que mapeia valores
String para valores Vec<String>, levando o nome de um artista a uma
lista de nomes de suas obras. Você pode iterar pelas entradas de um
HashMap com um loop for, e assim podemos escrever uma função para
imprimir uma Table:
fn show(table: Table) {
for (artist, works) in table {
println!("works by {}:", artist);
for work in works {
println!(" {}", work);
}
}
}
Construir e imprimir a tabela é simples:
fn main() {
let mut table = Table::new();
table.insert("Gesualdo".to_string(),
vec!["many madrigals".to_string(),
"Tenebrae Responsoria".to_string()]);
table.insert("Caravaggio".to_string(),
vec!["The Musicians".to_string(),
"The Calling of St. Matthew".to_string()]);
table.insert("Cellini".to_string(),
vec!["Perseus with the head of Medusa".to_string(),
"a salt cellar".to_string()]);
show(table);
}
E tudo funciona bem:
$ cargo run
Running `/home/jimb/rust/book/fragments/target/debug/fragments`
works by Gesualdo:
many madrigals
Tenebrae Responsoria
works by Cellini:
Perseus with the head of Medusa
a salt cellar
works by Caravaggio:
The Musicians
The Calling of St. Matthew
$
Mas, se você leu a seção do capítulo anterior sobre movimentos,
essa definição para show deve levantar algumas questões. Em
particular, HashMap não é Copy – nem pode ser, pois possui uma tabela
alocada dinamicamente. Então, quando o programa chama
show(table), toda a estrutura é movida para a função, deixando a
variável table não inicializada. (O programa também itera pelo seu
conteúdo sem nenhuma ordem específica; portanto, se você obteve
uma ordem diferente, não se preocupe.) Se o código de chamada
tentar utilizar table agora, ele terá problemas:
...
show(table);
assert_eq!(table["Gesualdo"][0], "many madrigals");
O Rust reclama que table não está mais disponível:
error: borrow of moved value: `table`
|
20 | let mut table = Table::new();
| --------- move occurs because `table` has type
| `HashMap<String, Vec<String>>`,
| which does not implement the `Copy` trait
...
31 | show(table);
| ----- value moved here
32 | assert_eq!(table["Gesualdo"][0], "many madrigals");
| ^^^^^ value borrowed here after move
De fato, se olharmos para a definição de show, o loop for exterior
toma posse da tabela hash e a consome inteiramente; e o loop for
interior faz o mesmo com cada um dos vetores. (Vimos esse
comportamento anteriormente, no exemplo “liberté, égalité,
fraternité”.) Por causa da semântica do movimento, destruímos
completamente toda a estrutura simplesmente tentando imprimi-la.
Obrigado, Rust!
A maneira correta de lidar com isso é utilizar referências. Uma
referência permite acessar um valor sem afetar sua posse. As
referências são de dois tipos:
• Uma referência compartilhada permite que você leia, mas não
modifique seu referente. Contudo, você pode ter tantas
referências compartilhadas quanto desejar para um determinado
valor. A expressão &e produz uma referência compartilhada para
valor de e; se e tem o tipo T, então &e tem o tipo &T, que se
pronuncia como “ref T”. As referências compartilhadas são Copy.
• Se você tem uma referência mutável a um valor, pode ler e
modificar o valor. No entanto, não pode ter nenhuma outra
referência ativa de qualquer tipo para esse valor ao mesmo
tempo. A expressão &mut e produz uma referência mutável para
valor de e; você escreve seu tipo como &mut T, que se pronuncia
como “ref mute T”. Referências mutáveis não são Copy.
Você pode pensar na distinção entre referências compartilhadas e
mutáveis como uma forma de impor uma regra vários leitores ou
único escritor em tempo de compilação. Na verdade, essa regra não
se aplica apenas a referências; abrange também o proprietário do
valor emprestado. Enquanto houver referências compartilhadas a um
valor, nem mesmo seu proprietário poderá modificá-lo; o valor está
bloqueado. Ninguém pode modificar table enquanto show estiver
trabalhando com ela. Da mesma forma, se houver uma referência
mutável a um valor, ela terá acesso exclusivo ao valor; você não
pode utilizar o proprietário de forma alguma, até que a referência
mutável desapareça. Manter o compartilhamento e a mutação
totalmente separados acaba sendo essencial para a segurança da
memória, por motivos que abordaremos mais adiante neste capítulo.
A função print do nosso exemplo não precisa modificar a tabela,
basta ler seu conteúdo. Portanto, o chamador deve ser capaz de
passar uma referência compartilhada para a tabela, como segue:
show(&table);
Referências são ponteiros não proprietários, então a variável table
permanece como proprietária de toda a estrutura; show apenas a
emprestou por um tempo. Naturalmente, precisaremos ajustar a
definição de show para combinar, mas você terá de olhar de perto
para ver a diferença:
fn show(table: &Table) {
for (artist, works) in table {
println!("works by {}:", artist);
for work in works {
println!(" {}", work);
}
}
}
O tipo de parâmetro table de show mudou de Table para &Table: em vez
de passar a tabela por valor (e, portanto, mover a posse para a
função), agora estamos passando uma referência compartilhada.
Essa é a única mudança textual. Mas como isso funciona enquanto
trabalhamos o corpo?
Considerando que nosso loop for externo original tomou posse do
HashMap e o consumiu, em nossa nova versão ele recebe uma
referência compartilhada ao HashMap. Iterar por uma referência
compartilhada para um HashMap é definido para produzir referências
compartilhadas à chave e ao valor de cada entrada: artist mudou de
uma String para uma &String, e works de uma Vec<String> para uma
&Vec<String>.
O loop interno é alterado de forma semelhante. A iteração por uma
referência compartilhada para um vetor é definida para produzir
referências compartilhadas para seus elementos, então work agora é
um &String. Nenhuma posse muda de mãos em qualquer lugar nesta
função; trata-se apenas de passar referências não proprietárias.
Agora, se quisermos escrever uma função para colocar em ordem
alfabética as obras de cada artista, uma referência compartilhada
não é suficiente, pois as referências compartilhadas não permitem
modificações. Em vez disso, a função de ordenação precisa ter uma
referência mutável à tabela:
fn sort_works(table: &mut Table) {
for (_artist, works) in table {
works.sort();
}
}
E precisamos passar-lhe uma:
sort_works(&mut table);
Esse empréstimo mutável concede a sort_works a capacidade de ler e
modificar nossa estrutura, conforme exigido pelo método sort dos
vetores.
Quando passamos um valor para uma função de uma maneira que
transfira a posse do valor para a função, dizemos que o passamos
por valor. Se, em vez disso, passarmos à função uma referência ao
valor, dizemos que passamos o valor por referência. Por exemplo,
corrigimos nossa função show alterando-a para aceitar a tabela por
referência, em vez de por valor. Muitas linguagens fazem essa
distinção, mas isso é especialmente importante no Rust, porque
explica como a posse é afetada.
Atribuição de referências
Atribuir uma referência a uma variável faz com que essa variável
aponte para algum lugar novo:
let x = 10;
let y = 20;
let mut r = &x;
if b { r = &y; }
assert!(*r == 10 || *r == 20);
A referência r inicialmente aponta para x. Mas, se b é verdade, o
código aponta para y em vez disso, conforme ilustrado na Figura 5.1.
Esse comportamento pode parecer óbvio demais para valer a pena
mencionar: obviamente r agora aponta para y, já que armazenamos
&y nela. Mas ressaltamos isso porque as referências em C++ se
comportam de maneira muito diferente: como mostrado
anteriormente, atribuir um valor a uma referência em C++
armazena o valor em seu referente. Uma vez que uma referência em
C++ foi inicializada, não há como fazê-la apontar para qualquer
outra coisa.
Referências a referências
O Rust permite referências a referências:
struct Point { x: i32, y: i32 }
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;
(Escrevemos os tipos de referência para maior clareza, mas você
pode omiti-los; não há nada aqui que o Rust não possa inferir.) O
operador . (ponto) segue quantas referências forem necessárias para
encontrar seu destino:
assert_eq!(rrr.y, 729);
Na memória, as referências são dispostas conforme a Figura 5.2.
Comparando referências
Como o operador . (ponto), os operadores de comparação do Rust
“veem através” de qualquer número de referências:
let x = 10;
let y = 10;
let rx = &x;
let ry = &y;
let rrx = ℞
let rry = &ry;
assert!(rrx <= rry);
assert!(rrx == rry);
A asserção final aqui é bem-sucedida, embora rrx e rry apontem para
valores diferentes (ou seja, rx e ry), porque o operador == segue
todas as referências e realiza a comparação em seus alvos finais, x e
y. Esse é quase sempre o comportamento que você deseja,
especialmente ao escrever funções genéricas. Se você realmente
deseja saber se duas referências apontam para a mesma memória,
pode utilizar std::ptr::eq, que os compara como endereços:
assert!(rx == ry); // seus referentes são iguais
assert!(!std::ptr::eq(rx, ry)); // mas ocupam endereços diferentes
Observe que os operandos de uma comparação devem ser
exatamente do mesmo tipo, incluindo as referências:
assert!(rx == rrx); // erro: tipo incompatível: `&i32` vs `&&i32`
assert!(rx == *rrx); // isso está certo
Segurança de referência
Como as apresentamos até agora, as referências se parecem muito
com ponteiros comuns em C ou C++. Mas estes não são seguros;
como o Rust mantém suas referências sob controle? Talvez a melhor
maneira de ver as regras em ação seja tentando quebrá-las.
Para explicar as ideias fundamentais, começaremos com os casos
mais simples, mostrando como o Rust garante que as referências
sejam utilizadas adequadamente em um único corpo de função. Em
seguida, veremos como passar referências entre funções e
armazená-las em estruturas de dados. Isso implica dar as ditas
funções e tipos de dados parâmetros de tempo de vida, que vamos
explicar. Por fim, apresentaremos alguns atalhos que o Rust fornece
para simplificar os padrões de uso comuns. Ao longo do caminho,
mostraremos como o Rust aponta códigos errados e geralmente
sugere soluções.
Figura 5.3: Tempos de vida permitidos para caracter em estilo -> &.
Se a referência não puder existir pelo menos tanto quanto a
variável, então em algum ponto r será um ponteiro perdido. Dizemos
que o tempo de vida da referência deve conter ou incluir o da
variável.
let x = 10;
g(&x);
A partir apenas da assinatura de g, o Rust sabe que não vai salvar p
em qualquer lugar que possa sobreviver à chamada: qualquer tempo
de vida que inclua a chamada deve funcionar para 'a. Então, o Rust
escolhe o menor tempo de vida possível para &x: o da chamada a g.
Isso atende a todas as restrições: não sobrevive a x e inclui toda a
chamada para a função g. Portanto, esse código passou no teste.
Note que, embora g aceite um parâmetro de tempo de vida 'a, não
precisamos mencioná-lo ao chamar g. Você só precisa se preocupar
com parâmetros de tempo de vida ao definir funções e tipos; ao
usá-los, o Rust infere os tempos de vida para você.
E se tentássemos passar &x para nossa função f antes de ela
armazenar seu argumento em uma variável estática?
fn f(p: &'static i32) { ... }
let x = 10;
f(&x);
Isso falha ao compilar: a referência &x não deve sobreviver a x, mas
passando-a para f nós a forçamos a existir pelo menos tanto quanto
'static. Não há como satisfazer a todos aqui, então o Rust rejeita o
código.
Retornando referências
É comum que uma função pegue uma referência a alguma estrutura
de dados e então retorne uma referência para alguma parte dessa
estrutura. Por exemplo, eis uma função que retorna uma referência
ao menor elemento de uma fatia:
// v deve ter pelo menos um elemento
fn smallest(v: &[i32]) -> &i32 {
let mut s = &v[0];
for r in &v[1..] {
if *r < *s { s = r; }
}
s
}
Omitimos os tempos de vida da assinatura dessa função da maneira
usual. Quando uma função recebe uma única referência como
argumento e retorna uma única referência, o Rust assume que as
duas devem ter o mesmo tempo de vida. Escrever isso
explicitamente nos daria:
fn smallest<'a>(v: &'a [i32]) -> &'a i32 { ... }
Suponha que chamemos smallest assim:
let s;
{
let parabola = [9, 4, 1, 0, 1, 4, 9];
s = smallest(¶bola);
}
assert_eq!(*s, 0); // ruim: aponta para o elemento do array dropado
A partir da assinatura de smallest, podemos ver que seu argumento e
valor de retorno devem ter o mesmo tempo de vida, 'a. Em nossa
chamada, o argumento ¶bola não deve sobreviver à própria
parabola, ainda que o valor de retorno de smallest deva durar pelo
menos tanto quanto s. Não há tempo de vida possível 'a que possa
satisfazer ambas as restrições, então o Rust rejeita o código:
error: `parabola` does not live long enough
|
11 | s = smallest(¶bola);
| -------- borrow occurs here
12 | }
| ^ `parabola` dropped here while still borrowed
13 | assert_eq!(*s, 0); // ruim: aponta para o elemento do array dropado
| - borrowed value needs to live until here
14 | }
Mover s de modo que seu tempo de vida esteja claramente contido
dentro de parabola corrige o problema:
{
let parabola = [9, 4, 1, 0, 1, 4, 9];
let s = smallest(¶bola);
assert_eq!(*s, 0); // ótimo: parabola ainda está viva
}
Os tempos de vida em assinaturas de função permitem que o Rust
avalie as relações entre as referências que você passa para a função
e aquelas que a função retorna, e eles garantem que estão sendo
utilizadas com segurança.
Figura 5.7: Uma referência a um vetor que foi movido para fora.
Ao longo de seu tempo de vida, uma referência compartilhada torna
seu referente somente leitura: você não pode atribuir algo ao
referente ou mover seu valor para outro lugar. Neste código, o
tempo de vida de r contém a tentativa de mover o vetor, então o
Rust rejeita o programa. Se você alterar o programa conforme
mostrado aqui, não há problema:
let v = vec![4, 8, 19, 27, 34, 10];
{
let r = &v;
r[0]; // ok: o vetor ainda está lá
}
let aside = v;
Nesta versão, r sai do escopo mais cedo, o tempo de vida da
referência termina antes que v seja movido para o lado, e tudo está
ok.
Eis uma maneira diferente de causar estragos. Suponha que temos
uma função útil para estender um vetor com os elementos de uma
fatia:
fn extend(vec: &mut Vec<f64>, slice: &[f64]) {
for elt in slice {
vec.push(*elt);
}
}
Essa é uma versão menos flexível (e muito menos otimizada) do
método extend_from_slice da biblioteca padrão em vetores. Podemos
usá-lo para construir um vetor a partir de fatias de outros vetores ou
arrays:
let mut wave = Vec::new();
let head = vec![0.0, 1.0];
let tail = [0.0, -1.0];
Precedência e associatividade
A Tabela 6.1 resume a sintaxe de expressão do Rust. Discutiremos
todos esses tipos de expressões neste capítulo. Os operadores são
listados em ordem de precedência, do mais alto para o mais baixo.
(Como a maioria das linguagens de programação, o Rust tem uma
precedência de operadores para determinar a ordem das operações
quando uma expressão contém vários operadores adjacentes. Por
exemplo, em limit < 2 * broom.size + 1, o operador . tem a precedência
mais alta, então o acesso ao campo acontece primeiro.)
Tabela 6.1: Expressões
Tipo de expressão Exemplo Traits relacionados
Literal de array [1, 2, 3]
Literal de repetição de [0; 50]
array
Tupla (6, "crullers")
Agrupamento (2 + 2)
Bloco { f(); g() }
Expressões de fluxo de if ok { f() }
controle if ok { 1 } else { 0 }
if let Some(x) = f() { x } else {
0}
match x { None => 0, _ => 1 }
for v in e { f(v); } std::iter::IntoIterator
while ok { ok = f(); }
while let Some(x) = it.next() {
f(x); }
loop { next_event(); }
break
continue
return 0
Chamada de macro println!("ok")
Caminho std::f64::consts::PI
Literal de struct Point {x: 0, y: 0}
Acesso ao campo de tupla pair.0 Deref, DerefMut
Acesso ao campo de struct point.x Deref, DerefMut
Chamada de método point.translate(50, 50) Deref, DerefMut
Chamada de função stdin() Fn(Arg0, ...) -> T,
FnMut(Arg0, ...) -> T,
FnOnce(Arg0, ...) -> T
Índice arr[0] Index, IndexMutDeref,
DerefMut
Verificação de erro create_dir("tmp")?
Tipo de expressão Exemplo Traits relacionados
NOT lógico/bit a bit !ok Not
Negação -num Neg
Desreferência *ptr Deref, DerefMut
Empréstimo &val
Conversão de tipo x as u32
Multiplicação n*2 Mul
Divisão n/2 Div
Resto (módulo) n%2 Rem
Adição n+1 Add
Subtração n-1 Sub
Deslocamento para a n << 1 Shl
esquerda
Deslocamento para a n >> 1 Shr
direita
AND bit a bit n&1 BitAnd
OR exclusivo bit a bit n^1 BitXor
OR bit a bit n|1 BitOr
Menor que n<1 std::cmp::PartialOrd
Menor ou igual n <= 1 std::cmp::PartialOrd
Maior que n>1 std::cmp::PartialOrd
Maior ou igual n >= 1 std::cmp::PartialOrd
Igual n == 1 std::cmp::PartialEq
Não igual n != 1 std::cmp::PartialEq
AND lógico x.ok && y.ok
OR lógico x.ok || backup.ok
Intervalo não inclusivo start .. stop
Intervalo inclusivo start ..= stop
Atribuição x = val
Atribuição composta x *= 1 MulAssign
x /= 1 DivAssign
x %= 1 RemAssign
x += 1 AddAssign
x -= 1 SubAssign
x <<= 1 ShlAssign
x >>= 1 ShrAssign
x &= 1 BitAndAssign
x ^= 1 BitXorAssign
Tipo de expressão Exemplo Traits relacionados
x |= 1 BitOrAssign
Closure |x, y| x + y
Todos os operadores que podem ser encadeados de forma útil são
associativos à esquerda. Ou seja, uma cadeia de operações como a –
b - c é agrupada como (a - b) - c, não a - (b - c). Os operadores que
podem ser encadeados dessa maneira são todos os que você pode
esperar:
* / % + - << >> & ^ | && || as
Os operadores de comparação, os operadores de atribuição e os
operadores de intervalo .. e ..= não podem ser encadeados de
nenhuma forma.
Declarações
Além de expressões e ponto e vírgulas, um bloco pode conter
qualquer número de declarações. Os mais comuns são declarações
let, que declaram variáveis locais:
let name: type = expr;
O tipo e o inicializador são opcionais. O ponto e vírgula é obrigatório.
Como todos os identificadores no Rust, os nomes das variáveis
devem começar com uma letra ou sublinhado e podem conter
dígitos somente após o primeiro caractere. O Rust tem uma
definição ampla de “letra”: isso inclui letras gregas, caracteres
latinos acentuados e muitos outros símbolos – qualquer coisa que o
Unicode Standard Annex #31 declare adequado. Emojis não são
permitidos.
Uma declaração let pode declarar uma variável sem inicializá-la. A
variável pode então ser inicializada com uma atribuição posterior.
Isso é ocasionalmente útil, porque às vezes uma variável deve ser
inicializada no meio de algum tipo de construção de fluxo de
controle:
let name;
if user.has_nickname() {
name = user.nickname();
} else {
name = generate_unique_name();
user.register(&name);
}
Aqui existem duas maneiras diferentes de se inicializar a variável
local name, mas de qualquer maneira será inicializada exatamente
uma vez e, portanto, name não precisa mais ser declarada mut.
É um erro utilizar uma variável antes de ser inicializada. (Isso está
intimamente relacionado ao erro de utilizar um valor depois que ele
foi movido. O Rust realmente quer que você utilize valores apenas
enquanto eles existirem!)
Ocasionalmente, você pode ver um código que parece redeclarar
uma variável existente, como este:
for line in file.lines() {
let line = line?;
...
}
A declaração let cria uma nova segunda variável, de um tipo
diferente. O tipo da primeira variável line é Result<String, io::Error>. O
segundo line é um String. Sua definição substitui a primeira para o
resto do bloco. Isso é chamado de sombreamento e é muito comum
em programas Rust. O código é equivalente a:
for line_result in file.lines() {
let line = line_result?;
...
}
Neste livro, continuaremos utilizando um sufixo _result nessas
situações para que as variáveis tenham nomes distintos.
Um bloco também pode conter declarações de itens. Um item é
simplesmente qualquer declaração que pode aparecer globalmente
em um programa ou módulo, como um fn, struct ou use.
Os capítulos posteriores abordarão os itens em detalhes. Por ora, fn
dá um exemplo suficiente. Qualquer bloco pode conter uma fn:
use std::io;
use std::cmp::Ordering;
v.sort_by(cmp_by_timestamp_then_name);
...
}
Quando uma fn é declarada dentro de um bloco, seu escopo é o
bloco inteiro – ou seja, pode ser utilizado em todo o bloco
envolvente. Mas uma fn aninhada não pode acessar variáveis locais
ou argumentos que estejam no escopo. Por exemplo, a função
cmp_by_timestamp_then_name não poderia utilizar v diretamente. (O Rust
também tem closures, que permitem a inclusão de escopos.
Consulte o Capítulo 14.)
Um bloco pode até conter um módulo inteiro. Isso pode parecer um
pouco demais – precisamos realmente ser capazes de aninhar cada
fragmento da linguagem dentro de cada outro fragmento? –, mas os
programadores (e particularmente os programadores que utilizam
macros) têm uma maneira de encontrar usos para cada gota de
ortogonalidade que a linguagem oferece.
if e match
A forma de uma expressão if é familiar:
if condition1 {
block1
} else if condition2 {
block2
} else {
block_n
}
Cada condition deve ser uma expressão do tipo bool; fiel à forma, o
Rust não converte implicitamente números ou ponteiros em valores
booleanos.
Ao contrário de C, os parênteses não são necessários em torno das
condições. Na verdade, rustc emitirá um aviso se parênteses
desnecessários estiverem presentes. As chaves, porém, são
necessárias.
Os blocos else if, bem como o else final, são opcionais. Uma expressão
if sem bloco else se comporta exatamente como se tivesse um vazio
bloco else.
Expressões match são algo como a instrução switch em C, porém mais
flexível. Um exemplo simples:
match code {
0 => println!("OK"),
1 => println!("Wires Tangled"),
2 => println!("User Asleep"),
_ => println!("Unrecognized Error {}", code)
}
Isso é algo que uma instrução switch poderia fazer. Exatamente um
dos quatro braços1 dessa expressão match será executado,
dependendo do valor de code. O curinga _ corresponde a tudo. Isso é
como o caso default: em uma instrução switch, exceto que deve vir por
último; colocar um padrão _ antes de outros padrões significa que
esse padrão terá precedência sobre os outros. Esses padrões nunca
corresponderão com nada (e o compilador o avisará sobre isso).
O compilador pode otimizar esse tipo de match utilizando uma tabela
de salto, assim como uma instrução switch em C++. Uma otimização
semelhante é aplicada quando cada braço de um match produz um
valor constante. Nesse caso, o compilador cria um array desses
valores e o match é compilado em um acesso de array. Além de uma
verificação de limites, não há nenhuma ramificação no código
compilado.
A versatilidade de match decorre da variedade de padrões suportados
que podem ser utilizados à esquerda de => em cada braço. Acima,
cada padrão é simplesmente um número inteiro constante. Também
mostramos expressões match que distinguem os dois tipos de valor
Option:
match params.get("name") {
Some(name) => println!("Hello, {}!", name),
None => println!("Greetings, stranger.")
}
Essa é apenas uma sugestão do que os padrões podem fazer. Um
padrão pode corresponder a um intervalo de valores. Pode
desempacotar tuplas. Pode corresponder a campos individuais de
structs. Pode buscar referências, emprestar partes de um valor e
muito mais. Os padrões do Rust são uma minilinguagem própria.
Dedicaremos várias páginas a eles no Capítulo 10.
A forma geral de uma expressão match é:
match value {
pattern => expr,
...
}
A vírgula após um braço pode ser dropada se a expr é um bloco.
O Rust verifica o dado value contra cada padrão por vez, começando
com o primeiro. Quando um padrão corresponde, a expr
correspondente é avaliada e a expressão match está completa;
nenhum outro padrão é verificado. Pelo menos um dos padrões deve
corresponder. O Rust proíbe expressões match que não cobrem todos
os valores possíveis:
let score = match card.rank {
Jack => 10,
Queen => 10,
Ace => 11
}; // erro: padrões não exaustivos
Todos os blocos de uma expressão if devem produzir valores do
mesmo tipo:
let suggested_pet =
if with_wings { Pet::Buzzard } else { Pet::Hyena }; // ok
let favorite_number =
if user.is_hobbit() { "eleventy-one" } else { 9 }; // erro
let best_sports_team =
if is_hockey_season() { "Predators" }; // erro
(O último exemplo é um erro porque em julho o resultado seria ().)
Da mesma forma, todos os braços de uma expressão match devem
ter o mesmo tipo:
let suggested_pet =
match favorites.element {
Fire => Pet::RedPanda,
Air => Pet::Buffalo,
Water => Pet::Orca,
_ => None // erro: tipos incompatíveis
};
if let
Há mais uma forma de if, a expressão if let:
if let pattern = expr {
block1
} else {
block2
}
A expr fornecida deve corresponder ao pattern para que block1 seja
executado, caso contrário block2 é executado. Às vezes, essa é uma
boa maneira de obter dados de uma Option ou um Result:
if let Some(cookie) = request.session_cookie {
return restore_session(cookie);
}
if let Err(err) = show_cheesy_anti_robot_task() {
log_robot_attempt(err);
politely_accuse_user_of_being_a_robot();
} else {
session.mark_as_human();
}
Nunca é estritamente necessário utilizar if let, porque match pode fazer
tudo que if let pode fazer. Uma expressão if let é uma abreviação para
um match com apenas um padrão:
match expr {
pattern => { block1 }
_ => { block2 }
}
Loops
Existem quatro expressões de loop:
while condition {
block
}
while let pattern = expr {
block
}
loop {
block
}
Expressões return
Uma expressão return sai da função atual, retornando um valor para o
chamador.
return sem um valor é um atalho para return ():
fn f() { // tipo de retorno omitido: o padrão é ()
return; // valor de retorno omitido: o padrão é ()
}
As funções não precisam ter uma expressão return explícita. O corpo
de uma função funciona como uma expressão de bloco: se a última
expressão não for seguida por um ponto e vírgula, seu valor será o
valor de retorno da função. Na verdade, essa é a maneira preferida
de fornecer o valor de retorno de uma função no Rust.
Mas isso não significa que return seja inútil, ou apenas uma
concessão para usuários que não têm experiência com linguagens
de expressão. Como uma expressão break, return pode abandonar o
trabalho em andamento. Por exemplo, no Capítulo 2, utilizamos o
operador ? para verificar erros após chamar uma função que pode
falhar:
let output = File::create(filename)?;
Explicamos que isso é uma abreviação para uma expressão match:
let output = match File::create(filename) {
Ok(f) => f,
Err(err) => return Err(err)
};
Esse código começa chamando File::create(filename). Se isso
retornar Ok(f), então toda expressão match é avaliada como f,
portanto f está armazenado em output e continuamos com a próxima
linha de código seguindo o match.
Caso contrário, corresponderemos Err(err) e alcançaremos a
expressão return. Quando isso acontece, não importa que estejamos
avaliando uma expressão match para determinar o valor da variável
output. Abandonamos tudo isso e saímos da função que a envolve,
retornando qualquer erro que obtivemos de File::create().
Vamos cobrir o operador ? mais completamente em “Propagando
erros”, na página 196.
Campos e elementos
Os campos de um struct são acessados utilizando uma sintaxe
familiar. As tuplas são iguais, exceto que seus campos têm números
em vez de nomes:
game.black_pawns // campo de struct
coords.1 // elemento de tupla
Se o valor à esquerda do ponto for uma referência ou um tipo de
ponteiro inteligente, ele será automaticamente desreferenciado,
assim como ocorre com chamadas de método.
Os colchetes acessam os elementos de um array, uma fatia ou um
vetor:
pieces[i] // elemento de array
O valor à esquerda dos colchetes é automaticamente
desreferenciado.
Expressões como essas três são chamadas lvalues, porque elas
podem aparecer no lado esquerdo de uma atribuição:
game.black_pawns = 0x00ff0000_00000000_u64;
coords.1 = 0;
pieces[2] = Some(Piece::new(Black, Knight, coords));
Naturalmente, isso só é permitido se game, coords e pieces são
declarados como variáveis mut.
Extrair uma fatia de um array ou vetor é simples:
let second_half = &game_moves[midpoint .. end];
Aqui game_moves pode ser um array, uma fatia ou um vetor; o
resultado, independentemente de tudo, é uma fatia emprestada de
comprimento end - midpoint. game_moves é considerado emprestado para
o tempo de vida de second_half.
O operador .. permite que qualquer operando seja omitido; produz
até quatro tipos diferentes de objetos dependendo de quais
operandos estão presentes:
.. // RangeFull
a .. // RangeFrom { start: a }
.. b // RangeTo { end: b }
a .. b // Range { start: a, end: b }
As duas últimas formas são não inclusivas (ou meio abertas): o valor
final não está incluído no intervalo representado. Por exemplo, o
intervalo 0 .. 3 inclui os números 0, 1 e 2.
O operador ..= produz intervalos inclusivos (ou fechados), que
incluem o valor final:
..= b // RangeToInclusive { end: b }
a ..= b // RangeInclusive::new(a, b)
Por exemplo, o intervalo 0 ..= 3 inclui os números 0, 1, 2 e 3.
Somente intervalos que incluem um valor inicial são iteráveis, pois
um loop deve ter um ponto de início. Mas no fatiamento de array,
todas as seis formas são úteis. Se o início ou o fim do intervalo for
omitido, o padrão será o início ou o fim dos dados que estão sendo
divididos.
Portanto, uma implementação de quicksort, o clássico algoritmo de
ordenação de divisão e conquista, pode parecer, em parte, assim:
fn quicksort<T: Ord>(slice: &mut [T]) {
if slice.len() <= 1 {
return; // Nada para resolver
}
// E a metade traseira
quicksort(&mut slice[pivot_index + 1 ..]);
}
Operadores de referência
Os operadores de endereço & e &mut são abordados no Capítulo 5.
O operador unário * é utilizado para acessar o valor apontado por
uma referência. Como vimos, o Rust segue referências
automaticamente quando você utiliza o operador . para acessar um
campo ou método, então o operador * é necessário apenas quando
queremos ler ou gravar todo o valor para o qual a referência aponta.
Por exemplo, às vezes um iterador produz referências, mas o
programa precisa dos valores subjacentes:
let padovan: Vec<u64> = compute_padovan_sequence(n);
for elem in &padovan {
draw_triangle(turtle, *elem);
}
Neste exemplo, o tipo de elem é &u64, então *elem é um u64.
Atribuição
O operador = pode ser utilizado para atribuir a variáveis mut e seus
campos ou elementos. Mas a atribuição não é tão comum no Rust
quanto em outras linguagens, pois as variáveis são imutáveis por
padrão.
Conforme descrito no Capítulo 4, se o valor tiver um tipo não-Copy, a
atribuição o move para o destino. A posse do valor é transferida da
origem para o destino. O valor anterior do destino, se houver, é
dropado.
A atribuição composta é suportada:
total += item.price;
Isso é equivalente a total = total + item.price;. Outros operadores também
são suportados: -=, *= e assim por diante. A lista completa é
fornecida na Tabela 6.1, anteriormente neste capítulo.
Ao contrário do C, o Rust não suporta atribuição de encadeamento:
você não pode escrever a = b = 3 para atribuir o valor 3 tanto para a
como para b. A atribuição é rara o suficiente no Rust para que você
não perca essa abreviação.
O Rust não possui operadores de incremento e decremento do C ++
e --.
Coerções de tipo
Converter um valor de um tipo para outro geralmente requer uma
conversão explícita no Rust. Coerções utilizam a palavra-chave as:
let x = 17; // x é do tipo i32
let index = x as usize; // converte para usize
Vários tipos de coerções são permitidos:
• Os números podem ser convertidos de qualquer um dos tipos
numéricos integrados para qualquer outro, por coerção.
A coerção de um inteiro para outro tipo inteiro é sempre bem
definida. A conversão para um tipo mais restrito resulta em
truncamento. Um inteiro com sinal convertido para um tipo mais
amplo é estendido por sinal, um inteiro sem sinal é estendido por
zero e assim por diante. Resumindo, não há surpresas.
A conversão de um tipo de ponto flutuante para um tipo inteiro
arredonda para zero: o valor de -1.99 as i32 é -1. Se o valor for muito
grande para caber no tipo inteiro, a conversão produzirá o valor
mais próximo que o tipo inteiro pode representar: o valor de 1e6 as
u8 é 255.
• Valores do tipo bool ou char, ou de um tipo enum parecido com C,
podem ser convertidos em qualquer tipo inteiro. (Abordaremos
enums no Capítulo 10.)
Fazer a coerção na outra direção não é permitido, pois os tipos
bool, char e enum têm restrições sobre seus valores que teriam de
ser aplicadas com verificações em tempo de execução. Por
exemplo, fazer a coerção de um tipo u16 em um char é proibido
porque alguns valores u16, como 0xd800, correspondem a pontos de
código substitutos Unicode e, portanto, não assumiria valores char
válidos. Existe um método padrão, std::char::from_u32(), que executa
a verificação em tempo de execução e retorna um Option<char>;
contudo, mais especificamente, a necessidade desse tipo de
conversão tornou-se rara. Normalmente, convertemos strings de
caracteres ou fluxos inteiros de uma só vez e os algoritmos em
texto Unicode geralmente não são triviais e é melhor deixá-los
para as bibliotecas.
Como exceção, um u8 pode ser convertido para um tipo char, já
que todos os números inteiros de 0 a 255 são pontos de código
Unicode válidos para char armazenar.
• Algumas conversões envolvendo tipos de ponteiro inseguros
também são permitidas. Ver “Ponteiros brutos”, na página 90.
Dissemos que uma conversão geralmente requer uma coerção.
Algumas conversões envolvendo tipos de referência são tão diretas
que a linguagem as realiza mesmo sem conversão. Um exemplo
trivial é a conversão de uma referência mut em uma referência não-
mut.
Várias conversões automáticas mais significativas, porém, podem
acontecer:
• Valores do tipo &String são automaticamente convertidos para o
tipo &str sem coerção.
• Valores do tipo &Vec<i32> são automaticamente convertidos para &
[i32].
• Valores do tipo &Box<Chessboard> são automaticamente convertidos
para &Chessboard.
Estas são chamadas de coerções deref, porque elas se aplicam a
tipos que implementam o trait Deref interno. O propósito da coerção
Deref é fazer com que tipos de ponteiro inteligentes, como Box,
comportem-se o mais próximo possível do valor subjacente. Utilizar
um Box<Chessboard> é basicamente como utilizar um simples Chessboard,
graças a Deref.
Tipos definidos pelo usuário também podem implementar o trait
Deref. Quando precisar escrever seu próprio tipo de ponteiro
inteligente, consulte “Deref e DerefMut“, na página 362.
Closures
O Rust tem closures, valores semelhantes a funções leves. Uma
closure geralmente consiste em uma lista de argumentos, fornecida
entre barras verticais, seguida por uma expressão:
let is_even = |x| x % 2 == 0;
O Rust infere os tipos de argumento e o tipo de retorno. Você
também pode escrevê-los explicitamente, como faria para uma
função. Se você especificar um tipo de retorno, o corpo da closure
deve ser um bloco, por uma questão de sanidade sintática:
let is_even = |x: u64| -> bool x % 2 == 0; // erro
A seguir
Expressões são o que pensamos como “código em execução”. Elas
são a parte de um programa Rust que compila as instruções de
máquina. Entretanto, são uma pequena fração de toda a linguagem.
O mesmo é verdade na maioria das linguagens de programação. A
primeira tarefa de um programa é executar, mas essa não é sua
única tarefa. Os programas precisam se comunicar. Eles têm de ser
testáveis. Eles precisam se manter organizados e flexíveis para que
possam continuar a evoluir. Eles precisam interoperar com códigos e
serviços criados por outras equipes. E mesmo apenas para executar,
programas em uma linguagem estaticamente tipada como o Rust
precisam de mais algumas ferramentas para organizar dados do que
apenas tuplas e arrays.
A seguir, passaremos vários capítulos falando sobre recursos nessa
área: módulos e crates, que fornecem a estrutura do seu programa
e, em seguida, structs e enums, que fazem o mesmo com seus
dados.
Primeiro, vamos dedicar algumas páginas ao importante tópico sobre
o que fazer quando as coisas dão errado.
1 N.R.: Rust chama de braço cada par padrão => Expressão. Em C/C++ os braços seriam
chamados de casos.
7capítulo
Tratamento de erros
Pânico
Um programa gera um pânico quando encontra algo tão confuso que
deve haver um bug no próprio programa. Algo como:
• Acesso ao array fora dos limites.
• Divisão de inteiro por zero.
• Chamar .expect() sobre um Result que é um Err.
• Falha na asserção.
(Há também a macro panic!(), para casos em que seu próprio código
descobre que falhou e, portanto, você precisa acionar um pânico
diretamente. panic!() aceita argumentos opcionais no estilo println!(),
para construir uma mensagem de erro.)
O que essas condições têm em comum é que são todas – para não
exagerar – culpa do programador. Uma boa regra de ouro é: “Não
gerar pânico”.
Mas todos cometemos erros. Quando esses erros que não deveriam
acontecer acontecem – o que acontece? Notavelmente, o Rust
oferece uma escolha. O Rust pode desempilhar o stack (pilha)
quando ocorre um pânico ou abortar o processo. Desempilhar é o
padrão.
Desempilhamento
Quando os piratas dividem os objetos saqueados de um ataque, o
capitão fica com metade do saque. Os tripulantes comuns ganham
partes iguais da outra metade. (Piratas odeiam frações; então, se
qualquer uma das divisões não sair igual, o resultado é arredondado
para baixo, com o resto indo para o papagaio do navio.)
fn pirate_share(total: u64, crew_size: usize) -> u64 {
let half = total / 2;
half / crew_size as u64
}
Isso pode ter funcionado bem por séculos até que um dia
descobrimos que o capitão é o único sobrevivente de um ataque. Se
passarmos um crew_size de zero a essa função, ele será dividido por
zero. Em C++ isso seria um comportamento indefinido. No Rust,
desencadeia um pânico, que normalmente ocorre da seguinte
maneira:
• Uma mensagem de erro é impressa no terminal:
thread 'main' panicked at 'attempt to divide by zero', pirates.rs:3780
note: Run with `RUST_BACKTRACE=1` for a backtrace.
Se você definir a variável de ambiente RUST_BACKTRACE, como as
mensagens sugerem, o Rust também vai mostrar o stack (pilha)
neste ponto.
• O stack (pilha) é desempilhado. Isso é muito parecido com o
tratamento de exceções em C++.
Quaisquer valores temporários, variáveis locais ou argumentos que
a função atual estava utilizando são dropados, na ordem inversa
em que foram criados. Dropar um valor significa simplesmente
limpá-lo: qualquer String ou Vec que o programa estava utilizando
são dropados, qualquer arquivo File aberto é fechado e assim por
diante. Métodos drop definidos pelo usuário também são
chamados; veja “Drop”, na página 353. No caso particular de
pirate_share(), não há nada para limpar.
Depois que a chamada de função atual é limpa, passamos para
seu chamador, dropando suas variáveis e argumentos da mesma
maneira. Então passamos para esse chamador da função e assim
por diante no stack (pilha).
• Por fim, o thread sai. Se o thread em pânico for o thread
principal, todo o processo será encerrado (com um código de
saída diferente de zero).
Talvez pânico seja um nome enganoso para esse processo ordenado.
Um pânico não é um acidente. Não é um comportamento indefinido.
É mais como um RuntimeException em Java ou um std::logic_error em C++.
O comportamento é bem definido; simplesmente não deveria estar
acontecendo.
O pânico é seguro. Não viola nenhuma das regras de segurança do
Rust; mesmo se você gerar um pânico no meio de um método de
biblioteca padrão, ele nunca deixará um ponteiro perdido ou um
valor inicializado pela metade na memória. A ideia é que o Rust
pegue o acesso inválido ao array, ou seja lá o que for, antes de
qualquer coisa ruim acontecer. Seria inseguro continuar, então o
Rust desmonta o stack (pilha). Mas o restante do processo pode
continuar em execução.
O pânico é por thread. Um thread pode gerar um pânico enquanto
outros threads continuam funcionando normalmente. No
Capítulo 19, mostraremos como um thread pai pode descobrir
quando um thread filho gera um pânico e lidar com o erro
normalmente.
Existe também uma maneira de capturar o desempilhamento do
stack (pilha), permitindo que o thread sobreviva e continue em
execução. A função std::panic::catch_unwind() da biblioteca padrão faz
isso. Não abordaremos como usá-lo, mas esse é o mecanismo
utilizado pelo mecanismo de teste do Rust para se recuperar quando
uma asserção falha em um teste. (Também pode ser necessário ao
escrever código Rust que possa ser chamado de C ou C++ porque
fazer o desempilhamento em código não Rust é um comportamento
indefinido; consulte o Capítulo 22.)
Se fosse possível, todos nós teríamos um código livre de bugs que
nunca gera um pânico. Mas ninguém é perfeito. Você pode utilizar
threads e catch_unwind() para lidar com o pânico, tornando seu
programa mais robusto. Uma ressalva importante é que essas
ferramentas capturam apenas pânicos que desempilham o stack
(pilha). Nem todo pânico procede dessa maneira.
Abortando
O desempilhamento do stack é o comportamento de pânico padrão,
mas há duas circunstâncias nas quais o Rust não tenta desempilhar
o stack.
Se um método .drop() aciona um segundo pânico enquanto o Rust
ainda está tentando limpar após o primeiro, isso é considerado fatal.
O Rust para de desempilhar e aborta todo o processo.
Além disso, o comportamento de pânico do Rust é personalizável. Se
você compilar com -C panic=abort, o primeiro pânico em seu programa
imediatamente aborta o processo. (Com essa opção, o Rust não
precisa saber como desempilhar o stack (pilha), então isso pode
reduzir o tamanho do seu código compilado.)
Isso conclui nossa discussão sobre pânico no Rust. Não há muito a
dizer, porque o código Rust comum não tem obrigação de lidar com
o pânico. Mesmo se você utilizar threads ou catch_unwind(), todo o seu
código de gerenciamento de pânico provavelmente estará
concentrado em alguns lugares. Não é razoável esperar que todas as
funções de um programa antecipem e lidem com bugs em seu
próprio código. Erros causados por outros fatores são outro
problema.
Result
O Rust não tem exceções. Em vez disso, as funções que podem
falhar têm um tipo de retorno que diz isto:
fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>
O tipo Result indica possível falha. Quando chamamos a função
get_weather(), ela retornará um resultado de sucesso Ok(weather), em
que weather é um novo valor WeatherReport ou um resultado do erro
Err(error_value), em que error_value é um io::Error explicando o que está
errado.
O Rust exige que escrevamos algum tipo de tratamento de erro
sempre que chamarmos essa função. Não podemos chegar ao
WeatherReport sem fazer algo com o Result e você receberá um aviso do
compilador se um valor de Result não for utilizado.
No Capítulo 10, veremos como a biblioteca padrão define Result e
como você pode definir seus próprios tipos semelhantes. Por
enquanto, vamos adotar uma abordagem de “livro de receitas” e
focar em como utilizar Results para obter o comportamento de
tratamento de erros que você deseja. Veremos como detectar,
propagar e relatar erros, bem como padrões comuns para organizar
e trabalhar com tipos Result.
Capturando erros
A maneira mais completa de lidar com um Result é a maneira que
mostramos no Capítulo 2: utilize uma expressão match.
match get_weather(hometown) {
Ok(report) => {
display_weather(hometown, &report);
}
Err(err) => {
println!("error querying the weather: {}", err);
schedule_weather_retry();
}
}
Esse é o equivalente do Rust a try/catch em outras linguagens. É o
que você utiliza quando deseja lidar com erros diretamente e não os
repassa ao chamador.
match é um pouco prolixo, então Result<T, E> oferece uma variedade de
métodos que são úteis em casos comuns específicos. Cada um
desses métodos tem uma expressão match na sua execução. (Para a
lista completa de métodos Result, consulte a documentação on-line.
Os métodos listados aqui são os que mais utilizamos.)
result.is_ok(), result.is_err()
Retorna um bool dizendo se result é um resultado de sucesso ou um
resultado de erro.
result.ok()
Retorna o valor de sucesso, se houver, como um Option<T>. Se result
é um resultado de sucesso, isso retorna Some(success_value); caso
contrário, retorna None, descartando o valor do erro.
result.err()
Retorna o valor do erro, se houver, como um Option<E>.
result.unwrap_or(fallback)
Retorna o valor de sucesso, se result é um resultado de sucesso.
Caso contrário, retorna fallback, descartando o valor do erro.
// Uma previsão bastante segura para o sul da Califórnia
const THE_USUAL: WeatherReport = WeatherReport::Sunny(72);
Imprimindo erros
Às vezes, a única maneira de lidar com um erro é mostrando-o no
terminal e seguindo em frente. Já mostramos uma maneira de fazer
isso:
println!("error querying the weather: {}", err);
A biblioteca padrão define vários tipos de erros com nomes
enfadonhos: std::io::Error, std::fmt::Error, std::str::Utf8Error e assim por
diante. Todos eles implementam uma interface comum, o trait
std::error::Error, o que significa que eles compartilham os seguintes
recursos e métodos:
println!()
Todos os tipos de erro podem ser impressos utilizando isso.
Imprimir um erro com o especificador de formato {} normalmente
exibe apenas uma breve mensagem de erro. Alternativamente,
você pode imprimir com o especificador de formato {:?}, para obter
uma visualização Debug do erro. Isso é menos amigável, mas inclui
informações técnicas extras.
// resultado de `println!("error: {}", err);`
error: failed to look up address information: No address associated with
hostname
// resultado de `println!("error: {:?}", err);`
error: Error { repr: Custom(Custom { kind: Other, error: StringError(
"failed to look up address information: No address associated with
hostname") }) }
err.to_string()
Retorna a mensagem de erro como String.
err.source()
Retorna uma Option do erro subjacente, se houver, que causou o err.
Por exemplo, um erro de rede pode fazer com que uma transação
bancária falhe, o que, por sua vez, pode fazer com que você perca
o barco que acabou de comprar. Se err.to_string() é "boat was repossessed",
então err.source() pode retornar um erro sobre a transação
malsucedida. Esse erro é .to_string() pode ser "failed to transfer $300 to
United Yacht Supply" e sua .source() pode ser um io::Error com detalhes
sobre a interrupção de rede específica que causou toda a confusão.
Esse terceiro erro é a causa raiz, então seu método .source()
retornaria None. Como a biblioteca padrão inclui apenas recursos de
baixo nível, a origem dos erros retornados da biblioteca padrão
geralmente é None.
A impressão de um valor de erro também não imprime sua origem.
Se você quiser ter certeza de imprimir todas as informações
disponíveis, utilize esta função:
use std::error::Error;
use std::io::{Write, stderr};
/// Escreve uma mensagem de erro em `stderr`
/// Se outro erro ocorrer durante a criação da mensagem de erro
/// ou a escrita em `stderr`, ele é ignorado
fn print_error(mut err: &dyn Error) {
let _ = writeln!(stderr(), "error: {}", err);
while let Some(source) = err.source() {
let _ = writeln!(stderr(), "caused by: {}", source);
err = source;
}
}
A macro writeln! funciona como println!, exceto que escreve os dados
em um fluxo de sua escolha. Aqui, escrevemos as mensagens de
erro no fluxo de erro padrão, std::io::stderr. Poderíamos utilizar a macro
eprintln! para fazer a mesma coisa, mas eprintln! gera um pânico se
ocorrer um erro. No print_error, queremos ignorar os erros que surgem
ao escrever a mensagem; explicamos porque em “Ignorando erros”,
na página 201, mais adiante no capítulo.
Os tipos de erro da biblioteca padrão não incluem um rastreamento
do stack (pilha), mas o popular crate anyhow fornece um tipo de erro
pronto, quando utilizado com uma versão instável do compilador
Rust. (No Rust 1.50, as funções da biblioteca padrão para capturar
backtraces ainda não estavam estabilizadas.)
Propagando erros
Na maioria dos lugares onde tentamos algo que pode falhar, não
queremos detectar e tratar o erro imediatamente. É simplesmente
muito código para utilizar uma instrução match de 10 linhas em todos
os lugares onde algo pode dar errado.
Em vez disso, se ocorrer um erro, geralmente queremos deixar
nosso chamador lidar com isso. Queremos que os erros se
propaguem no call stack (pilha de chamadas).
O Rust tem um operador ? que faz isso. Você pode adicionar um ? a
qualquer expressão que produza um Result, como o resultado de uma
chamada de função:
let weather = get_weather(hometown)?;
O comportamento de ? depende se essa função retornar um
resultado de sucesso ou um resultado de erro:
• Com sucesso, ele desencapsula o Result para obter o valor de
sucesso dentro. O tipo de weather aqui não é Result<WeatherReport,
io::Error> mas simplesmente WeatherReport.
• Em caso de erro, ele retorna imediatamente da função que o
envolve, passando o resultado do erro para cima na cadeia de
chamadas. Para garantir que isso funcione, ? só pode ser utilizado
em um Result em funções que possuem um tipo Result de retorno.
Não há nada de mágico no operador ?. Você pode expressar a
mesma coisa utilizando uma expressão match, embora seja muito
mais prolixo:
let weather = match get_weather(hometown) {
Ok(success_value) => success_value,
Err(err) => return Err(err)
};
As únicas diferenças entre isso e o operador ? são alguns pontos
delicados envolvendo tipos e conversões. Abordaremos esses
detalhes na próxima seção.
Em código mais antigo, você pode ver a macro try!(), que era a
maneira usual de propagar erros até que o operador ? foi introduzido
no Rust 1.13:
let weather = try!(get_weather(hometown));
A macro se expande para uma expressão match, como a anterior.
É fácil esquecer o quão difundida é a possibilidade de erros em um
programa, especialmente no código que faz interface com o sistema
operacional. Às vezes, o operador ? aparece em quase todas as
linhas de uma função:
use std::fs;
use std::io;
use std::path::Path;
fn move_all(src: &Path, dst: &Path) -> io::Result<()> {
for entry_result in src.read_dir()? { // a abertura do diretório pode falhar
let entry = entry_result?; // a leitura do diretório pode falhar
let dst_file = dst.join(entry.file_name());
fs::rename(entry.path(), dst_file)?; // a renomeação pode falhar
}
Ok(()) // ufa!
}
? também funciona de forma semelhante com o tipo Option. Em uma
função que retorna Option, você pode utilizar ? para desencapsular um
valor ou retornar mais cedo no caso de None:
let weather = get_weather(hometown).ok()?;
Ignorando erros
Ocasionalmente, queremos apenas ignorar um erro completamente.
Por exemplo, em nossa função print_error(), tivemos de lidar com a
situação improvável em que imprimir o erro aciona outro erro. Isso
pode acontecer, por exemplo, se stderr é redirecionado para outro
processo e esse processo está morto. O erro original que estávamos
tentando relatar é provavelmente mais importante para propagarem
então queremos apenas ignorar os problemas com stderr, mas o
compilador Rust avisa sobre valores Result:
writeln!(stderr(), "error: {}", err); // aviso: resultado não utilizado
A expressão let _ = ... é utilizada para silenciar esse aviso:
let _ = writeln!(stderr(), "error: {}", err); // ok, ignore o resultado
#[derive(Debug, Clone)]
pub struct JsonError {
pub message: String,
pub line: usize,
pub column: usize,
}
Esse struct será chamado de json::error::JsonError e, quando quiser gerar
um erro desse tipo, você pode escrever:
return Err(JsonError {
message: "expected ']' at end of array".to_string(),
line: current_line,
column: current_column
});
Isso vai funcionar bem. No entanto, se deseja que seu tipo de erro
funcione como os tipos de erro padrão, como os usuários de sua
biblioteca esperam, então você tem um pouco mais de trabalho a
fazer:
use std::fmt;
Crates
Os programas do Rust são feitos de crates. Cada crate é uma
unidade completa e coesa: todo o código-fonte para uma única
biblioteca ou executável, além de quaisquer testes associados,
exemplos, ferramentas, configuração e outras tranqueiras. Para seu
simulador de samambaia, você pode utilizar bibliotecas de terceiros
para gráficos 3D, bioinformática, computação paralela e assim por
diante. Essas bibliotecas são distribuídas como crates (consulte a
Figura 8.1).
Edições
O Rust tem garantias de compatibilidade extremamente fortes.
Qualquer código compilado no Rust 1.0 deve compilar tão bem no
Rust 1.50 ou, se for lançado, no Rust 1.900.
Mas, às vezes, há propostas convincentes de extensões para a
linguagem que fariam com que o código antigo não fosse mais
compilado. Por exemplo, depois de muita discussão, o Rust
estabeleceu uma sintaxe para suporte de programação assíncrona
que reaproveita os identificadores async e await como palavras-chave
(ver Capítulo 20). Mas essa mudança de linguagem quebraria
qualquer código existente que utilize async ou await como o nome de
uma variável.
Para evoluir sem quebrar o código existente, o Rust utiliza edições. A
edição 2015 do Rust é compatível com o Rust 1.0. A edição de 2018
transformou async e await em palavras-chave e simplificou o sistema
de módulos, enquanto a edição de 2021 melhorou a ergonomia do
array e disponibilizou algumas definições de biblioteca amplamente
utilizadas em todos os lugares por padrão. Todas essas foram
melhorias importantes para a linguagem, mas quebrariam o código
existente. Para evitar isso, cada crate indica em qual edição do Rust
esse crate foi escrito com uma linha como esta na seção [package] na
parte superior do seu arquivo Cargo.toml:
edition = "2021"
Se essa palavra-chave estiver ausente, a edição de 2015 será
assumida, de modo que os crates antigos não precisem ser
alterados. Mas, se quiser utilizar funções assíncronas ou o novo
sistema de módulos, você precisará de edition = "2018" ou posterior em
seu arquivo Cargo.toml.
O Rust promete que o compilador sempre aceitará todas as edições
existentes da linguagem e os programas podem misturar livremente
crates escritos em diferentes edições. É até bom que um crate da
edição de 2015 dependa de um crate da edição de 2021. Em outras
palavras, a edição de um crate afeta apenas como seu código-fonte
é interpretado; distinções de edição desaparecem no momento em
que o código é compilado. Isso significa que não há pressão para
atualizar crates antigos apenas para continuar a participar do
ecossistema Rust moderno. Da mesma forma, não há pressão para
manter seu crate em uma edição mais antiga para evitar
inconvenientes para seus usuários. Você só precisa alterar as edições
quando quiser utilizar novos recursos de linguagem em seu próprio
código.
As edições não saem todos os anos, apenas quando o projeto Rust
decide que é necessário. Por exemplo, não há edição de 2020.
Configurar edition como "2020" causa um erro. O Rust Edition Guide
(https://oreil.ly/bKEO7) cobre as mudanças introduzidas em cada
edição e fornece um bom histórico sobre o sistema de edição.
Quase sempre é uma boa ideia utilizar a edição mais recente,
especialmente para novos códigos. cargo new cria novos projetos na
última edição por padrão. Este livro utiliza a edição de 2021 em todo
o livro.
Se você tiver um crate escrito em uma edição mais antiga do Rust, o
comando cargo fix pode ajudá-lo a atualizar automaticamente seu
código para a edição mais recente. O Rust Edition Guide explica o
comando cargo fix em detalhes.
Perfis de compilação
Existem várias definições de configuração que você pode colocar em
seu arquivo Cargo.toml que afetam as linhas de comando rustc que
cargo gera (Tabela 8.1).
Módulos
Considerando que os crates são sobre compartilhamento de código
entre projetos, módulos são sobre organização de código dentro de
um projeto. Eles atuam como namespaces do Rust, contêineres para
as funções, tipos, constantes etc. que compõem seu programa ou
biblioteca Rust. Um módulo se parece com isto:
mod spores {
use cells::{Cell, Gene};
/// Uma célula gerada por uma samambaia adulta. Ela se dispersa com
// o vento como parte do ciclo de vida da samambaia. Um esporo cresce
// em um prótalo -- um organismo completamente separado, com até 5 mm
// de diâmetro -- que produz o zigoto que se desenvolve em uma nova
/// samambaia. (O sexo das plantas é complicado.)
pub struct Spore {
...
}
/// Simula a produção de um esporo por meiose
pub fn produce_spore(factory: &mut Sporangium) -> Spore {
...
}
/// Extrai os genes em um determinado esporo
pub(crate) fn genes(spore: &Spore) -> Vec<Gene> {
...
}
/// Mistura genes para se preparar para a meiose (parte da interfase)
fn recombine(parent: &mut Cell) {
...
}
...
}
Um módulo é uma coleção de itens, características nomeadas como
o struct Spore e as duas funções neste exemplo. A palavra-chave pub
torna um item público, para que possa ser acessado de fora do
módulo.
Uma função é marcada pub(crate), o que significa que está disponível
em qualquer lugar dentro desse crate, mas não é exposta como
parte da interface externa. Ela não pode ser utilizada por outros
crates e não aparecerá na documentação desse crate.
Qualquer coisa que não esteja marcada como pub é privada e só
pode ser utilizada no mesmo módulo em que está definida ou em
qualquer módulo filho:
let s = spores::produce_spore(&mut factory); // ok
spores::recombine(&mut cell); // erro: `recombine` é privado
Marcar um item como pub é muitas vezes conhecido como “exportar”
esse item.
O restante desta seção cobre os detalhes que você precisa saber
para fazer uso total dos módulos:
• Mostramos como aninhar módulos e distribuí-los em diferentes
arquivos e diretórios, se necessário.
• Explicamos a sintaxe de path que o Rust utiliza para referenciar
itens de outros módulos e mostramos como importar itens para
que você possa usá-los sem ter de escrever seus caminhos
completos.
• Abordamos o controle refinado do Rust para campos struct.
• Apresentamos os módulos de prelúdio, que reduzem o código que
você precisaria escrever reunindo importações comuns que quase
qualquer usuário usará.
• Apresentamos as definições de constantes e variáveis estáticas,
duas maneiras de definir valores nomeados, para clareza e
consistência.
Módulos aninhados
Os módulos podem ser aninhados e é bastante comum ver um
módulo que é apenas uma coleção de submódulos:
mod plant_structures {
pub mod roots {
...
}
pub mod stems {
...
}
pub mod leaves {
...
}
}
Se você deseja que um item em um módulo aninhado fique visível
para outros crates, certifique-se de marcá-lo, e também todos os
módulos envolventes, como públicos. Caso contrário, você pode ver
um aviso como este:
warning: function is never used: `is_square`
|
23 | / pub fn is_square(root: &Root) -> bool {
24 | | root.cross_section_shape().is_square()
25 | | }
| |_________^
|
Talvez essa função realmente seja um código morto no momento.
Mas, se você pretendia utilizá-la em outros crates, o Rust está
informando que ela não é realmente visível para eles. Você deve
certificar-se de que seus módulos envolventes sejam todos
marcados com pub também.
Também é possível especificar pub(super), tornando um item visível
apenas para o módulo pai e pub(in <path>), o que o torna visível em
um módulo pai específico e seus descendentes. Isso é especialmente
útil com módulos profundamente aninhados:
mod plant_structures {
pub mod roots {
pub mod products {
pub(in crate::plant_structures::roots) struct Cytokinin {
...
}
}
Caminhos e importações
O operador :: é utilizado para acessar os recursos de um módulo. O
código em qualquer lugar do seu projeto pode referenciar qualquer
recurso de biblioteca padrão escrevendo seu caminho:
if s1 > s2 {
std::mem::swap(&mut s1, &mut s2);
}
std é o nome da biblioteca padrão. O caminho std referencia o módulo
de nível superior da biblioteca padrão. std::mem é um submódulo
dentro da biblioteca padrão e std::mem::swap é uma função pública
nesse módulo.
Você poderia escrever todo o seu código dessa maneira, digitando
std::f64::consts::PI e std::collections::HashMap::new toda vez que quisesse um
círculo ou um dicionário, mas isso seria tedioso e difícil de ler. A
alternativa é importar recursos nos módulos em que são utilizados:
use std::mem;
if s1 > s2 {
mem::swap(&mut s1, &mut s2);
}
A declaração use faz com que o nome mem seja um alias local para
std::mem ao longo do bloco ou módulo envolvente.
Poderíamos escrever use std::mem::swap; para importar a função swap
em si em vez do módulo mem. Mas o que fizemos anteriormente
geralmente é considerado o melhor estilo: importar tipos, traits e
módulos (como std::mem) e, em seguida, utilizar caminhos relativos
para acessar as funções, constantes e outros membros dentro deles.
Vários nomes podem ser importados de uma só vez:
use std::collections::{HashMap, HashSet}; // importa ambos
use std::fs::{self, File}; // importa `std::fs` e `std::fs::File`
use std::io::prelude::*; // importa tudo
Esse é apenas um atalho para escrever todas as importações
individuais:
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::fs::File;
// todos os itens públicos em std::io::prelude:
use std::io::prelude::Read;
use std::io::prelude::Write;
use std::io::prelude::BufRead;
use std::io::prelude::Seek;
Você pode utilizar as para importar um item, mas dar a ele um nome
diferente localmente:
use std::io::Result as IOResult;
Prelúdio padrão
Dissemos há pouco que cada módulo começa com uma “folha em
branco”, no que diz respeito aos nomes importados. Mas a lousa não
é completamente em branco.
Por um lado, a biblioteca padrão std é vinculada automaticamente a
cada projeto. Isso significa que você sempre pode ir com use
std::whatever ou consultar std itens por nome, como std::mem::swap() inline
no seu código. Além disso, alguns nomes particularmente úteis,
como Vec e Result, estão incluídos no prelúdio padrão e são
importados automaticamente. O Rust se comporta como se cada
módulo, incluindo o módulo raiz, começasse com a seguinte
importação:
use std::prelude::v1::*;
O prelúdio padrão contém algumas dezenas de traits e tipos
comumente utilizados.
No Capítulo 2, mencionamos que às vezes as bibliotecas fornecem
módulos chamados prelude. Mas std::prelude::v1 é o único prelúdio que é
importado automaticamente. Nomear um módulo prelude é apenas
uma convenção que informa aos usuários que ele deve ser
importado utilizando *.
fn main() {
let mut fern = Fern {
size: 1.0,
growth_rate: 0.001
};
run_simulation(&mut fern, 1000);
println!("final fern size: {}", fern.size);
}
Assumiremos que esse programa tem um arquivo Cargo.toml trivial:
[package]
name = "fern_sim"
version = "0.1.0"
authors = ["You <[email protected]>"]
edition = "2021"
Transformar esse programa em uma biblioteca é fácil. Eis os passos:
1. Renomeie o arquivo src/main.rs para src/lib.rs.
2. Adicione a palavra-chave pub para itens em src/lib.rs que serão
recursos públicos de nossa biblioteca.
3. Mova a função main para um arquivo temporário em algum lugar.
Voltaremos a isso em um minuto.
O arquivo src/lib.rs resultante fica assim:
pub struct Fern {
pub size: f64,
pub growth_rate: f64
}
impl Fern {
/// Simula uma samambaia crescendo por um dia
pub fn grow(&mut self) {
self.size *= 1.0 + self.growth_rate;
}
}
Diretório src/bin
Fazer o programa original de linha de comando fern_sim funcionar
novamente também é simples: Cargo tem algum suporte integrado
para pequenos programas que vivem no mesmo crate de uma
biblioteca.
Na verdade, o próprio Cargo é escrito dessa maneira. A maior parte
do código está em uma biblioteca Rust. O programa de linha de
comando cargo que utilizamos ao longo deste livro é um programa
encapsulador simples que chama a biblioteca para todo o trabalho
pesado. Tanto a biblioteca quanto o programa de linha de comando
residem no mesmo repositório de origem (https://oreil.ly/aJKOk).
Também podemos manter nosso programa e nossa biblioteca no
mesmo crate. Coloque esse código em um arquivo chamado
src/bin/efern.rs:
use fern_sim::{Fern, run_simulation};
fn main() {
let mut fern = Fern {
size: 1.0,
growth_rate: 0.001
};
run_simulation(&mut fern, 1000);
println!("final fern size: {}", fern.size);
}
A função main é aquela que separamos anteriormente.
Acrescentamos uma declaração use de alguns itens do crate fern_sim,
Fern e run_simulation. Em outras palavras, estamos utilizando esse crate
como uma biblioteca.
Como colocamos esse arquivo em src/bin, Cargo compilará a
biblioteca fern_sim e esse programa na próxima vez que executarmos
cargo build. Podemos executar o programa efern utilizando cargo run --
bin efern. Eis como fica o código, utilizando --verbose para mostrar os
comandos que Cargo está executando:
$ cargo build --verbose
Compiling fern_sim v0.1.0 (file:///.../fern_sim)
Running `rustc src/lib.rs --crate-name fern_sim --crate-type lib ...`
Running `rustc src/bin/efern.rs --crate-name efern --crate-type bin ...`
$ cargo run --bin efern --verbose
Fresh fern_sim v0.1.0 (file:///.../fern_sim)
Running `target/debug/efern`
final fern size: 2.7169239322355985
Ainda não tivemos que fazer nenhuma alteração em Cargo.toml,
porque, novamente, o padrão do Cargo é examinar seus arquivos de
origem e descobrir as coisas. Ele automaticamente trata arquivos .rs
em src/bin como programas extras para compilar.
Também podemos compilar programas maiores no diretório src/bin
utilizando subdiretórios. Suponha que desejamos fornecer um
segundo programa que desenha uma samambaia na tela, mas o
código de desenho é grande e modular, portanto tem seu próprio
arquivo. Podemos dar ao segundo programa seu próprio
subdiretório:
fern_sim/
├── Cargo.toml
└── src/
└── bin/
├── efern.rs
└── draw_fern/
├── main.rs
└── draw.rs
Isso tem a vantagem de permitir que binários maiores tenham seus
próprios submódulos sem sobrecarregar o código da biblioteca ou o
diretório src/bin.
Naturalmente, agora que fern_sim é uma biblioteca, também temos
outra opção. Poderíamos ter colocado esse programa em seu próprio
projeto isolado, em um diretório completamente separado, com sua
própria listagem de Cargo.toml fern_sim como uma dependência:
[dependencies]
fern_sim = { path = "../fern_sim" }
Talvez seja isso que você fará para outros programas de simulação
de samambaias no futuro. O diretório src/bin é ideal para programas
simples como efern e draw_fern.
Atributos
Qualquer item em um programa Rust pode ser decorado com
atributos. Atributos são a sintaxe genérica do Rust para escrever
diversas instruções e avisos para o compilador. Por exemplo,
suponha que você esteja recebendo este aviso:
libgit2.rs: warning: type `git_revspec` should have a camel case name
such as `GitRevspec`, #[warn(non_camel_case_types)] on by default
Mas você escolheu esse nome por um motivo e gostaria que o Rust
parasse de falar sobre isso. Você pode desativar o aviso adicionando
um atributo #[allow] no tipo:
#[allow(non_camel_case_types)]
pub struct git_revspec {
...
}
A compilação condicional é outro recurso escrito utilizando um
atributo, ou seja, #[cfg]:
// Só inclua esse módulo no projeto se estivermos compilando para Android
#[cfg(target_os = "android")]
mod mobile;
A sintaxe completa de #[cfg] é especificada no Rust Reference
(https://oreil.ly/F7gqB); as opções mais utilizadas estão listadas na
Tabela 8.2.
Tabela 8.2: Opções de #[cfg] mais comumente utilizadas
Opção de #[cfg(...)] Habilitada quando
Test Os testes estão ativados (compilando com cargo test ou rustc --
test).
debug_assertions As asserções de depuração estão habilitadas (normalmente em
compilações não otimizadas).
Unix Compilando para Unix, incluindo macOS.
Windows Compilando para Windows.
target_pointer_width Visando a uma plataforma de 64 bits. O outro valor possível é "32".
= "64"
target_arch = Segmentação x86-64 em particular. Outros valores: "x86", "arm",
"x86_64" "aarch64", "powerpc", "powerpc64", "mips".
target_os = "macos" Compilando para macOS. Outros valores: "windows", "ios",
"android", "linux", "freebsd", "openbsd", "netbsd", "dragonfly".
feature = "robots" O recurso definido pelo usuário chamado "robots" está ativado
(compilando com cargo build --feature robots ou rustc --cfg
feature='"robots"'). As características são declaradas na Seção
[features] de Cargo.toml (https://oreil.ly/IfEpj).
not(A) A não é satisfeita. Para fornecer duas implementações diferentes de
uma função, marque uma delas com #[cfg(X)] e a outra com #
[cfg(not(X))].
all(A,B) Ambas, A e B, são satisfeitas (o equivalente a &&).
any(A,B) Qualquer A ou B é satisfeita (o equivalente a ||).
fn main() {
// Eu me pergunto qual código do Rust real
// substituiria este uso de assert_eq!
trace_macros!(true);
assert_eq!(10*10*10 + 9*9*9, 12*12*12 + 1*1*1);
trace_macros!(false);
}
Com o tempo, a equipe do Rust às vezes estabiliza um recurso
experimental para que se torne uma parte padrão da linguagem. O
atributo #![feature] então se torna supérfluo e o Rust gera um aviso
aconselhando você a removê-lo.
Testes e documentação
Como vimos em “Escrevendo e executando testes unitários”, na
página 30, uma estrutura de testes unitários simples é incorporada
ao Rust. Testes são funções comuns marcadas com o atributo #[test]:
#[test]
fn math_works() {
let x: i32 = 1;
assert!(x.is_positive());
assert_eq!(x + 1, 2);
}
cargo test executa todos os testes em seu projeto:
$ cargo test
Compiling math_test v0.1.0 (file:///.../math_test)
Running target/release/math_test-e31ed91ae51ebf22
running 1 test
test math_works ... ok
#[test]
fn trig_works() {
use std::f64::consts::PI;
assert!(roughly_equal(PI.sin(), 0.0));
}
Em compilações que omitem o código de teste, roughly_equal parece
não utilizado e o Rust reclamará:
$ cargo build
Compiling math_test v0.1.0 (file:///.../math_test)
warning: function is never used: `roughly_equal`
|
7 | / fn roughly_equal(a: f64, b: f64) -> bool {
8|| (a - b).abs() < 1e-6
9||}
| |_^
|
= note: #[warn(dead_code)] on by default
Portanto, a convenção, quando seus testes se tornam substanciais o
suficiente para exigir código de suporte, é colocá-los em um módulo
tests e declarar o módulo inteiro como somente teste utilizando o
atributo #[cfg]:
#[cfg(test)] // inclua este módulo apenas ao testar
mod tests {
fn roughly_equal(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}
#[test]
fn trig_works() {
use std::f64::consts::PI;
assert!(roughly_equal(PI.sin(), 0.0));
}
}
O mecanismo de teste do Rust utiliza vários threads para executar
vários testes ao mesmo tempo, um bom benefício colateral de seu
código Rust ser thread-safe por padrão. Para desativar isso, execute
um único teste, cargo test testname, ou excute cargo test -- --test-threads 1. (O
primeiro -- assegura que cargo test passa a opção --test-threads até o
executável de teste.) Isso significa que, tecnicamente, o programa
Mandelbrot que mostramos no Capítulo 2 não era o segundo
programa multithread daquele capítulo, mas o terceiro! A execução
de cargo test em “Escrevendo e executando testes unitários”, na
página 30, foi o primeiro.
Normalmente, o mecanismo de teste mostra apenas a saída dos
testes que falharam. Para mostrar a saída dos testes que passaram
no teste, execute:
cargo test -- --no-capture.
Testes de integração
Seu simulador de samambaia continua a crescer. Você decidiu
colocar toda a funcionalidade principal em uma biblioteca que pode
ser utilizada por vários executáveis. Seria bom ter alguns testes
vinculados à biblioteca da mesma forma que um usuário final faria,
utilizando fern_sim.rlib como crate externo. Além disso, você tem
alguns testes que começam carregando uma simulação salva de um
arquivo binário e é estranho ter esses arquivos de teste grandes em
seu diretório src. Os testes de integração ajudam com esses dois
problemas.
Os testes de integração são arquivos .rs que vivem em um diretório
de testes no mesmo diretório src do seu projeto. Quando você
executa cargo test, Cargo compila cada teste de integração como um
crate independente e separado, vinculado à sua biblioteca e ao
mecanismo de teste Rust. Eis um exemplo:
// tests/unfurl.rs - os brotos da samambaia se desenrolam à luz do sol
use fern_sim::Terrarium;
use std::time::Duration;
#[test]
fn test_fiddlehead_unfurling() {
let mut world = Terrarium::load("tests/unfurl_files/fiddlehead.tm");
assert!(world.fern(0).is_furled());
let one_hour = Duration::from_secs(60 * 60);
world.apply_sunlight(one_hour);
assert!(world.fern(0).is_fully_unfurled());
}
Os testes de integração são valiosos em parte porque eles veem seu
crate de fora, assim como um usuário faria. Eles testam a API
pública do crate.
cargo test executa testes unitários e testes de integração. Para
executar apenas os testes de integração em um determinado
arquivo – por exemplo, tests/unfurl.rs –, utilize o comando cargo test --
test unfurl.
Documentação
O comando cargo doc cria documentação HTML para sua biblioteca:
$ cargo doc --no-deps --open
Documenting fern_sim v0.1.0 (file:///.../fern_sim)
A opção --no-deps diz ao Cargo que gere documentação apenas para o
próprio fern_sim, e não para todos os crates de que depende.
A opção --open diz ao Cargo que abra a documentação em seu
navegador posteriormente.
Você pode ver o resultado na Figura 8.2. Cargo salva os novos
arquivos de documentação em target/doc. A página inicial é
target/doc/fern_sim/index.html.
Figura 8.2: Exemplo de documentação gerada por rustdoc.
A documentação é gerada a partir dos recursos públicos de sua
biblioteca, além de qualquer comentários do documento que você
anexou a eles. Já vimos alguns comentários de documentos neste
capítulo. Parecem comentários:
/// Simula a produção de um esporo por meiose
pub fn produce_spore(factory: &mut Sporangium) -> Spore {
...
}
Mas, quando o Rust vê comentários que começam com três barras,
ele os trata como um atributo #[doc]. O Rust trata o exemplo anterior
exatamente da mesma forma:
#[doc = "Simulate the production of a spore by meiosis."]
pub fn produce_spore(factory: &mut Sporangium) -> Spore {
...
}
Quando você compila uma biblioteca ou binário, esses atributos não
mudam nada, mas, quando você gera a documentação, os
comentários do documento sobre os recursos públicos são incluídos
na saída.
Da mesma forma, os comentários que começam com //! são tratados
como atributos #![doc] e são anexados ao recurso envolvente,
geralmente um módulo ou crate. Por exemplo, seu arquivo
fern_sim/src/lib.rs pode começar assim:
//! Simula o crescimento de samambaias, desde o nível de
//! células individuais para cima
O conteúdo de um comentário de documento é tratado como
Markdown, uma notação abreviada para formatação HTML simples.
Os asteriscos são utilizados para *itálico* e **negrito**, uma linha em
branco é tratada como uma quebra de parágrafo e assim por diante.
Você também pode incluir tags HTML, que são copiadas literalmente
na documentação formatada.
Um recurso especial dos comentários do documento no Rust é que
os links Markdown podem utilizar caminhos de itens do Rust, como
leaves::Leaf, em vez de URLs relativos, para indicar o que referenciam.
Cargo procurará o que o caminho referencia e o substituirá por um
link no lugar certo na página de documentação certa. Por exemplo, a
documentação gerada a partir desse código está vinculada às
páginas de documentação para VascularPath, Leaf e Root:
/// Cria e retorne um [`VascularPath`] que representa o caminho dos
/// nutrientes do dado [`Root`][r] para o dado [`Leaf`](leaves::Leaf).
///
/// [r]: roots::Root
pub fn trace_path(leaf: &leaves::Leaf, root: &roots::Root) -> VascularPath {
...
}
Você também pode adicionar aliases de pesquisa para facilitar a
localização de coisas utilizando o recurso de pesquisa integrado.
Pesquisar por “path” ou “route” na documentação desse crate levará
a VascularPath:
#[doc(alias = "route")]
pub struct VascularPath {
...
}
Para blocos de documentação mais longos ou para simplificar seu
fluxo de trabalho, você pode incluir arquivos externos em sua
documentação. Por exemplo, se o seu repositório de arquivos
README.md contém o mesmo texto que você gostaria de utilizar
como documentação de nível superior do seu crate, você pode
colocá-lo na parte superior de lib.rs ou main.rs:
#![doc = include_str!("../README.md")]
Você pode utilizar `backticks` para definir trechos de código no meio
do texto. Na saída, esses trechos serão formatados em uma fonte de
largura fixa. Exemplos de código maiores podem ser adicionadas
recuando quatro espaços:
/// Um bloco de código em um comentário de documento:
///
/// if samples::everything().works() {
/// println!("ok");
/// }
Você também pode utilizar blocos de código protegidos por
Markdown. Isso tem exatamente o mesmo efeito:
/// Outro trecho, o mesmo código, mas escrito de forma diferente:
///
/// ```
/// if samples::everything().works() {
/// println!("ok");
/// }
/// ```
Seja qual for o formato que você utiliza, uma coisa interessante
acontece quando você inclui um bloco de código em um comentário
de documento. O Rust transforma-o automaticamente em um teste.
Doc-Tests
Quando você executa testes em um crate de biblioteca Rust, o Rust
verifica se todo o código que aparece em sua documentação
realmente é executado e funciona. Ele faz isso pegando cada bloco
de código que aparece em um comentário de documento,
compilando-o como um crate executável separado, vinculando-o à
sua biblioteca e executando-o.
Eis um exemplo independente de um doc-test. Crie um novo projeto
executando cargo new --lib ranges (o flag --lib informa ao Cargo que
estamos criando um crate de biblioteca, não um crate executável) e
coloque o seguinte código em ranges/src/lib.rs:
use std::ops::Range;
/// Retorna verdadeiro se dois intervalos se sobrepõem.
///
/// assert_eq!(ranges::overlap(0..7, 3..10), true);
/// assert_eq!(ranges::overlap(1..5, 101..105), false);
///
/// Se um dos intervalos estiver vazio, eles não serão considerados sobrepostos.
///
/// assert_eq!(ranges::overlap(0..0, 0..10), false);
///
pub fn overlap(r1: Range<usize>, r2: Range<usize>) -> bool {
r1.start < r1.end && r2.start < r2.end &&
r1.start < r2.end && r2.start < r1.end
}
Os dois pequenos blocos de código no comentário do documento
aparecem na documentação gerada por cargo doc, como mostra a
Figura 8.3.
running 2 tests
test overlap_0 ... ok
test overlap_1 ... ok
Especificando dependências
Vimos uma maneira de dizer ao Cargo onde obter o código-fonte dos
crates dos quais seu projeto depende: pelo número da versão.
image = "0.13"
Existem várias maneiras de especificar dependências e algumas
coisas bastante sutis que você pode querer dizer sobre quais versões
utilizar, então vale a pena gastar algumas páginas nisso.
Em primeiro lugar, você pode querer utilizar dependências que não
são publicadas no crates.io. Uma maneira de fazer isso é
especificando uma URL e uma revisão do repositório Git:
image = { git = "https://github.com/Piston/image.git", rev = "528f19c" }
Esse crate específico é de código aberto, hospedado no GitHub, mas
você pode facilmente apontar para um repositório Git privado
hospedado em sua rede corporativa. Como mostrado aqui, você
pode especificar para utilizar rev, tag ou branch. (Todas essas são
maneiras de dizer ao Git qual revisão do código-fonte verificar.)
Outra alternativa é especificar um diretório que contenha o código-
fonte do crate:
image = { path = "vendor/image" }
Isso é conveniente quando sua equipe tem um único repositório de
controle de versão que contém o código-fonte de vários crates ou
talvez todo o grafo de dependências. Cada crate pode especificar
suas dependências utilizando caminhos relativos.
Ter esse nível de controle sobre suas dependências é poderoso. Se
você decidir que qualquer um dos crates de código aberto que você
utiliza não é exatamente do seu agrado, você pode bifurcá-lo
trivialmente: basta clicar no botão Fork no GitHub e alterar uma
linha no seu arquivo Cargo.toml. Seu próximo cargo build utilizará
perfeitamente o fork do crate em vez da versão oficial.
Versões
Quando você escreve algo como image = "0.13.0" em seu arquivo
Cargo.toml, Cargo interpreta isso de forma bastante vaga. Ele utiliza
a versão mais recente do image que é considerado compatível com a
versão 0.13.0.
As regras de compatibilidade são adaptadas do Versionamento
Semântico (http://semver.org).
• Um número de versão que começa com 0.0 é tão cru que Cargo
nunca assume que é compatível com qualquer outra versão.
• Um número de versão que começa com 0.x, em que x é diferente
de zero, é considerado compatível com outras versões pontuais na
série 0.x. Especificamos image versão 0.6.1, mas Cargo
utilizaria 0.6.3 se disponível. (Isso não é o que o padrão de versão
semântica diz sobre números de versão 0.x, mas a regra provou
ser muito útil para ser deixada de fora.)
• Quando um projeto atinge 1.0, apenas novas versões principais
quebram a compatibilidade. Portanto, se você solicitar a
versão 2.0.1, Cargo poderá utilizar a versão 2.17.99, mas não
a 3.0.
Os números de versão são flexíveis por padrão porque, caso
contrário, o problema de qual versão utilizar se tornaria rapidamente
restrito. Suponha que uma biblioteca, libA, utilizava num = "0.1.31"
enquanto outra, libB, utilizava num = "0.1.29". Se os números de versão
exigissem correspondências exatas, nenhum projeto poderia utilizar
essas duas bibliotecas juntas. Permitir que Cargo utilize qualquer
versão compatível é um padrão muito mais prático.
Ainda assim, projetos diferentes têm necessidades diferentes
quando se trata de dependências e controle de versão. Você pode
especificar uma versão exata ou intervalo de versões utilizando
operadores, conforme ilustrado na Tabela 8.3.
Tabela 8.3: Especificando versões em um arquivo Cargo.toml
Linha Cargo.toml Significado
image = "=0.10.0" Use apenas a versão exata 0.10.0
image = ">=1.0.5" Use 1.0.5 ou qualquer versão superior (mesmo 2.9, se estiver
disponível)
image = ">1.0.5 Use uma versão superior a 1.0.5, mas inferior a 1.1.9
<1.1.9"
image = "<=2.7.10" Use qualquer versão até 2.7.10
Outra especificação de versão que você verá ocasionalmente é o
curinga *. Isso informa ao Cargo que qualquer versão serve. A
menos que algum outro arquivo Cargo.toml contenha uma restrição
mais específica, Cargo utilizará a versão mais recente disponível. A
documentação de Cargo em doc.crates.io (https://oreil.ly/gI1Lq)
abrange especificações de versão com mais detalhes.
Observe que as regras de compatibilidade significam que os
números de versão não podem ser escolhidos apenas por motivos de
marketing. Eles realmente significam algo. São um contrato entre os
mantenedores de um crate e seus usuários. Se você mantiver um
crate que está na versão 1.7 e decidir remover uma função ou fazer
qualquer outra alteração que não seja totalmente compatível com
versões anteriores, deve aumentar o número da versão para 2.0. Se
fosse chamá-lo de 1.8, estaria afirmando que a nova versão é
compatível com 1.7 e seus usuários podem se deparar com
compilações quebradas.
Cargo.lock
Os números da versão em Cargo.toml são deliberadamente flexíveis,
mas não queremos que Cargo nos atualize para as versões mais
recentes da biblioteca toda vez que fizermos build (construímos).
Imagine estar no meio de uma intensa sessão de depuração quando
de repente cargo build atualiza você para uma nova versão de uma
biblioteca. Isso pode ser incrivelmente perturbador. Qualquer
mudança no meio da depuração é ruim. Na verdade, quando se trata
de bibliotecas, nunca é um bom momento para uma mudança
inesperada.
Cargo possui, portanto, um mecanismo integrado para evitar isso.
Na primeira vez que você cria um projeto, Cargo gera um arquivo
Cargo.lock que registra a versão exata de cada crate utilizada.
Compilações posteriores consultarão esse arquivo e continuarão a
utilizar as mesmas versões. Cargo atualiza para versões mais
recentes somente quando você solicitar, aumentando manualmente
o número da versão em seu arquivo Cargo.toml ou executando cargo
update:
$ cargo update
Updating registry `https://github.com/rust-lang/crates.io-index`
Updating libc v0.2.7 -> v0.2.11
Updating png v0.4.2 -> v0.4.3
cargo updateapenas atualiza para as versões mais recentes
compatíveis com o que você especificou em Cargo.toml. Se você
especificou image = "0.6.1" e deseja atualizar para a versão 0.10.0, terá
de mudar isso em Cargo.toml. Na próxima vez que você compilar,
Cargo atualizará para a nova versão da biblioteca image e armazenará
o novo número de versão em Cargo.lock.
O exemplo anterior mostra Cargo atualizando dois crates
hospedados em crates.io. Algo muito semelhante acontece com as
dependências armazenadas no Git. Suponha que nosso arquivo
Cargo.toml contenha isto:
image = { git = "https://github.com/Piston/image.git", branch = "master" }
não extrairá novas alterações do repositório Git se perceber
cargo build
que temos um arquivo Cargo.lock. Em vez disso, ele lê Cargo.lock e
utiliza a mesma revisão da última vez. Mas cargo update vai puxar de
master para que nossa próxima compilação utilize a revisão mais
recente.
Cargo.lock é gerado automaticamente para você e você
normalmente não o editará manualmente. Contudo, se o seu projeto
for um executável, você deve adicionar Cargo.lock ao controle de
versão. Dessa forma, todos que criarem seu projeto obterão
consistentemente as mesmas versões. A história do seu arquivo
Cargo.lock vai registrar suas atualizações de dependência.
Se o seu projeto é uma biblioteca Rust comum, não se preocupe em
comitar Cargo.lock. Os usuários posteriores da sua biblioteca terão
arquivos Cargo.lock que contêm informações de versão para todo o
grafo de dependências; eles vão ignorar o arquivo Cargo.lock da sua
biblioteca. No caso raro de seu projeto ser uma biblioteca
compartilhada (ou seja, a saída é um arquivo .dll, .dylib ou .so), não
existe tal usuário cargo posterior e você deve, portanto, comitar
Cargo.lock.
Os especificadores de versão flexíveis de Cargo.toml facilitam o uso
de bibliotecas Rust em seu projeto e maximizam a compatibilidade
entre as bibliotecas. O controle de compilação de Cargo.lock oferece
suporte a compilações consistentes e reproduzíveis em todas as
máquinas. Juntos, eles percorrem um longo caminho para ajudá-lo a
evitar o inferno das dependências.
Publicando crates em crates.io
Você decidiu publicar sua biblioteca de simulação de samambaia
como software de código aberto. Parabéns! Essa parte é fácil.
Primeiro, certifique-se de que a Cargo possa empacotar o crate para
você.
$ cargo package
warning: manifest has no description, license, license-file, documentation,
homepage or repository. See http://doc.crates.io/manifest.html#package-metadata
for more info.
Packaging fern_sim v0.1.0 (file:///.../fern_sim)
Verifying fern_sim v0.1.0 (file:///.../fern_sim)
Compiling fern_sim v0.1.0 (file:///.../fern_sim/target/package/fern_sim-0.1.0)
O comando cargo package cria um arquivo (neste caso,
target/package/fern_sim-0.1.0.crate) contendo todos os arquivos-
fonte da sua biblioteca, incluindo Cargo.toml. Esse é o arquivo que
você enviará para crates.io para compartilhar com o mundo. (Você
pode utilizar cargo package --list para ver quais arquivos estão incluídos.)
Cargo então verifica novamente seu trabalho construindo sua
biblioteca a partir do arquivo .crate, assim como seus eventuais
usuários farão.
Cargo adverte que na seção [package] de Cargo.toml estão faltando
algumas informações que serão importantes para usuários
posteriores, como a licença sob a qual você está distribuindo o
código. A URL no aviso é um excelente recurso, então não
explicaremos todos os campos em detalhes aqui. Resumindo, você
pode corrigir o aviso adicionando algumas linhas a Cargo.toml:
[package]
name = "fern_sim"
version = "0.1.0"
edition = "2021"
authors = ["You <[email protected]>"]
license = "MIT"
homepage = "https://fernsim.example.com/"
repository = "https://gitlair.com/sporeador/fern_sim"
documentation = "http://fernsim.example.com/docs"
description = """
Fern simulation, from the cellular level up.
"""
Depois de publicar esse crate em crates.io, qualquer pessoa
que baixar seu crate poderá ver o arquivo Cargo.toml. Então, se o
campo authors contiver um endereço de e-mail que você prefere
manter privado, agora é a hora de alterá-lo.
Outro problema que às vezes surge nesta fase é que seu arquivo
Cargo.toml pode estar especificando a localização de outros crates
por path, como mostrado em “Especificando dependências”, na
página 238:
image = { path = "vendor/image" }
Para você e sua equipe, isso pode funcionar bem. Mas,
naturalmente, quando outras pessoas baixam biblioteca fern_sim, elas
não terão os mesmos arquivos e diretórios em seus computadores
que você. Cargo, portanto, ignora a chave path em bibliotecas
baixadas automaticamente e isso pode causar erros de compilação.
A correção, porém, é direta: se sua biblioteca for publicada em
crates.io, suas dependências também devem estar em crates.io.
Especifique um número de versão em vez de um path:
image = "0.13.0"
Se preferir, você pode especificar tanto um path, que tem precedência
para suas próprias compilações locais, e uma version para todos os
outros usuários:
image = { path = "vendor/image", version = "0.13.0" }
É claro que, nesse caso, é sua responsabilidade garantir que os dois
permaneçam sincronizados.
Por fim, antes de publicar um crate, você precisará fazer login em
crates.io e obter uma chave de API. Essa etapa é simples: assim que
você tiver uma conta no crates.io, sua página “Account Settings”
mostrará um comando cargo login, como este:
$ cargo login 5j0dV54BjlXBpUUbfIj7G9DvNl1vsWW1
Cargo salva a chave em um arquivo de configuração e a chave da
API deve ser mantida em segredo, como uma senha. Portanto,
execute esse comando apenas em um computador que você
controla.
Feito isso, o passo final é executar cargo publish:
$ cargo publish
Updating registry `https://github.com/rust-lang/crates.io-index`
Uploading fern_sim v0.1.0 (file:///.../fern_sim)
Com isso, sua biblioteca se junta a milhares de outras no crates.io.
Espaços de trabalho
À medida que seu projeto continua a crescer, você acaba escrevendo
muitos crates. Eles vivem lado a lado em um único repositório de
origem:
fernsoft/
├── .git/...
├── fern_sim/
│ ├── Cargo.toml
│ ├── Cargo.lock
│ ├── src/...
│ └── target/...
├── fern_img/
│ ├── Cargo.toml
│ ├── Cargo.lock
│ ├── src/...
│ └── target/...
└── fern_video/
├── Cargo.toml
├── Cargo.lock
├── src/...
└── target/...
Do jeito que Cargo funciona, cada crate tem o próprio diretório de
construção, target, que contém uma compilação separada de todas as
dependências desse crate. Esses diretórios de compilação são
completamente independentes. Mesmo que dois crates tenham uma
dependência comum, elas não podem compartilhar nenhum código
compilado. Isso é um desperdício.
Você pode economizar tempo de compilação e espaço em disco
utilizando um workspace do Cargo, uma coleção de crates que
compartilham um diretório de compilação comum e um arquivo
Cargo.lock.
Tudo o que você precisa fazer é criar um arquivo Cargo.toml no
diretório raiz do seu repositório e colocar estas linhas nele:
[workspace]
members = ["fern_sim", "fern_img", "fern_video"]
Aqui fern_sim etc. são os nomes dos subdiretórios que contêm seus
crates. Exclua quaisquer outros arquivos Cargo.lock e diretórios
target que existem nesses subdiretórios.
Depois de fazer isso, cargo build em qualquer crate criará e utilizará
automaticamente um diretório de compilação compartilhado no
diretório raiz (neste caso, fernsoft/target). O comando cargo build --
workspace constrói todas os crates no espaço de trabalho atual.
Cargo test e cargo doc aceitam a --workspace opção também.
impl Queue {
/// Insere um caractere no final de uma fila
pub fn push(&mut self, c: char) {
self.younger.push(c);
}
q.push('0');
q.push('1');
assert_eq!(q.pop(), Some('0'));
q.push('∞');
assert_eq!(q.pop(), Some('1'));
assert_eq!(q.pop(), Some('∞'));
assert_eq!(q.pop(), None);
Simplesmente escrever q.push(...) empresta uma referência mutável
a q, como se você tivesse escrito (&mut q).push(...), já que é isso que os
métodos push de self exigem.
Se um método não precisa modificar seu self, então você pode defini-
lo para obter uma referência compartilhada. Por exemplo:
impl Queue {
pub fn is_empty(&self) -> bool {
self.older.is_empty() && self.younger.is_empty()
}
}
Novamente, a expressão de chamada de método sabe que tipo de
referência deve ser emprestada:
assert!(q.is_empty());
q.push('☉');
assert!(!q.is_empty());
Ou, se um método quiser tomar posse de self, pode receber self por
valor:
impl Queue {
pub fn split(self) -> (Vec<char>, Vec<char>) {
(self.older, self.younger)
}
}
A chamada de método de split se parece com as outras chamadas de
método:
let mut q = Queue { older: Vec::new(), younger: Vec::new() };
q.push('P');
q.push('D');
assert_eq!(q.pop(), Some('P'));
q.push('X');
let (older, younger) = q.split();
// q agora não foi inicializado
assert_eq!(older, vec!['D']);
assert_eq!(younger, vec!['X']);
Mas note que, como split recebe seu self por valor, isso move a Queue
para fora de q, deixando q não inicializada. Como o self de split agora
possui a fila, ele é capaz de mover os vetores individuais para fora
dela e devolvê-los ao chamador.
Às vezes, receber self por valor dessa maneira, ou mesmo por
referência, não é suficiente, então o Rust também permite que você
passe self por meio de tipos de ponteiros inteligentes.
Consts associadas
Outra característica de linguagens como C# e Java, que o Rust
adota em seu sistema de tipos, é a ideia de valores associados a um
tipo, em vez de uma instância específica desse tipo. No Rust, eles
são conhecidos como consts associadas.
Como o nome indica, as consts associadas são valores constantes.
Em geral, são utilizados para especificar valores comumente
utilizados de um tipo. Por exemplo, você pode definir um vetor
bidimensional para uso em álgebra linear com um vetor unitário
associado:
pub struct Vector2 {
x: f32,
y: f32,
}
impl Vector2 {
const ZERO: Vector2 = Vector2 { x: 0.0, y: 0.0 };
const UNIT: Vector2 = Vector2 { x: 1.0, y: 0.0 };
}
Esses valores são associados ao próprio tipo e você pode usá-los
sem referenciar outra instância de Vector2. Assim como as funções
associadas, elas são acessadas nomeando o tipo ao qual estão
associadas, seguido de seu nome:
let scaled = Vector2::UNIT.scaled_by(2.0);
Tampouco uma const associada precisa ser do mesmo tipo que o
tipo ao qual está associada; poderíamos utilizar esse recurso para
adicionar IDs ou nomes a tipos. Por exemplo, se houvesse vários
tipos semelhantes a Vector2, que precisassem ser gravados em um
arquivo e carregados na memória posteriormente, uma const
associada poderia ser utilizada para adicionar nomes ou IDs
numéricos, que poderiam ser gravados ao lado dos dados para
identificar seu tipo:
impl Vector2 {
const NAME: &'static str = "Vector2";
const ID: u32 = 18;
}
Structs genéricos
Nossa definição anterior de Queue é insatisfatória: foi escrita para
armazenar caracteres, mas não há nada em sua estrutura ou em
seus métodos que seja específico para caracteres. Se fôssemos
definir outro struct que contivesse, digamos, valores String, o código
pode ser idêntico, exceto que char seria substituído por String. Isso
seria uma perda de tempo.
Felizmente, structs em Rust podem ser genéricos, o que significa
que a definição deles é um template no qual você pode inserir
qualquer tipo que quiser. Por exemplo, eis uma definição para Queue
que pode conter valores de qualquer tipo:
pub struct Queue<T> {
older: Vec<T>,
younger: Vec<T>
}
Você pode ler o <T> em Queue<T> como “para qualquer tipo de
elemento T...”. Portanto, essa definição diz: “Para qualquer tipo T,
uma Queue<T> são dois campos do tipo Vec<T>”. Por exemplo, em
Queue<String>, T é String e, portanto, older e younger são do tipo
Vec<String>. Em Queue<char>, T é char e obtemos um struct idêntico à
definição específica de char com que começamos. Na verdade, Vec em
si é um struct genérico, definido exatamente dessa maneira.
Nas definições de um struct genérico, os nomes de tipo utilizados
entre os símbolos de menor (<) e maior (>) são chamados de
parâmetros de tipo. Um bloco impl para um struct genérico se parece
com isto:
impl<T> Queue<T> {
pub fn new() -> Queue<T> {
Queue { older: Vec::new(), younger: Vec::new() }
}
...
}
Você pode ler a linha impl<T> Queue<T> como “para qualquer tipo T,
aqui estão algumas funções associadas disponíveis em Queue<T>.”
Então, você pode utilizar o parâmetro de tipo T como um tipo nas
definições de função associadas.
A sintaxe pode parecer um pouco redundante, mas o impl<T> deixa
claro que o bloco impl cobre qualquer tipo T, o que o distingue de um
bloco impl escrito para um tipo específico de Queue, como este:
impl Queue<f64> {
fn sum(&self) -> f64 {
...
}
}
Esse cabeçalho de bloco impl diz: “Eis algumas funções associadas
especificamente para Queue<f64>”. Isso dá a Queue<f64> um método
sum, não disponível em nenhum outro tipo de Queue.
Utilizamos a abreviação do Rust para parâmetros self no código
anterior; escrever Queue<T> em todos os lugares torna-se trabalhoso
e distrativo. Como outra abreviação, cada bloco impl, genérico ou
não, define o parâmetro de tipo especial Self (note o nome CamelCase)
para ser do tipo ao qual estamos adicionando métodos. No código
anterior, Self seria Queue<T>, então podemos abreviar a definição de
Queue::new um pouco mais adiante:
pub fn new() -> Self {
Queue { older: Vec::new(), younger: Vec::new() }
}
Você deve ter notado que, no corpo de new, não precisamos escrever
o parâmetro de tipo na expressão de construção; simplesmente
escrever Queue { ... } foi suficiente. Essa é a inferência de tipo do Rust
em ação: já que há apenas um tipo que funciona para o valor de
retorno dessa função – ou seja, Queue<T> –, o Rust fornece o
parâmetro para nós. Mas você sempre precisará fornecer parâmetros
de tipo em assinaturas de função e definições de tipo. O Rust não
infere estes; em vez disso, utiliza esses tipos explícitos como base a
partir da qual infere os tipos no corpo das funções.
Self também pode ser utilizado dessa forma; poderíamos ter escrito
Self { ... } em seu lugar. Cabe a você decidir qual acha mais fácil de
entender.
Para chamadas de função associadas, pode fornecer o parâmetro de
tipo explicitamente utilizando a notação ::<> (turbopeixe/turbofish):
let mut q = Queue::<char>::new();
Mas, na prática, geralmente pode deixar o Rust descobrir isso para
você:
let mut q = Queue::new();
let mut r = Queue::new();
for i in 1..slice.len() {
if slice[i] < *least { least = &slice[i]; }
if slice[i] > *greatest { greatest = &slice[i]; }
}
Extrema { greatest, least }
}
Aqui, como find_extrema empresta elementos de slice, que tem tempo
de vida 's, o struct Extrema que retornamos também utiliza 's como o
tempo de vida de suas referências. O Rust sempre infere parâmetros
de tempo de vida para chamadas, então chamadas a find_extrema não
precisam mencioná-los:
let a = [0, -3, 0, 15, 48];
let e = find_extrema(&a);
assert_eq!(*e.least, -3);
assert_eq!(*e.greatest, 48);
Como é muito comum que o tipo de retorno utilize o mesmo tempo
de vida como um argumento, o Rust nos permite omitir os tempos
de vida quando há um candidato óbvio. Poderíamos também ter
escrito a assinatura de find_extrema assim, sem mudança de
significado:
fn find_extrema(slice: &[i32]) -> Extrema {
...
}
De certo, poderíamos querer dizer Extrema<'static>, mas isso é bastante
incomum. O Rust fornece uma abreviação para o caso comum.
sum
}
}
Aqui a função new aceita um array de comprimento N e recebe seus
elementos como os coeficientes de um novo valor Polynomial. O
método eval itera pelo intervalo 0..N para encontrar o valor do
polinômio em um determinado ponto x.
Tal como acontece com os parâmetros de tipo e o tempo de vida, o
Rust geralmente pode inferir os valores corretos para parâmetros
constantes:
use std::f64::consts::FRAC_PI_2; // π/2
Mutabilidade interior
Mutabilidade é como qualquer outra coisa: em excesso, causa
problemas, mas muitas vezes você quer só um pouquinho. Por
exemplo, digamos que seu sistema de controle de um robô aranha
tenha um struct central, SpiderRobot, que contém configurações e
identificadores de E/S. Ele é configurado quando o robô inicializa, e
os valores nunca mudam:
pub struct SpiderRobot {
species: String,
web_enabled: bool,
leg_devices: [fd::FileDesc; 8],
...
}
Cada sistema principal do robô é tratado por um struct diferente e
cada um tem um ponteiro de volta para o SpiderRobot:
use std::rc::Rc;
impl SpiderRobot {
/// Grava uma linha no arquivo de log
pub fn log(&self, message: &str) {
let mut file = self.log_file.borrow_mut();
// `writeln!` é como `println!`, mas envia
// saída para o arquivo fornecido
writeln!(file, "{}", message).unwrap();
}
}
A variável file tem tipo RefMut<File>. Ele pode ser utilizado apenas
como uma referência mutável a um File. Para obter detalhes sobre
como gravar em arquivos, consulte o Capítulo 18.
As células são fáceis de utilizar. Ter de chamar .get() e .set() ou .borrow()
e .borrow_mut() é um pouco estranho, mas é apenas o preço que
pagamos por quebrar as regras. A outra desvantagem é menos
óbvia e mais séria: as células – e quaisquer tipos que as contenham
– não são thread-safe. O Rust, portanto, não permitirá que vários
threads os acessem ao mesmo tempo. Descreveremos os tipos de
mutabilidade interna thread-safe no Capítulo 19, quando discutirmos
“Mutex<T>”, na página 596, “Atomics”, na página 604, e “Variáveis
globais”, na página 606.
Quer um struct tenha campos nomeados ou seja do tipo tupla, ele é
uma agregação de outros valores: se eu tiver um struct SpiderSenses,
então tenho um ponteiro Rc para um struct SpiderRobot compartilhado,
e tenho olhos, e tenho um acelerômetro etc. Portanto, a essência de
um struct é a palavra “e”: Eu tenho um X e um Y. Mas e se houvesse
outro tipo de tipo construído em torno da palavra “ou”? Ou seja,
quando você tem um valor desse tipo, teria ora um X ou um Y?
Esses tipos acabam sendo tão úteis que são onipresentes no Rust e
são o assunto do próximo capítulo.
10
capítulo
Enums e padrões
Enums
Enumerações simples no estilo C são diretas:
enum Ordering {
Less,
Equal,
Greater,
}
Isso declara um tipo Ordering com três valores possíveis, chamados
variantes ou construtores: Ordering::Less, Ordering::Equal e Ordering::Greater.
Esse enum específico faz parte da biblioteca padrão, portanto o
código Rust pode importá-lo sozinho:
use std::cmp::Ordering;
use self::Pet::*;
Na memória, os valores de enums no estilo C são armazenados
como inteiros. Ocasionalmente, é útil dizer ao Rust quais números
inteiros utilizar:
enum HttpStatus {
Ok = 200,
NotModified = 304,
NotFound = 404,
...
}
Caso contrário, o Rust atribuirá os números para você, começando
em 0.
Por padrão, o Rust armazena enums no estilo C utilizando o menor
tipo inteiro interno que pode acomodá-los. A maioria cabe em um
único byte:
use std::mem::size_of;
assert_eq!(size_of::<Ordering>(), 1);
assert_eq!(size_of::<HttpStatus>(), 2); // 404 não cabe em um u8
Você pode substituir a escolha de representação na memória do
Rust adicionando um atributo #[repr] para a enumeração. Para
detalhes, consulte “Encontrando representações de dados comuns”,
na página 747.
A conversão de uma enumeração estilo C para um número inteiro é
permitida:
assert_eq!(HttpStatus::Ok as i32, 200);
No entanto, converter na outra direção, do número inteiro para a
enumeração, não é permitido. Ao contrário de C e C++, o Rust
garante que um valor de enum seja apenas um dos valores
especificados na declaração enum. Uma conversão não verificada de
um tipo inteiro para um tipo enum pode quebrar essa garantia,
portanto não é permitida. Você pode escrever sua própria conversão
verificada:
fn http_status_from_u32(n: u32) -> Option<HttpStatus> {
match n {
200 => Some(HttpStatus::Ok),
304 => Some(HttpStatus::NotModified),
404 => Some(HttpStatus::NotFound),
...
_ => None,
}
}
ou utilizar o crate enum_primitive (crates.io/crates/enum_primitive). Ele
contém uma macro que gera automaticamente esse tipo de código
de conversão para você.
Tal como acontece com structs, o compilador vai implementar
recursos como o operador == para você, mas você tem de pedir:
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum TimeUnit {
Seconds, Minutes, Hours, Days, Months, Years,
}
Enums podem ter métodos, assim como structs:
impl TimeUnit {
/// Retorna o substantivo plural para essa unidade de tempo
fn plural(self) -> &'static str {
match self {
TimeUnit::Seconds => "seconds",
TimeUnit::Minutes => "minutes",
TimeUnit::Hours => "hours",
TimeUnit::Days => "days",
TimeUnit::Months => "months",
TimeUnit::Years => "years",
}
}
/// Retorna o substantivo singular para essa unidade de tempo
fn singular(self) -> &'static str {
self.plural().trim_end_matches('s')
}}
Mas isso já é o suficiente para enums no estilo C. O tipo mais
interessante de enum no Rust é aquele cujas variantes contêm
dados. Mostraremos como eles são armazenados na memória, como
torná-los genéricos adicionando parâmetros de tipo e como construir
estruturas de dados complexas a partir de enums.
let three_hours_from_now =
RoughTime::InTheFuture(TimeUnit::Hours, 3);
Enums também podem ter variantes de struct, que contêm campos
nomeados, assim como structs comuns:
enum Shape {
Sphere { center: Point3d, radius: f32 },
Cuboid { corner1: Point3d, corner2: Point3d },
}
Enums na memória
Na memória, enums com dados são armazenados como uma tag de
inteiro pequeno, além de memória suficiente para conter todos os
campos da maior variante. O campo tag é para uso interno do Rust.
Ele informa qual construtor criou o valor e, portanto, quais campos
ele possui.
A partir do Rust 1.56, RoughTime cabe em 8 bytes, conforme mostra a
Figura 10.1.
enum Json {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<Json>),
Object(Box<HashMap<String, Json>>),
}
A explicação dessa estrutura de dados em inglês não pode melhorar
muito em relação ao código Rust. O padrão JSON especifica os
vários tipos de dados que podem aparecer em um documento JSON:
null, valores booleanos, números, strings, arrays de valores JSON e
objetos com chaves de string e valores JSON. O enum Json
simplesmente explica esses tipos.
Esse não é um exemplo hipotético. Uma enumeração muito
semelhante pode ser encontrada em serde_json, uma biblioteca de
serialização para structs Rust que é um dos crates mais baixados em
crates.io.
O Box em volta do HashMap que representa um Object serve apenas
para tornar todos os valores Json mais compactos. Na memória,
valores do tipo Json ocupam quatro palavras de máquina. Valores
String e Vec têm três palavras e o Rust adiciona um byte de tag.
Valores Null e Boolean não têm dados suficientes para utilizar todo esse
espaço, mas todos os valores Json devem ser do mesmo tamanho. O
espaço extra não é utilizado. A Figura 10.2 mostra alguns exemplos
de como os valores Json realmente aparecem na memória.
Um HashMap é maior ainda. Se tivéssemos de deixar espaço para isso
em cada valor Json, eles seriam bem grandes, oito palavras ou mais.
Mas um Box<HashMap> é uma única palavra: é apenas um ponteiro
para dados alocados no heap. Poderíamos tornar Json ainda mais
compacto colocando mais campos em boxes.
Enums genéricos
Enums podem ser genéricos. Dois exemplos da biblioteca padrão
estão entre os tipos de dados mais utilizados na linguagem:
enum Option<T> {
None,
Some(T),
}
Padrões
Lembre-se da definição de nosso tipo RoughTime do início deste
capítulo:
enum RoughTime {
InThePast(TimeUnit, u32),
JustNow,
InTheFuture(TimeUnit, u32),
}
Suponha que você tenha um valor RoughTime e queira exibi-lo em uma
página da web. Você precisa acessar os campos TimeUnit e u32 dentro
do valor. O Rust não permite que você os acesse diretamente,
escrevendo rough_time.0 e rough_time.1, porque, afinal, o valor pode ser
RoughTime::JustNow, que não tem campos. Mas então, como você pode
obter os dados?
Você precisa de uma expressão match:
1 fn rough_time_to_english(rt: RoughTime) -> String {
2 match rt {
3 RoughTime::InThePast(units, count) =>
4 format!("{} {} ago", count, units.plural()),
5 RoughTime::JustNow =>
6 format!("just now"),
7 RoughTime::InTheFuture(units, count) =>
8 format!("{} {} from now", count, units.plural()),
9 }
10 }
match realiza correspondência de padrões; neste exemplo, os padrões
são as partes que aparecem antes do símbolo => nas linhas 3, 5 e 7.
Padrões que combinam os valores se parecem com as expressões
utilizadas para criar valores RoughTime. Isso não é coincidência.
Expressões produzem valores; padrões consomem valores. Os dois
utilizam muito da mesma sintaxe.
Vamos ver o que acontece quando essa expressão match é executada.
Suponha que rt é o valor RoughTime::InTheFuture(TimeUnit::Months, 1). O Rust
primeiro tenta corresponder esse valor com o padrão na linha 3.
Como você pode ver na Figura 10.4, ele não corresponde.
Padrões de referência
Os padrões Rust oferecem suporte a dois recursos para trabalhar
com referências. Os padrões ref emprestam partes de um valor
correspondente. Os padrões & correspondem às referências.
Abordaremos padrões ref primeiro.
A correspondência de um valor não copiável move o valor.
Continuando com o exemplo da conta, esse código seria inválido:
match account {
Account { name, language, .. } => {
ui.greet(&name, &language);
ui.show_settings(&account); // erro: empréstimo de valor movido: `account`
}
}
Aqui, os campos account.name e account.language são movidos para
variáveis locais name e language. O restante de account é dropado. É por
isso que não podemos emprestar uma referência a ele depois.
Se name e language fossem ambos os valores copiáveis, o Rust copiaria
os campos em vez de movê-los e esse código ficaria bem. Mas
suponha que esses são Strings. O que podemos fazer?
Precisamos de um tipo de padrão que empresta valores
correspondentes em vez de movê-los. A palavra-chave ref faz
exatamente isso:
match account {
Account { ref name, ref language, .. } => {
ui.greet(name, language);
ui.show_settings(&account); // ok
}
}
Agora as variáveis locais name e language são referências aos campos
correspondentes em account. Como a account está apenas sendo
emprestada, não consumida, não há problema em continuar
chamando métodos nela.
Você pode utilizar ref mut para emprestar referências mut:
match line_result {
Err(ref err) => log_error(err), // `err` é &Error (ref compartilhada)
Ok(ref mut line) => { // `line` é &mut String (mut ref)
trim_comments(line); // modifique a String no lugar
handle(line);
}
}
O padrão Ok(ref mut line) corresponde a qualquer resultado de sucesso
e toma emprestada uma referência mut ao valor de sucesso
armazenado dentro dela.
O tipo oposto de padrão de referência é o padrão &. Um padrão
começando com & corresponde a uma referência:
match sphere.center() {
&Point3d { x, y, z } => ...
}
Neste exemplo, suponha que sphere.center() retorne uma referência a
um campo privado de sphere, um padrão comum no Rust. O valor
retornado é o endereço de um Point3d. Se o centro está na origem,
então sphere.center() retorna &Point3d { x: 0.0, y: 0.0, z: 0.0 }.
A correspondência de padrões ocorre conforme mostrado na
Figura 10.7.
Guardas de correspondência
À
Às vezes, um braço do match tem condições adicionais que devem ser
atendidas antes de ser considerado uma correspondência. Suponha
que estamos implementando um jogo de tabuleiro com espaços
hexagonais e o jogador apenas clicou para mover uma peça. Para
confirmar que o clique foi válido, podemos tentar algo como isto:
fn check_move(current_hex: Hex, click: Point) -> game::Result<Hex> {
match point_to_hex(click) {
None =>
Err("That's not a game space."),
Some(current_hex) => // tenta corresponder se o usuário clicou no current_hex
// (não funciona: veja a explicação abaixo)
Err("You are already there! You must click somewhere else."),
Some(other_hex) =>
Ok(other_hex)
}
}
Isso falha porque os identificadores em padrões introduzem novas
variáveis. O padrão Some(current_hex) aqui cria uma nova variável local
current_hex, escondendo o argumento current_hex. O Rust emite vários
avisos sobre esse código – em particular, o último braço do match é
inalcançável. Uma maneira de corrigir isso é simplesmente utilizar
uma expressão if no braço de correspondência:
match point_to_hex(click) {
None => Err("That's not a game space."),
Some(hex) => {
if hex == current_hex {
Err("You are already there! You must click somewhere else")
} else {
Ok(hex)
}
}
}
Mas o Rust também fornece guardas de correspondência, condições
extras que devem ser verdadeiras para que um braço de
correspondência seja aplicado, escrito como if CONDITION, entre o
padrão e o token => do braço:
match point_to_hex(click) {
None => Err("That's not a game space."),
Some(hex) if hex == current_hex =>
Err("You are already there! You must click somewhere else"),
Some(hex) => Ok(hex)
}
Se o padrão corresponder, mas a condição for falsa, a
correspondência continua com o próximo braço.
// ... executar algum código somente se uma pesquisa de tabela for bem-sucedida
if let Some(document) = cache_map.get(&id) {
return send_cached_response(document);
}
Visão geral
As enumerações do Rust podem ser novas na programação de
sistemas, mas não são uma ideia nova. Viajando sob vários nomes
que soam acadêmicos, como tipos de dados algébricos, eles são
utilizados em linguagens de programação funcionais há mais de
40 anos. Não se sabe direito por que tão poucas outras linguagens
na tradição C nunca os tiveram. Talvez seja simplesmente porque,
para um projetista de linguagem de programação, combinar
variantes, referências, mutabilidade e segurança de memória é
extremamente desafiador. As linguagens de programação funcional
dispensam a mutabilidade. unions do C, por outro lado, têm variantes,
ponteiros e mutabilidade – mas são tão espetacularmente inseguros
que, mesmo em C, são o último recurso. O borrow checker do Rust
é a mágica que torna possível combinar todos os quatro sem
comprometer.
Programação é processamento de dados. Colocar os dados na forma
certa pode ser a diferença entre um programa pequeno, rápido e
elegante, e um emaranhado lento e gigantesco de fita adesiva e
chamadas de método virtuais.
Esse é o endereço de enums do espaço do problema. Eles são uma
ferramenta de design para colocar os dados na forma certa. Para
casos em que um valor pode ser uma coisa, ou outra coisa, ou talvez
nada, enums são melhores que hierarquias de classes em todos os
eixos: mais rápido, mais seguro, menos código, mais fácil de
documentar.
O fator limitante é a flexibilidade. Os usuários finais de uma
enumeração não podem estendê-la para adicionar novas variantes.
As variantes podem ser adicionadas apenas alterando a declaração
enum. E, quando isso acontece, o código existente quebra. Cada
expressão match que corresponde individualmente a cada variante da
enumeração deve ser revisada – ela precisa de um novo braço para
lidar com a nova variante. Em alguns casos, trocar flexibilidade por
simplicidade é apenas bom senso. Afinal, não se espera que a
estrutura do JSON mude. E, em alguns casos, revisitar todos os usos
de um enum quando ele muda é exatamente o que queremos. Por
exemplo, quando um enum é utilizado em um compilador para
representar os vários operadores de uma linguagem de
programação, adicionar um novo operador deve envolver mexer em
todo o código que manipula os operadores.
Mas às vezes mais flexibilidade é necessária. Para essas situações, o
Rust possui traits, o tópico de nosso próximo capítulo.
11
capítulo
Traits e genéricos
Utilizando traits
Um trait é uma característica que qualquer tipo pode ou não
suportar. Na maioria das vezes, um trait representa uma capacidade:
algo que um tipo pode fazer.
• Um valor que implementa std::io::Write pode escrever bytes.
• Um valor que implementa std::iter::Iterator pode produzir uma
sequência de valores.
• Um valor que implementa std::clone::Clone pode fazer clones de si
mesmo na memória.
• Um valor que implementa std::fmt::Debug pode ser impresso
utilizando println!() com o especificador de formato {:?}.
Esses quatro traits fazem parte da biblioteca padrão do Rust e
muitos tipos padrão os implementam. Por exemplo:
• std::fs::File implementa o trait Write; ele escreve bytes em um
arquivo local. std::net::TcpStream escreve em uma conexão de rede.
Vec<u8> também implementa Write. Cada chamada de .write() sobre
um vetor de bytes acrescenta alguns dados ao final.
• Range<i32> (o tipo de 0..10) implementa o trait Iterator, assim como
alguns tipos de iteradores associados a fatias, tabelas hash e
assim por diante.
• A maioria dos tipos de biblioteca padrão implementa Clone. As
exceções são principalmente tipos como TcpStream que representam
mais do que apenas dados na memória.
• Da mesma forma, a maioria dos tipos de biblioteca padrão
suporta Debug.
Há uma regra incomum sobre métodos de trait: a própria trait deve
estar no escopo. Caso contrário, todos os seus métodos estão
ocultos:
let mut buf: Vec<u8> = vec![];
buf.write_all(b"hello")?; // erro: nenhum método chamado `write_all`
Nesse caso, o compilador imprime uma mensagem de erro amigável
que sugere adicionar use std::io::Write; e de fato isso corrige o
problema:
use std::io::Write;
Objetos trait
Existem duas maneiras de utilizar traits para escrever código
polimórfico no Rust: objetos trait e genéricos. Apresentaremos
primeiro os objetos trait e passaremos aos genéricos na próxima
seção.
O Rust não permite variáveis do tipo dyn Write:
use std::io::Write;
Qual utilizar
A escolha de utilizar objetos trait ou código genérico é sutil. Como
ambos os recursos são baseados em traits, eles têm muito em
comum.
Objetos trait são a escolha certa sempre que você precisar de uma
É
coleção de valores de tipos mistos, todos juntos. É tecnicamente
possível fazer uma salada genérica:
trait Vegetable {
...
}
Métodos padrão
O tipo de escritor Sink que discutimos anteriormente pode ser
implementado em algumas linhas de código. Primeiro, definimos o
tipo:
/// Um Writer que ignora todos os dados que você escreve nele
pub struct Sink;
Sinké um struct vazio, pois não precisamos armazenar nenhum dado
nele. Em seguida, apresentamos uma implementação do trait Write
para Sink:
use std::io::{Write, Result};
/// Trait para valores para os quais você pode enviar HTML
trait WriteHtml {
fn write_html(&mut self, html: &HtmlDocument) -> io::Result<()>;
}
A implementação do trait para todos os escritores o torna um trait
de extensão, adicionando um método a todos os escritores Rust:
/// Você pode escrever HTML em qualquer escritor std::io
impl<W: Write> WriteHtml for W {
fn write_html(&mut self, html: &HtmlDocument) -> io::Result<()> {
...
}
}
A linha impl<W: Write> WriteHtml for W significa “para cada tipo W que
implementa Write, eis uma implementação de WriteHtml para W”.
A biblioteca serde oferece um bom exemplo de como pode ser útil
implementar traits definidos pelo usuário em tipos padrão. serde é
uma biblioteca de serialização. Ou seja, você pode usá-lo para
escrever estruturas de dados do Rust no disco e recarregá-las
posteriormente. A biblioteca define um trait, Serialize, que é
implementado para cada tipo de dados compatível com a biblioteca.
Então, no código-fonte serde, há implementação do código Serialize
para bool, i8, i16, i32, tipos de array e tupla etc., e todas as estruturas
de dados padrão, como Vec e HashMap.
O resultado de tudo isso é que serde adiciona um método .serialize()
para todos esses tipos. Pode ser utilizado assim:
use serde::Serialize;
use serde_json;
Ok(())
}
Dissemos anteriormente que, quando você implementa um trait, o
trait ou o tipo deve ser novo no crate atual. Isso é chamado de regra
órfã. Isso ajuda o Rust a garantir que as implementações de traits
sejam únicas. Seu código não pode impl Write for u8, porque tanto Write
como u8 são definidos na biblioteca padrão. Se o Rust permitisse que
os crates fizessem isso, poderia haver várias implementações de Write
para u8, em crates diferentes, e o Rust não teria uma maneira
razoável de decidir qual implementação utilizar para uma
determinada chamada de método.
(O C++ tem uma restrição de unicidade semelhante: a Regra de
uma definição. No estilo C++ típico, ela não é imposta pelo
compilador, exceto nos casos mais simples e você obtém um
comportamento indefinido se a quebrar.)
Self em traits
Um trait pode utilizar a palavra-chave Self como um tipo. O trait Clone
padrão, por exemplo, se parece com isto (ligeiramente simplificado):
pub trait Clone {
fn clone(&self) -> Self;
...
}
Utilizar Self como o tipo de retorno aqui significa que o tipo de x.clone()
é igual ao tipo de x, seja lá o que for. Se x é um String, então o tipo de
x.clone() é String – não dyn Clone ou qualquer outro tipo clonável.
Da mesma forma, se definirmos esse trait:
pub trait Spliceable {
fn splice(&self, other: &Self) -> Self;
}
com duas implementações:
impl Spliceable for CherryTree {
fn splice(&self, other: &Self) -> Self {
...
}
}
Subtraits
Podemos declarar que um trait é uma extensão de outro trait:
/// Alguém no mundo do jogo, seja o jogador ou algum outro
/// duende, gárgula, esquilo, ogro etc.
trait Creature: Visible {
fn position(&self) -> (i32, i32);
fn facing(&self) -> Direction;
...
}
A frase trait Creature: Visible significa que todas as criaturas são visíveis.
Todo tipo que implementa Creature também deve implementar o trait
Visible:
impl Visible for Broom {
...
}
impl Creature for Broom {
...
}
Podemos implementar os dois traits em qualquer ordem, mas é um
erro implementar Creature para um tipo sem também implementar
Visible. Aqui dizemos que Creature é um subtrait de Visible e que Visible é
um supertrait de Creature.
Os subtraits se assemelham a subinterfaces em Java ou C#, pois os
usuários podem presumir que qualquer valor que implemente um
subtrait também implemente seu supertrait. Mas no Rust, um
subtrait não herda os itens associados de seu supertrait; cada trait
ainda precisa estar no escopo se você quiser chamar seus métodos.
Na verdade, os subtraits do Rust são apenas uma abreviação para
um limite em Self. Uma definição de Creature assim é exatamente
equivalente ao mostrado anteriormente:
trait Creature where Self: Visible {
...
}
str::to_string("hello")
A segunda forma se parece exatamente com uma chamada de
função associada. Isso funciona mesmo que o método to_string aceite
um argumento self. Simplesmente passe self como o primeiro
argumento da função.
Como to_string é um método do trait ToString padrão, existem mais
duas formas que você pode utilizar:
ToString::to_string("hello")
<str as ToString>::to_string("hello")
Todas essas quatro chamadas de método chamam exatamente a
mesma coisa. Na maioria das vezes, você apenas escreverá
value.method().As outras formas são chamadas de método qualificadas.
Elas especificam o tipo ou trait ao qual um método está associado. A
última forma, com os símbolos de menor e maior, especifica ambos:
uma chamada de método totalmente qualificada.
Quando você escreve "hello".to_string(), utilizando o operador ., não diz
exatamente qual método to_string você está chamando. O Rust tem
um algoritmo de pesquisa de método que descobre isso,
dependendo dos tipos, coerções de deref e assim por diante. Com
chamadas totalmente qualificadas, você pode dizer exatamente qual
método deseja e isso pode ajudar em alguns casos estranhos:
• Quando dois métodos têm o mesmo nome. O exemplo dramático
clássico é o Outlaw (fora da lei) com dois métodos .draw()2 de dois
traits diferentes, um para desenhar na tela e outro para interagir
com a lei:
outlaw.draw(); // erro: desenhar na tela ou sacar a pistola?
impl Trait
Como você pode imaginar, combinações de muitos tipos genéricos
podem se tornar confusas. Por exemplo, combinar apenas alguns
iteradores utilizando combinadores da biblioteca padrão rapidamente
transforma seu tipo de retorno em uma monstruosidade:
use std::iter;
use std::vec::IntoIter;
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) ->
iter::Cycle<iter::Chain<IntoIter<u8>, IntoIter<u8>>> {
v.into_iter().chain(u.into_iter()).cycle()
}
Poderíamos facilmente substituir esse tipo de retorno complicado por
um objeto trait:
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> Box<dyn Iterator<Item=u8>> {
Box::new(v.into_iter().chain(u.into_iter()).cycle())
}
Entretanto, assumir o overhead do despacho dinâmico e uma
alocação de heap inevitável toda vez que essa função é chamada
apenas para evitar uma assinatura de tipo feia não parece uma boa
escolha, na maioria dos casos.
O Rust tem um recurso chamado impl Trait concebido precisamente
para essa situação. impl Trait nos permite “apagar” o tipo de um valor
de retorno, especificando apenas o trait ou traits que ele
implementa, sem despacho dinâmico ou alocação de heap:
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> impl Iterator<Item=u8> {
v.into_iter().chain(u.into_iter()).cycle()
}
Agora, em vez de especificar um tipo específico aninhado de
iteradores de struct combinador, a assinatura de cyclical_zip apenas
afirma que ele retorna algum tipo de iterador sobre u8. O tipo de
retorno expressa a intenção da função, em vez de seus detalhes de
implementação.
Isso definitivamente limpou o código e o tornou mais legível, mas
impl Trait é mais do que apenas uma abreviação conveniente. Utilizar
impl Trait significa que você pode alterar o tipo real que está sendo
retornado no futuro, desde que ainda implemente Iterator<Item=u8>, e
qualquer código que chame a função continuará a compilar sem
problemas. Isso fornece muita flexibilidade para os autores da
biblioteca, porque apenas a funcionalidade relevante é codificada na
assinatura de tipo.
Por exemplo, se a primeira versão de uma biblioteca utiliza
iteradores combinadores como anteriormente, mas um algoritmo
melhor para o mesmo processo é descoberto, o autor da biblioteca
pode utilizar diferentes combinadores ou até mesmo criar um tipo
personalizado que implemente Iterator e os usuários da biblioteca
podem obter as melhorias de desempenho sem alterar seu código.
Pode ser tentador utilizar impl Trait para fazer uma versão despachada
estaticamente se aproximar do padrão Factory que é comumente
utilizado em linguagens orientadas a objetos. Por exemplo, você
pode definir um trait assim:
trait Shape {
fn new() -> Self;
fn area(&self) -> f64;
}
Depois de implementá-lo para alguns tipos, você pode querer utilizar
diferentes Shapes dependendo de um valor de tempo de execução,
como uma string que um usuário insere. Isso não funciona com
impl Shape como o tipo de retorno:
fn make_shape(shape: &str) -> impl Shape {
match shape {
"circle" => Circle::new(),
"triangle" => Triangle::new(), // erro: tipos incompatíveis
"shape" => Rectangle::new(),
}
}
Do ponto de vista do chamador, uma função como essa não faz
muito sentido. Impl Trait é uma forma de despacho estático, então o
compilador precisa saber o tipo que está sendo retornado da função
em tempo de compilação para alocar a quantidade certa de espaço
no stack (pilha) e acessar corretamente os campos e métodos desse
tipo. Aqui pode ser Circle, Triangle, ou Rectangle, que podem ocupar
diferentes quantidades de espaço e todos têm diferentes
implementações de area().
É importante observar que o Rust não permite que métodos de traits
utilizem valores de retorno impl Trait. O suporte a isso exigirá algumas
melhorias no sistema de tipos da linguagem. Até que esse trabalho
seja concluído, apenas funções livres e funções associadas a tipos
específicos podem utilizar retornos impl Trait.
impl Trait também pode ser utilizado em funções que recebem
argumentos genéricos. Por exemplo, considere esta função genérica
simples:
fn print<T: Display>(val: T) {
println!("{}", val);
}
É idêntica a esta versão utilizando impl Trait:
fn print(val: impl Display) {
println!("{}", val);
}
Há uma exceção importante. O uso de genéricos permite que os
chamadores da função especifiquem o tipo dos argumentos
genéricos, como print::<i32>(42), enquanto utilizar impl Trait não permite.
Cada argumento impl Trait recebe o próprio parâmetro de tipo
anônimo, então impl Trait para argumentos é limitado apenas às
funções genéricas mais simples, sem relacionamentos entre os tipos
de argumentos.
Consts associadas
Como structs e enums, traits podem ter constantes associadas. Você
pode declarar um trait com uma constante associada, utilizando a
mesma sintaxe de um struct ou enum:
trait Greet {
const GREETING: &'static str = "Hello";
fn greet(&self) -> String;
}
Mas consts associadas em traits têm um poder especial. Assim como
os tipos e funções associados, você pode declará-las, mas não
atribuir um valor a elas:
trait Float {
const ZERO: Self;
const ONE: Self;
}
Em seguida, os implementadores do trait podem definir estes
valores:
impl Float for f32 {
const ZERO: f32 = 0.0;
const ONE: f32 = 1.0;
}
#[test]
fn test_dot() {
assert_eq!(dot(&[1, 2, 3, 4], &[1, 1, 1, 1]), 10);
assert_eq!(dot(&[53.0, 7.0], &[1.0, 5.0]), 88.0);
}
Isso ocasionalmente acontece no Rust: há um período de intensa
discussão com o compilador, no final do qual o código parece
bastante bom, como se tivesse sido fácil de escrever e funciona
lindamente.
O que estamos fazendo aqui é a engenharia reversa dos limites N,
utilizando o compilador para guiar e verificar nosso trabalho. A razão
pela qual foi um pouco trabalhoso é que não havia um único trait
Number na biblioteca padrão que incluísse todos os operadores e
métodos que queríamos utilizar. Acontece que existe um crate de
código aberto popular chamado num que define tal trait! Se
soubéssemos, poderíamos ter adicionado num ao nosso Cargo.toml e
escrito:
use num::Num;
1 N.R.: Os métodos que implementam o Write (escrita) são chamados em inglês de writers.
Daí o termo escritor (writer) em português. Ou seja, um escritor é um código que
implementa o trait Write.
2 N.R.: O verbo to draw em inglês pode significar sacar uma arma ou desenhar. Daí a
metáfora usada pelo autor ao explicar a ambiguidade de .draw() para um Outlaw (fora da
lei, bandido) no contexto de um jogo.
12 capítulo
Sobrecarga de operador
assert_eq!(4.125f32.add(5.75), 9.875);
assert_eq!(10.add(20), 10 + 20);
Eis a definição de std::ops::Add:
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
Em outras palavras, o trait Add<T> é a capacidade de adicionar um
valor T para si mesmo. Por exemplo, se você quiser adicionar valores
i32 e u32 ao seu tipo, seu tipo deve implementar tanto Add<i32> como
Add<u32>. O parâmetro de tipo do trait Rhs assume por padrão o valor
de Self; portanto, se estiver implementando adição entre dois valores
do mesmo tipo, basta escrever Add para esse caso. O tipo Output
associado descreve o resultado da adição.
Por exemplo, para somar valores Complex<i32>, Complex<i32> deve
implementar Add<Complex<i32>>. Como estamos adicionando um tipo a
ele mesmo, apenas escrevemos Add:
use std::ops::Add;
Operadores unários
Além do operador de desreferenciação *, que abordaremos
separadamente em “Deref e DerefMut”, na página 362, o Rust tem
dois operadores unários que você pode personalizar, mostrados na
Tabela 12.2.
Tabela 12.2: Traits internos para operadores unários
Nome do Expressã Expressão
trait o equivalente
std::ops::Ne -x x.neg()
g
std::ops::Not !x x.not()
trait Not {
type Output;
fn not(self) -> Self::Output;
}
Negar um número complexo simplesmente nega cada um de seus
componentes. Veja como podemos escrever uma implementação
genérica de negação para valores Complex:
use std::ops::Neg;
Operadores binários
Os operadores binários aritméticos e de bit a bit do Rust e seus traits
internos correspondentes aparecem na Tabela 12.3.
Tabela 12.3: Traits internos para operadores binários
Expressã Expressão
Categoria Nome do trait
o equivalente
Operadores std::ops::Add x+y x.add(y)
aritméticos std::ops::Sub x-y x.sub(y)
std::ops::Mul x*y x.mul(y)
std::ops::Div x/y x.div(y)
std::ops::Rem x%y x.rem(y)
Operadores bit a bit std::ops::BitAn x&y x.bitand(y)
d
std::ops::BitOr x|y x.bitor(y)
std::ops::BitXor x^y x.bitxor(y)
std::ops::Shl x << y x.shl(y)
std::ops::Shr x >> y x.shr(y)
Comparações de equivalência
Os operadores de igualdade do Rust, == e !=, são atalhos para
chamadas aos métodos std::cmp::PartialEq eq e ne do trait:
assert_eq!(x == y, x.eq(&y));
assert_eq!(x != y, x.ne(&y));
Eis a definição de std::cmp::PartialEq:
trait PartialEq<Rhs = Self>
where
Rhs: ?Sized,
{
fn eq(&self, other: &Rhs) -> bool;
fn ne(&self, other: &Rhs) -> bool {
!self.eq(other)
}
}
Como o método ne tem uma definição padrão, você só precisa
definir eq para implementar o trait PartialEq, então aqui está uma
implementação completa para Complex:
impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, other: &Complex<T>) -> bool {
self.re == other.re && self.im == other.im
}
}
Em outras palavras, para qualquer tipo de componente T que pode
ser comparado por igualdade, isso implementa comparação para
Complex<T>. Supondo que também tenhamos implementado
std::ops::Mul para Complex em algum lugar ao longo da linha, agora
podemos escrever:
let x = Complex { re: 5, im: 2 };
let y = Complex { re: 2, im: 5 };
assert_eq!(x * y, Complex { re: 0, im: 29 });
Implementações de PartialEq são quase sempre da forma mostrada
aqui: comparam cada campo do operando da esquerda com o
campo correspondente da direita. Torna-se logo tedioso escrevê-los
e a igualdade é uma operação comum para suportar; então, se
pedir, o Rust vai gerar uma implementação de PartialEq para você
automaticamente. Basta adicionar PartialEq para o atributo derive da
definição de tipo assim:
#[derive(Clone, Copy, Debug, PartialEq)]
struct Complex<T> {
...
}
A implementação gerada automaticamente pelo Rust é
essencialmente idêntica ao nosso código escrito à mão, comparando
cada campo ou elemento do tipo por vez. O Rust também pode
derivar implementações PartialEq para tipos enum. Naturalmente, cada
um dos valores que o tipo contém (ou pode conter, no caso de um
enum) deve implementar PartialEq.
Ao contrário dos traits aritméticos e bit a bit, que recebem seus
operandos por valor, PartialEq recebe seus operandos por referência.
Isso significa que comparar valores não-Copy como Strings, Vecs ou
HashMaps não faz com que eles sejam movidos, o que seria
problemático:
let s = "d\x6fv\x65t\x61i\x6c".to_string();
let t = "\x64o\x76e\x74a\x69l".to_string();
assert!(s == t); // s e t são apenas emprestados...
Index e IndexMut
Você pode especificar como uma expressão de indexação como a[i]
funciona em seu tipo, implementando os traits std::ops::Index e
std::ops::IndexMut. Arrays suportam o operador [] diretamente, mas em
qualquer outro tipo, a expressão a[i] normalmente é uma abreviação
para *a.index(i), em que index é um método do trait std::ops::Index.
Contudo, se a expressão estiver sendo atribuída ou emprestada
mutavelmente, é uma abreviação para *a.index_mut(i), uma chamada
para o método do trait std::ops::IndexMut.
Eis as definições dos traits:
trait Index<Idx> {
type Output: ?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
assert_eq!(m["十"], 10);
assert_eq!(m["千"], 1000);
Essas expressões de indexação são equivalentes a:
use std::ops::Index;
assert_eq!(*m.index("十"), 10);
assert_eq!(*m.index("千"), 1000);
O tipo associado Output do trait Index especifica que tipo uma
expressão de indexação produz: para o nosso HashMap, o tipo Output
da implementação de Index é i32.
O trait IndexMut estende Index com um método index_mut que utiliza
uma referência mutável para self e retorna uma referência mutável
para um valor Output. O Rust seleciona automaticamente index_mut
quando a expressão de indexação ocorre em um contexto em que é
necessário. Por exemplo, suponha que escrevemos o seguinte:
let mut desserts =
vec!["Howalon".to_string(), "Soan papdi".to_string()];
desserts[0].push_str(" (fictional)");
desserts[1].push_str(" (real)");
Como o método push_str opera em &mut self, essas duas últimas linhas
são equivalentes a:
use std::ops::IndexMut;
(*desserts.index_mut(0)).push_str(" (fictional)");
(*desserts.index_mut(1)).push_str(" (real)");
Uma limitação de IndexMut é que, por projeto, ele deve retornar uma
referência mutável para algum valor. É por isso que você não pode
utilizar uma expressão como m[" 十 "] = 10; para inserir um valor no
HashMap m: a tabela precisaria criar uma entrada para " 十 " primeiro,
com algum valor padrão, e retornar uma referência mutável para
isso. Mas nem todos os tipos têm valores padrão baratos, e alguns
podem ser caros de dropar; seria um desperdício criar tal valor
apenas para ser imediatamente dropado pela atribuição. (Existem
planos para melhorar isso em versões futuras da linguagem.)
O uso mais comum da indexação é para coleções. Por exemplo,
suponha que estamos trabalhando com imagens de bitmap, como as
que criamos no plotter do conjunto de Mandelbrot no Capítulo 2.
Lembre-se de que nosso programa continha um código como este:
pixels[row * bounds.0 + column] = ...;
Seria melhor ter um tipo Image<u8> que age como um array
bidimensional, permitindo acessar pixels sem ter de escrever toda a
aritmética:
image[row][column] = ...;
Para fazer isso, precisaremos declarar um struct:
struct Image<P> {
width: usize,
pixels: Vec<P>,
}
Outros operadores
Nem todos os operadores podem ser sobrecarregados no Rust. A
partir do Rust 1.56, o operador de verificação de erros ? funciona
apenas com Result e alguns outros tipos da biblioteca padrão, mas o
trabalho está em andamento para expandir isso também para tipos
definidos pelo usuário. Da mesma forma, os operadores lógicos && e
|| são limitados apenas a valores booleanos. Os operadores .. e ..=
sempre criam um struct representando os limites do intervalo, o
operador & sempre pega referências emprestadas e o operador =
sempre move ou copia valores. Nenhum deles pode ser
sobrecarregado.
O operador de desreferenciação, *val e o operador ponto para
acessar campos e métodos de chamada, como em val.field e
val.method(), pode ser sobrecarregado utilizando os traits Deref e
DerefMut, que serão abordados no próximo capítulo. (Não os incluímos
aqui porque esses traits fazem mais do que apenas sobrecarregar
alguns operadores.)
O Rust não suporta sobrecarga de operador de chamada de
função, f(x). Em vez disso, quando você precisar de um valor que
possa ser chamado (invocado), normalmente apenas escreverá uma
closure. Explicaremos como isso funciona e abordaremos os traits
especiais Fn, FnMut e FnOnce no Capítulo 14.
println!("before assignment");
a = Appellation { name: "Hera".to_string(), nicknames: vec![] };
println!("at end of block");
}
Quando atribuímos o segundo Appellation para a, o primeiro é dropado
e, quando deixamos o escopo de a, o segundo é dropado. Esse
código imprime o seguinte:
before assignment
Dropping Zeus (AKA cloud collector, king of the gods)
at end of block
Dropping Hera
Uma vez que nossa implementação de std::ops::Drop para Appellation não
faz nada além de imprimir uma mensagem, como, exatamente, sua
memória é limpa? O tipo Vec implementa Drop, dropando cada um de
seus elementos e, em seguida, liberando o buffer alocado no heap
que eles ocupavam. Uma String utiliza um Vec<u8> internamente para
manter seu texto, então String não precisa implementar Drop em si;
isso deixa seu Vec cuidar de liberar os caracteres. O mesmo princípio
se estende a valores Appellation: quando um é dropado, no final é a
implementação Vec de Drop que realmente se encarrega de liberar o
conteúdo de cada uma das strings e, finalmente, liberar o buffer que
contém os elementos do vetor. Quanto à memória que armazena o
valor Appellation em si, ele também tem algum proprietário, talvez uma
variável local ou alguma estrutura de dados, que é responsável por
liberá-lo.
Se o valor de uma variável for movido para outro lugar, de modo que
a variável não seja inicializada quando sair do escopo, o Rust não
tentará dropar essa variável: não há valor nela para dropar.
Esse princípio vale mesmo quando uma variável pode ou não ter seu
valor deslocado, dependendo do fluxo de controle. Em casos como
esse, o Rust acompanha o estado da variável com um sinalizador
invisível indicando se o valor da variável precisa ser dropado ou não:
let p;
{
let q = Appellation { name: "Cardamine hirsuta".to_string(),
nicknames: vec!["shotweed".to_string(),
"bittercress".to_string()] };
if complicated_condition() {
p = q;
}
}
println!("Sproing! What was that?");
Dependendo se complicated_condition retorna true ou false, p ou q acabará
por possuir o Appellation, com a outra variável não inicializada. O local
onde ele cai determina se ele será dropado antes ou depois do
println!, uma vez que q sai do escopo antes do println!, e p depois.
Embora um valor possa ser movido de um lugar para outro, o Rust o
dropa apenas uma vez.
Normalmente, você não precisará implementar std::ops::Drop a menos
que esteja definindo um tipo que possua recursos que o Rust ainda
não conheça. Por exemplo, em sistemas Unix, a biblioteca padrão do
Rust utiliza o seguinte tipo internamente para representar um
descritor de arquivo do sistema operacional:
struct FileDesc {
fd: c_int,
}
O campo fd de um FileDesc é simplesmente o número do descritor de
arquivo que deve ser fechado quando o programa terminar; c_int é
um alias para i32. A biblioteca padrão implementa Drop para FileDesc do
seguinte modo:
impl Drop for FileDesc {
fn drop(&mut self) {
let _ = unsafe { libc::close(self.fd) };
}
}
Aqui, libc::close é o nome Rust para a função close da biblioteca C. O
código Rust pode chamar funções C apenas dentro de blocos unsafe,
então a biblioteca utiliza um aqui.
Se um tipo implementa Drop, não pode implementar o trait Copy. Se
um tipo é Copy, isso significa que a duplicação simples byte por byte
é suficiente para produzir uma cópia independente do valor. Mas
normalmente é um erro chamar o mesmo método drop mais de uma
vez sobre os mesmos dados.
O prelúdio padrão inclui uma função para dropar um valor, drop, mas
sua definição é tudo, menos mágica:
fn drop<T>(_x: T) { }
Em outras palavras, ele recebe seu argumento por valor, tomando
posse do chamador – e depois não faz nada com ele. O Rust dropa o
valor de _x quando sai do escopo, como faria com qualquer outra
variável.
Sized
Um tipo dimensionado (sized type) é aquele cujos valores têm todos
o mesmo tamanho na memória. Quase todos os tipos no Rust são
dimensionados: todo u64 tem oito bytes, toda tupla (f32, f32, f32) tem
doze. Mesmo enums são dimensionados: não importa qual variante
esteja realmente presente, um enum sempre ocupa espaço
suficiente para conter sua maior variante. E, embora um Vec<T>
possua um buffer alocado em heap cujo tamanho pode variar, o
valor de Vec em si é um ponteiro para o buffer, sua capacidade e seu
comprimento; portanto, Vec<T> é um tipo dimensionado.
Todos os tipos dimensionados implementam o trait std::marker::Sized,
que não possui métodos ou tipos associados. O Rust o implementa
automaticamente para todos os tipos aos quais se aplica; você não
pode implementá-lo sozinho. O único uso para Sized é como um limite
para variáveis de tipo: um limite como T: Sized requer que T seja um
tipo cujo tamanho é conhecido em tempo de compilação. Traits
desse tipo são chamados traits marcadores, porque a própria
linguagem Rust os utiliza para marcar certos tipos como tendo
características de interesse.
Entretanto, o Rust também tem alguns tipos não dimensionados
cujos valores não são todos do mesmo tamanho. Por exemplo, o tipo
de fatia de string str (note, sem &) é não dimensionado. Os literais de
string "diminutive" e "big" são referências a fatias str que ocupam dez e
três bytes. Ambos são mostrados na Figura 13.1. Tipos de fatia de
array como [T] (novamente, sem &) também são não dimensionados:
uma referência compartilhada como &[u8] pode apontar para uma
fatia [u8] de qualquer tamanho. Como os tipos str e [T] denotam
conjuntos de valores de tamanhos variados, eles são tipos não
dimensionados.
O outro tipo comum de tipo não dimensionado no Rust é um tipo
dyn, o referente de um objeto trait. Como explicamos em “Objetos
trait”, na página 301, um objeto trait é um ponteiro para algum valor
que implementa uma determinada trait. Por exemplo, os tipos &dyn
std::io::Write e Box<dyn std::io::Write> são ponteiros para algum valor que
implementa o trait Write. O referente pode ser um arquivo ou um
soquete de rede ou algum tipo para o qual você mesmo
implementou Write. Como o conjunto de tipos que implementam Write
é aberto, dyn Write é considerado um tipo não dimensionado: seus
valores têm vários tamanhos.
Figura 13.1: Referências a valores não dimensionados.
O Rust não pode armazenar valores não dimensionados em variáveis
ou passá-los como argumentos. Você só pode lidar com eles por
meio de ponteiros como &str ou Box<dyn Write>, que são eles próprios
dimensionados. Conforme mostrado na Figura 13.1, um ponteiro
para um valor não dimensionado é sempre um ponteiro gordo, duas
palavras de largura: um ponteiro para uma fatia também carrega o
comprimento da fatia, e um objeto trait também carrega um
ponteiro para uma vtable de implementações de método.
Objetos trait e ponteiros para fatias são perfeitamente simétricos.
Em ambos os casos, o tipo carece de informações necessárias para
usá-lo: você não pode indexar um [u8] sem saber seu comprimento,
nem pode invocar um método em um Box<dyn Write> sem conhecer a
implementação de Write adequada ao valor específico a que ela se
refere. E em ambos os casos, o ponteiro gordo preenche as
informações que faltam no tipo, carregando um comprimento ou um
ponteiro vtable. A informação estática omitida é substituída por
informação dinâmica.
Como os tipos não dimensionados são tão limitados, a maioria das
variáveis de tipo genérico deve ser restrita a tipos Sized. Na verdade,
isso é necessário com tanta frequência que é o padrão implícito no
Rust: se você escrever struct S<T> { ... }, o Rust entende que você quer
dizer struct S<T: Sized> { ... }. Se você não quiser restringir T dessa
forma, deve cancelar explicitamente, escrevendo struct S<T: ?Sized> { ...
}. A sintaxe ?Sized é específica para esse caso e significa “não
necessariamente Sized”. Por exemplo, se você escrever struct S<T: ?
Sized> { b: Box<T> }, então o Rust permitirá que escreva S<str> e S<dyn
Write>, em que o box se torna um ponteiro gordo, assim como S<i32>
e S<String>, em que o box é um ponteiro comum.
Apesar de suas restrições, os tipos não dimensionados fazem com
que o sistema de tipos do Rust funcione de maneira mais suave.
Lendo a documentação da biblioteca padrão, você ocasionalmente
encontrará um limite ?Sized sobre uma variável de tipo; isso quase
sempre significa que o tipo fornecido é apenas apontado e permite
que o código associado funcione com fatias e objetos trait, bem
como valores comuns. Quando uma variável de tipo tem o limite ?
Sized, as pessoas costumam dizer que é questionavelmente
dimensionado: pode ser Sized, ou talvez não.
Além de fatias e objetos trait, há mais uma espécie de tipo não
dimensionado. O último campo de um tipo struct (mas apenas o
último) pode ser não dimensionado e tal struct é ele próprio não
dimensionado. Por exemplo, um ponteiro de contagem de referência
Rc<T> é implementado internamente como um ponteiro para o tipo
privado RcBox<T>, que armazena a contagem de referência ao lado
do T. Eis uma definição simplificada de RcBox:
struct RcBox<T: ?Sized> {
ref_count: usize,
value: T,
}
O campo value é o T cujas referências Rc<T> está contando; Rc<T>
cancela a referência a um ponteiro para esse campo. O campo
ref_count contém o contador de referências.
O RcBox real é apenas um detalhe de implementação da biblioteca
padrão e não está disponível para uso público. Mas suponha que
estamos trabalhando com a definição anterior. Você pode utilizar
esse RcBox com tipos dimensionados, como RcBox<String>; o resultado é
um tipo struct dimensionado. Ou pode usá-lo com tipos não
dimensionados, como RcBox<dyn std::fmt::Display> (em que Display é o trait
para tipos que podem ser formatados por println! e macros
semelhantes); RcBox<dyn Display> é um tipo struct não dimensionado.
Você não pode construir um valor RcBox<dyn Display> diretamente. Em
vez disso, primeiro precisa criar um RcBox dimensionado cujo tipo value
implementa Display, como RcBox<String>. O Rust então permite que
converta uma referência &RcBox<String> para uma referência gorda
&RcBox<dyn Display>:
let boxed_lunch: RcBox<String> = RcBox {
ref_count: 1,
value: "lunch".to_string()
};
use std::fmt::Display;
let boxed_displayable: &RcBox<dyn Display> = &boxed_lunch;
Essa conversão acontece implicitamente ao passar valores para
funções, então você pode passar um &RcBox<String> para uma função
que espera um &RcBox<dyn Display>:
fn display(boxed: &RcBox<dyn Display>) {
println!("For your enjoyment: {}", &boxed.value);
}
display(&boxed_lunch);
Isso produziria a seguinte saída:
For your enjoyment: lunch
Clone
O trait std::clone::Clone é para tipos que podem fazer cópias de si
mesmos. Clone é definido da seguinte forma:
trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
O método clone deve construir uma cópia independente de self e
retorná-la. Como o tipo de retorno desse método é Self e as funções
podem não retornar valores não dimensionados, o próprio trait Clone
estende o trait Sized: isso tem o efeito de limitar as implementações
dos tipos Self para que sejam Sized.
A clonagem de um valor geralmente envolve a alocação de cópias de
tudo o que ele possui, portanto um clone pode ser caro, tanto em
tempo quanto em memória. Por exemplo, a clonagem de um
Vec<String> não apenas copia o vetor, mas também copia cada um de
seus elementos String. É por isso que o Rust não apenas clona valores
automaticamente, mas exige que você faça uma chamada de
método explícita. Os tipos de ponteiro contados por referência, como
Rc<T> e Arc<T>, são exceções: a clonagem de um deles simplesmente
incrementa a contagem de referência e fornece a você um novo
ponteiro.
O método clone_from transforma self em uma cópia de source. A
definição padrão de clone_from simplesmente clona source e depois
move este para *self. Isso sempre funciona, mas, para alguns tipos,
existe uma maneira mais rápida de obter o mesmo efeito. Por
exemplo, suponha que s e t sejam Strings. A instrução s = t.clone(); deve
clonar t, dropar o valor antigo de s e, então, mover o valor clonado
para s; isso é uma alocação de heap e uma desalocação de heap.
Mas, se o buffer de heap pertencente ao s original tem capacidade
suficiente para armazenar o conteúdo de t, nenhuma alocação ou
desalocação é necessária: você pode simplesmente copiar o texto
de t para o buffer de s e ajustar o comprimento. No código genérico,
você deve utilizar clone_from sempre que possível para aproveitar as
implementações otimizadas quando presentes.
Se sua implementação de Clone simplesmente aplica clone a cada
campo ou elemento do seu tipo e, em seguida, constrói um novo
valor desses clones, e a definição padrão de clone_from é boa o
suficiente, então o Rust vai implementar isso para você: basta
colocar #[derive(Clone)] acima da sua definição de tipo.
Praticamente todos os tipos na biblioteca padrão que fazem sentido
copiar implementam Clone. Tipos primitivos como bool e i32 o fazem.
Tipos contêineres como String, Vec<T> e HashMap também o fazem. Não
faz sentido copiar alguns tipos, como std::sync::Mutex; aqueles não
implementam Clone. Alguns tipos como std::fs::File podem ser copiados,
mas a cópia pode falhar se o sistema operacional não tiver os
recursos necessários; esses tipos não implementam Clone, uma vez
que clone deve ser infalível. Em vez disso, std::fs::File fornece um
método try_clone, que retorna um std::io::Result<File>, que pode relatar
uma falha.
Copy
No Capítulo 4, explicamos que, para a maioria dos tipos, a atribuição
move os valores, em vez de copiá-los. Mover valores torna muito
mais simples rastrear os recursos que eles possuem. Mas em “Tipos
de cópia: A exceção aos movimentos”, na página 122, apontamos a
exceção: tipos simples que não possuem nenhum recurso podem ser
tipos Copy, em que a atribuição faz uma cópia da origem, em vez de
mover o valor e deixar a origem não inicializada.
Naquele momento, deixamos vago exatamente o que Copy era, mas
agora podemos dizer a você: um tipo é Copy se implementa o trait
marcador std::marker::Copy, que é definido da seguinte forma:
trait Copy: Clone { }
Isso certamente é fácil de implementar para seus próprios tipos:
impl Copy for MyType { }
Mas como Copy é um trait marcador com significado especial para a
linguagem, o Rust permite que um tipo implemente Copy somente se
uma cópia rasa byte por byte copiar tudo o que ele precisa. Tipos
que possuem quaisquer outros recursos, como buffers de heap ou
identificadores do sistema operacional, não podem implementar Copy.
Qualquer tipo que implemente o trait Drop não pode ser Copy. O Rust
presume que, se um tipo precisa de código de limpeza especial, ele
também deve exigir código de cópia especial e, portanto, não pode
ser Copy.
Como com Clone, pode pedir ao Rust que derive Copy para você,
utilizando #[derive(Copy)]. Muitas vezes você verá ambos derivados de
uma só vez, com #[derive(Copy, Clone)].
Pense bem antes de fazer um tipo Copy. Embora isso torne o tipo
mais fácil de utilizar, ele impõe pesadas restrições sobre sua
implementação. Cópias implícitas também podem ser caras.
Explicamos esses fatores em detalhes em “Tipos de cópia: a exceção
aos movimentos”, na página 122.
Deref e DerefMut
Você pode especificar como os operadores de desreferenciação,
como * e ., comportam-se em seus tipos, implementando o traits
std::ops::Deref e std::ops::DerefMut. Tipos de ponteiro como Box<T> e Rc<T>
implementam esses traits para que eles possam se comportar como
os tipos de ponteiro internos do Rust. Por exemplo, se você tiver um
valor Box<Complex> b, então *b referencia valor Complex para o qual b
aponta, e b.re referencia seu componente real. Se o contexto atribui
ou empresta uma referência mutável ao referente, o Rust utiliza o
trait DerefMut (“desreferenciar mutavelmente”); caso contrário, o
acesso somente leitura é suficiente e utiliza Deref.
Os traits são definidos assim:
trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
Default
Alguns tipos têm um valor padrão razoavelmente óbvio: o vetor ou
string padrão está vazio, o número padrão é zero, a Option padrão é
None e assim por diante. Tipos como esses podem implementar o
trait std::default::Default:
trait Default {
fn default() -> Self;
}
O método default simplesmente retorna um novo valor do tipo Self. A
implementação String de Default é simples e direta:
impl Default for String {
fn default() -> String {
String::new()
}
}
Todos os tipos de coleção do Rust – Vec, HashMap, BinaryHeap e assim
por diante – implementam Default, com métodos default que retornam
uma coleção vazia. Isso é útil quando você precisa construir uma
coleção de valores, mas quer deixar seu chamador decidir
exatamente que tipo de coleção construir. Por exemplo, método
partition do trait Iterator divide os valores que o iterador produz em
duas coleções, utilizando uma closure para decidir onde cada valor
vai:
use std::collections::HashSet;
let squares = [4, 9, 16, 25, 36, 49, 64];
let (powers_of_two, impure): (HashSet<i32>, HashSet<i32>)
= squares.iter().partition(|&n| n & (n-1) == 0);
assert_eq!(powers_of_two.len(), 3);
assert_eq!(impure.len(), 4);
A closure |&n| n & (n-1) == 0 utiliza alguma manipulação de bits para
reconhecer números que são potências de dois, e partition utiliza isso
para produzir dois HashSets. Mas, naturalmente, partition não é
específico para HashSets; você pode utilizá-la para produzir qualquer
tipo de coleção que quiser, desde que o tipo de coleção implemente
Default, a fim de produzir uma coleção vazia para começar, e
Extend<T>, para adicionar um T à coleção. String implementa Default e
Extend<char>, então você pode escrever:
let (upper, lower): (String, String)
= "Great Teacher Onizuka".chars().partition(|&c| c.is_uppercase());
assert_eq!(upper, "GTO");
assert_eq!(lower, "reat eacher nizuka");
Outro uso comum de Default é produzir valores padrão para structs
que representam uma grande coleção de parâmetros, a maioria dos
quais você normalmente não precisa alterar. Por exemplo, o crate
glium fornece ligações do Rust com a poderosa e complexa biblioteca
de gráficos OpenGL. O struct glium::DrawParameters inclui 24 campos,
cada um controlando um detalhe diferente de como o OpenGL deve
renderizar alguns gráficos. A função glium draw espera um struct
DrawParameters como um argumento. Como DrawParameters implementa
Default, você pode criar um para passar para draw, mencionando
apenas os campos que deseja alterar:
let params = glium::DrawParameters {
line_width: Some(0.02),
point_size: Some(0.02),
.. Default::default()
};
target.draw(..., ¶ms).unwrap();
Isso chama Default::default() para criar um valor DrawParameters
inicializado com os valores padrão para todos os seus campos e, em
seguida, utiliza a sintaxe .. para structs a fim de criar um novo com
os campos line_width e point_size alterados, prontos para você passar
para target.draw.
Se um tipo T implementa Default, então a biblioteca padrão
implementa Default automaticamente para Rc<T>, Arc<T>, Box<T>,
Cell<T>, RefCell<T>, Cow<T>, Mutex<T> e RwLock<T>. O valor padrão para o
tipo Rc<T>, por exemplo, é um Rc apontando para o valor padrão
para o tipo T.
Se todos os tipos de elemento de um tipo de tupla implementam
Default, o tipo de tupla também o faz, assumindo valor padrão de
cada elemento.
O Rust não implementa implicitamente Default para tipos struct, mas,
se todos os campos de um struct implementarem Default, você pode
implementar Default para o struct utilizando automaticamente #
[derive(Default)].
AsRef e AsMut
Quando um tipo implementa AsRef<T>, isso significa que você pode
emprestar um &T a partir dele de forma eficiente. AsMut é o análogo
para referências mutáveis. Suas definições são as seguintes:
trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}
Assim, por exemplo, Vec<T> implementa AsRef<[T]> e String implementa
AsRef<str>. Você também pode emprestar conteúdo de uma String
como um array de bytes, então String também implementa AsRef<[u8]>.
AsRef é normalmente utilizado para tornar as funções mais flexíveis
nos tipos de argumento que aceitam. Por exemplo, a função
std::fs::File::open é declarada assim:
fn open<P: AsRef<Path>>(path: P) -> Result<File>
O que open realmente quer é um &Path, o tipo que representa um
caminho do sistema de arquivos. Mas, com essa assinatura, open
aceita qualquer coisa da qual se pode emprestar um &Path – isto é,
qualquer coisa que implemente AsRef<Path>. Tais tipos incluem String e
str, os tipos de string da interface do sistema operacional OsString e
OsStr e, naturalmente, PathBuf e Path; consulte a documentação da
biblioteca para obter a lista completa. Isso é o que permite que você
passe literais de string para open:
let dot_emacs = std::fs::File::open("/home/jimb/.emacs")?;
Todas as funções de acesso ao sistema de arquivos da biblioteca
padrão aceitam argumentos de path dessa maneira. Para
chamadores, o efeito se assemelha ao de uma função
sobrecarregada em C++ embora o Rust tenha uma abordagem
diferente para estabelecer quais tipos de argumento são aceitáveis.
Mas isso não pode ser a história completa. Um literal de string é
uma &str, mas o tipo que implementa AsRef<Path> é str, sem um &. E,
como explicamos em “Deref e DerefMut”, na página 186, o Rust não
tenta coerções deref para satisfazer limites de variável de tipo,
portanto elas também não ajudarão aqui.
Felizmente, a biblioteca padrão inclui a implementação geral:
impl<'a, T, U> AsRef<U> for &'a T
where T: AsRef<U>,
T: ?Sized, U: ?Sized
{
fn as_ref(&self) -> &U {
(*self).as_ref()
}
}
Em outras palavras, para quaisquer tipos T e U, se T: AsRef<U>, então
&T: AsRef<U> também é possível: basta seguir a referência e proceder
como antes. Em particular, uma vez que str: AsRef<Path>, então &str:
AsRef<Path> também é possível. Em certo sentido, essa é uma maneira
de obter uma forma limitada de coerção deref na verificação de
limites AsRef em variáveis de tipo.
Você pode supor que, se um tipo implementa AsRef<T>, deve também
implementar AsMut<T>. Mas há casos em que isso não é apropriado.
Por exemplo, mencionamos que String implementa AsRef<[u8]>; isso faz
sentido, pois cada String certamente tem um buffer de bytes que
pode ser útil para acessar como dados binários. Contudo, String
garante ainda que esses bytes sejam uma codificação UTF-8 bem
formada de texto Unicode; se String implementasse AsMut<[u8]>, isso
permitiria que os chamadores alterassem os bytes de String para
qualquer coisa que eles quisessem e você não poderia mais confiar
em uma String para obter UTF-8 bem formado. Só faz sentido para
um tipo implementar AsMut<T> se modificar o dado T não puder violar
as invariantes do tipo.
Embora AsRef e AsMut sejam bastante simples, fornecer traits padrão e
genéricos para conversão de referência evita a proliferação de traits
de conversão mais específicas. Você deve evitar definir seus próprios
traits AsFoo quando poderia apenas implementar AsRef<Foo>.
Borrow e BorrowMut
O trait é semelhante a AsRef: se um tipo implementa
std::borrow::Borrow
Borrow<T>, então seu método borrow empresta eficientemente um &T
dele. Mas Borrow impõe mais restrições: um tipo deve implementar
Borrow<T> somente quando um &T obtém sua chave hash e
comparado da mesma maneira que o valor do qual é emprestado. (O
Rust não impõe isso; é apenas a intenção documentada do trait.)
Isso torna Borrow valioso ao lidar com chaves em tabelas hash e
árvores ou ao lidar com valores que utilizam hash ou que são
comparados por algum outro motivo.
Essa distinção é importante quando emprestamos de Strings, por
exemplo: String implementa AsRef<str>, AsRef<[u8]> e AsRef<Path>, mas
esses três tipos de destino geralmente terão valores de hash
diferentes. Apenas a fatia &str tem garantidamente o mesmo hash da
String equivalente, assim String implementa apenas Borrow<str>.
A definição de Borrow é idêntica à de AsRef; apenas os nomes foram
alterados:
trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
Borrow foi projetado para lidar com uma situação específica com
tabelas hash genéricas e outros tipos de coleção associativa. Por
exemplo, suponha que você tenha um std::collections::HashMap<String, i32>,
mapeando strings para números. As chaves dessa tabela são Strings;
cada entrada possui uma. Qual deve ser a assinatura do método que
procura uma entrada nessa tabela? Eis uma primeira tentativa:
impl<K, V> HashMap<K, V> where K: Eq + Hash
{
fn get(&self, key: K) -> Option<&V> { ... }
}
Isso faz sentido: para procurar uma entrada, você deve fornecer
uma chave do tipo apropriado para a tabela. Mas, nesse caso, K é
String; essa assinatura iria forçá-lo a passar uma String por valor para
cada chamada a get, o que é claramente um desperdício. Você
realmente só precisa de uma referência à chave:
impl<K, V> HashMap<K, V> where K: Eq + Hash
{
fn get(&self, key: &K) -> Option<&V> { ... }
}
Isso é um pouco melhor, mas agora você tem de passar a chave
como um &String; portanto, se quiser procurar uma constante string,
precisará escrever:
hashtable.get(&"twenty-two".to_string())
Isso é ridículo: aloca uma String buffer no heap e copia o texto para
ela, apenas para que possa emprestá-la como uma &String, passá-la
para get e, então, dropá-la.
Isso deve ser bom o suficiente para passar qualquer coisa que
suporte hash e deva ser comparado com nosso tipo de chave; uma
&str deve ser perfeitamente adequada, por exemplo. Então aqui está
a iteração final, que é o que você encontrará na biblioteca padrão:
impl<K, V> HashMap<K, V> where K: Eq + Hash
{
fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
where K: Borrow<Q>,
Q: Eq + Hash
{ ... }
}
Em outras palavras, se você pode pegar emprestada a chave de uma
entrada como &Q e a referência resultante tem o mesmo hash e
pode ser comparada exatamente como ocorreria com a própria
chave, então claramente &Q deve ser um tipo de chave aceitável.
Como a String implementa Borrow<str> e Borrow<String>, essa versão final
de get permite que você passe qualquer uma, &String ou &str, como
uma chave, conforme necessário.
Vec<T> e [T: N] implementam Borrow<[T]>. Cada tipo semelhante a
string permite o empréstimo de seu tipo de fatia correspondente:
String implementa Borrow<str>, PathBuf implementa Borrow<Path> e assim
por diante. E todos os tipos de coleção associativa da biblioteca
padrão utilizam Borrow para decidir quais tipos podem ser passados
para suas funções de pesquisa.
A biblioteca padrão inclui uma implementação geral para que cada
tipo T seja emprestado de si mesmo: T: Borrow<T>. Isso garante que
&K é sempre um tipo aceitável para procurar entradas em um
HashMap<K, V>.
Por conveniência, cada tipo &mut T também implementa Borrow<T>,
retornando uma referência compartilhada &T como sempre. Isso
permite que você passe referências mutáveis para funções de
pesquisa de coleção sem ter de emprestar novamente uma
referência compartilhada, emulando a coerção implícita usual do
Rust de referências mutáveis para referências compartilhadas.
O trait BorrowMut é o análogo de Borrow para referências mutáveis:
trait BorrowMut<Borrowed: ?Sized>: Borrow<Borrowed> {
fn borrow_mut(&mut self) -> &mut Borrowed;
}
As mesmas expectativas descritas para Borrow aplicam-se a BorrowMut
também.
From e Into
Os traits std::convert::From e std::convert::Into representam conversões que
consomem um valor de um tipo e retornam um valor de outro.
Considerando que os traits AsRef e AsMut emprestam uma referência
de um tipo de outro, From e Into tomam posse de seu argumento,
transformam-no e, em seguida, retornam a posse do resultado de
volta ao chamador.
Suas definições são bem simétricas:
trait Into<T>: Sized {
fn into(self) -> T;
}
trait From<T>: Sized {
fn from(other: T) -> Self;
}
A biblioteca padrão implementa automaticamente a conversão trivial
de cada tipo para si mesmo: cada tipo T implementa From<T> e
Into<T>.
Embora os traits simplesmente forneçam duas maneiras de fazer a
mesma coisa, elas se prestam a usos diferentes.
Você geralmente utiliza Into para tornar suas funções mais flexíveis
nos argumentos que aceitam. Por exemplo, se você escrever:
use std::net::Ipv4Addr;
fn ping<A>(address: A) -> std::io::Result<bool>
where A: Into<Ipv4Addr>
{
let ipv4_address = address.into();
...
}
então ping pode aceitar não apenas um Ipv4Addr como argumento,
mas também u32 ou um array [u8; 4], já que esses tipos
convenientemente implementam Into<Ipv4Addr>. (Às vezes é útil tratar
um endereço IPv4 como um único valor de 32 bits ou um array de
4 bytes.) Como a única coisa que ping sabe sobre address é que
implementa Into<Ipv4Addr>, não há necessidade de especificar qual
tipo você deseja ao chamar into; há apenas um que poderia
funcionar, então a inferência de tipo o preenche para você.
Como ocorre com AsRef na seção anterior, o efeito é muito parecido
com o de sobrecarregar uma função em C++. Com a definição de
ping de antes, podemos fazer qualquer uma dessas chamadas:
println!("{:?}", ping(Ipv4Addr::new(23, 21, 68, 141))); // passa um Ipv4Addr
println!("{:?}", ping([66, 146, 219, 98])); // passa um [u8; 4]
println!("{:?}", ping(0xd076eb94_u32)); // passa um u32
O trait From, porém, desempenha um papel diferente. O método from
serve como um construtor genérico para produzir uma instância de
um tipo de algum outro valor único. Por exemplo, em vez de Ipv4Addr
tendo dois métodos nomeados from_array e from_u32, ele simplesmente
implementa From<[u8;4]> e From<u32>, permitindo-nos escrever:
let addr1 = Ipv4Addr::from([66, 146, 219, 98]);
let addr2 = Ipv4Addr::from(0xd076eb94_u32);
Podemos deixar que a inferência de tipo resolva qual implementação
se aplica.
Dada uma implementação de From apropriada, a biblioteca padrão
implementa automaticamente o trait Into correspondente. Ao definir
seu próprio tipo, se ele tiver construtores de argumento único, você
deve escrevê-los como implementações de From<T> para os tipos
apropriados; você receberá as implementações de Into
correspondente de graça.
Como os métodos de conversão from e into assumem a posse de seus
argumentos, uma conversão pode reutilizar os recursos do valor
original para construir o valor convertido. Por exemplo, suponha que
você escreva:
let text = "Beautiful Soup".to_string();
let bytes: Vec<u8> = text.into();
A implementação de Into<Vec<u8>> para String simplesmente pega o
heap do buffer String e o reaproveita, inalterado, como o buffer de
elementos do vetor retornado. A conversão não tem necessidade de
alocar ou copiar o texto. Esse é outro caso em que as
movimentações permitem implementações eficientes.
Essas conversões também fornecem uma boa maneira de relaxar um
valor de um tipo restrito em algo mais flexível, sem enfraquecer as
garantias do tipo restrito. Por exemplo, uma String garante que seu
conteúdo seja sempre UTF-8 válido; seus métodos de mutação são
cuidadosamente restritos para garantir que nada que você possa
fazer introduzirá um UTF-8 inválido. Mas esse exemplo
eficientemente “rebaixa” um String para um bloco de bytes simples
com o qual você pode fazer o que quiser: talvez o comprima ou
combine com outros dados binários que não sejam UTF-8. Como into
toma seu argumento por valor, text não é mais inicializado após a
conversão, o que significa que podemos acessar livremente a antiga
String do buffer sem ser capaz de corromper qualquer existente String.
Contudo, conversões baratas não fazem parte do contrato de Into e
From. Enquanto espera-se que as conversões de AsRef e AsMut sejam
baratas, as conversões From e Into podem alocar, copiar ou processar
o conteúdo do valor. Por exemplo, String implementa From<&str>, que
copia a fatia de string em um novo buffer alocado em heap para a
String. E std::collections::BinaryHeap<T> implementa From<Vec<T>>, que
compara e reordena os elementos de acordo com os requisitos de
seu algoritmo.
O operador ? utiliza From e Into para ajudar a limpar o código em
funções que podem falhar de várias maneiras, convertendo
automaticamente de tipos de erros específicos para gerais quando
necessário.
Por exemplo, imagine um sistema que precisa ler dados binários e
converter parte deles de números de base 10 escritos como texto
UTF-8. Isso significa utilizar std::str::from_utf8 e a implementação de
FromStr para i32, o que pode retornar erros de tipos diferentes.
Supondo que utilizamos os tipos GenericError e GenericResult que
definimos no Capítulo 7 ao discutir o tratamento de erros, o
operador ? fará a conversão para nós:
type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;
fn parse_i32_bytes(b: &[u8]) -> GenericResult<i32> {
Ok(std::str::from_utf8(b)?.parse::<i32>()?)
}
Como a maioria dos tipos de erro, Utf8Error e ParseIntError implementam
o trait Error e a biblioteca padrão nos dá uma cobertura do From impl
para converter de qualquer coisa que implemente Error para um
Box<dyn Error>, o qual ? utiliza automaticamente:
impl<'a, E: Error + Send + Sync + 'a> From<E>
for Box<dyn Error + Send + Sync + 'a> {
fn from(err: E) -> Box<dyn Error + Send + Sync + 'a> {
Box::new(err)
}
}
Isso transforma o que teria sido uma função razoavelmente grande
com duas instruções match em uma única linha.
Antes de From e Into serem adicionados à biblioteca padrão, o código
Rust estava cheio de traits de conversão ad hoc e métodos de
construção, cada um específico a um único tipo. From e Into codificam
as convenções que você pode seguir para tornar seus tipos mais
fáceis de utilizar, pois seus usuários já estão familiarizados com eles.
Outras bibliotecas e a própria linguagem também podem contar com
esses traits como uma maneira canônica e padronizada de codificar
conversões.
From e Into são traits infalíveis – sua API exige que as conversões não
falhem. Infelizmente, muitas conversões são mais complexas do que
isso. Por exemplo, inteiros grandes como i64 podem armazenar
números muito maiores do que i32, e converter um número como
2_000_000_000_000i64 em um i32 não faz muito sentido sem algumas
informações adicionais. Fazer uma conversão bit a bit simples, na
qual os primeiros 32 bits são descartados, geralmente não produz o
resultado que esperamos:
let huge = 2_000_000_000_000i64;
let smaller = huge as i32;
println!("{}", smaller); // -1454759936
Existem muitas opções para lidar com essa situação. Dependendo do
contexto, essa conversão por “encapsulamento (wrapping)” pode ser
apropriada. Por outro lado, aplicações como processamento de sinal
digital e sistemas de controle muitas vezes podem se contentar com
uma conversão de “saturação”, na qual números maiores que o valor
máximo possível são limitados a esse máximo.
TryFrom e TryInto
Como não está claro como essa conversão deve se comportar, o
Rust não implementa From<i64> para i32, ou qualquer outra conversão
entre tipos numéricos que perderiam informações. Em vez disso, i32
implementa TryFrom<i64>. TryFrom e TryInto são os primos que podem
falhar de From e Into e são igualmente recíprocos; implementar TryFrom
significa que TryInto também é implementado.
Suas definições são apenas um pouco mais complexas do que From e
Into.
pub trait TryFrom<T>: Sized {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}
ToOwned
Dada uma referência, a maneira usual de produzir uma cópia com
posse de seu referente é chamar clone, assumindo que o tipo
implementa std::clone::Clone. Mas e se você quiser clonar um &str ou um
&[i32]? O que você provavelmente quer é uma String ou um Vec<i32>,
mas a definição de Clone não permite isso: por definição, a clonagem
de um &T deve sempre retornar um valor do tipo T, e str e [u8] são
não dimensionados; eles nem são tipos que uma função poderia
retornar.
O trait std::borrow::ToOwned fornece uma maneira um pouco mais
flexível de converter uma referência em um valor possuído:
trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;
}
Diferentemente de clone, que deve retornar exatamente Self, to_owned
pode devolver qualquer coisa da qual que você poderia emprestar
um &Self: o tipo Owned deve implementar Borrow<Self>. Você pode
emprestar um &[T] a partir de um Vec<T>, então [T] pode implementar
ToOwned<Owned=Vec<T>>, desde que T implemente Clone, para que
possamos copiar os elementos da fatia para o vetor. De forma
similar, str implementa ToOwned<Owned=String>, Path implementa
ToOwned<Owned=PathBuf> e assim por diante.
1 N.R.: Código de colagem (glue code) é um código que não contribui para nenhuma
funcionalidade do programa, e serve comente para unir diferentes partes de um código
que de outra forma não seriam compatíveis. Fonte: Wikipédia.
capítulo 14
Closures
if (pendingSort)
pendingSort.cancel();
cities.sort_by_key(my_key_fn);
Structs podem ter campos tipados como função. Tipos genéricos
como Vec podem armazenar um grande número de funções, desde
que todas compartilhem o mesmo tipo fn. E o custo de
armazenagem de valores do tipo função é pequeno: um valor do
tipo fn é apenas o endereço de memória do código de máquina da
função, assim como um ponteiro de função em C++.
Uma função pode receber outra função como argumento. Por
exemplo:
/// Dada uma lista de cidades e uma função de teste,
/// retorna quantas cidades passam no teste
fn count_selected_cities(cities: &Vec<City>,
test_fn: fn(&City) -> bool) -> usize
{
let mut count = 0;
for city in cities {
if test_fn(city) {
count += 1;
}
}
count
}
count_selected_cities(
&my_cities,
|city| city.monster_attack_risk > limit); // também ok
Por que nossa primeira tentativa não funcionou? Bem, uma closure
pode ser chamada, mas não é uma fn. A closure |city|
city.monster_attack_risk > limit tem seu próprio tipo que não é um tipo fn.
Na verdade, cada closure que você escreve tem seu próprio tipo,
porque uma closure pode conter dados: valores emprestados ou
roubados de escopos envolventes. Isso pode ser qualquer número
de variáveis, em qualquer combinação de tipos. Portanto, cada
closure tem um tipo ad hoc criado pelo compilador, grande o
suficiente para conter esses dados. Não há duas closures
exatamente do mesmo tipo. Mas toda closure implementa um
trait Fn; a closure em nossos exemplos implementa Fn(&City) -> i64.
Como cada closure tem seu próprio tipo, o código que funciona com
closures geralmente precisa ser genérico, como count_selected_cities. É
um pouco chato escrever os tipos genéricos um de cada vez, mas,
para ver as vantagens desse design, continue lendo.
Desempenho de closure
As closures do Rust são projetadas para serem rápidas: mais rápidas
do que os ponteiros de função, rápidas o suficiente para que você
possa utilizá-las mesmo em códigos sensíveis ao desempenho. Se
você estiver familiarizado com lambdas do C++, descobrirá que as
closures do Rust são tão rápidas e compactas, porém mais seguras.
Na maioria das linguagens, as closures são alocados no heap,
despachadas dinamicamente e liberadas pelo coletor de lixo.
Portanto, criar, chamar e coletar cada uma delas custa um
pouquinho de tempo extra de CPU. Pior ainda, as closures tendem a
descartar o inlining, uma técnica chave que os compiladores utilizam
para eliminar o overhead da chamada de função e habilitar uma
série de outras otimizações. No final das contas, as closures são
lentas o suficiente nessas linguagens para que valha a pena removê-
las manualmente de loops internos apertados1.
As closures do Rust não têm nenhuma dessas desvantagens de
desempenho. Elas não sofrem coleta de lixo. Como tudo no Rust,
elas não são alocadas no heap, a menos que você as coloque em um
Box, Vec ou outro contêiner. E, como cada closure tem um tipo
distinto, sempre que o compilador do Rust souber o tipo de closure
que você está chamando, ele pode incorporar o código para essa
closure específica. Isso torna aceitável o uso de closures em loops
apertados e os programas Rust costumam fazer isso com
entusiasmo, como você verá no Capítulo 15.
A Figura 14.1 mostra como as closures do Rust são dispostas na
memória. Na parte superior da figura, mostramos algumas variáveis
locais que nossas closures vão referenciar: uma string food e uma
enumeração simples weather, cujo valor numérico casualmente é 27.
A closure (a) utiliza ambas as variáveis. Aparentemente, estamos
procurando cidades que tenham tacos e tornados. Na memória, essa
closure parece um pequeno struct contendo referências às variáveis
que ele utiliza.
Observe que ela não contém um ponteiro para seu código! Isso não
é necessário: contanto que o Rust conheça o tipo da closure, ele
saberá qual código executar quando você a chamar.
Closures e segurança
Ao longo deste capítulo até aqui, falamos sobre como o Rust garante
que as closures respeitem as regras de segurança da linguagem
quando elas pegam emprestadas ou movem variáveis do código ao
redor. Mas há algumas outras consequências que não são
exatamente óbvias. Nesta seção, explicaremos um pouco mais sobre
o que acontece quando uma closure dropa ou modifica um valor
capturado.
FnOnce
Vamos tentar uma vez mais, para enganar o Rust e fazê-lo dropar
uma String duas vezes. Dessa vez, utilizaremos esta função genérica:
fn call_twice<F>(closure: F) where F: Fn() {
closure();
closure();
}
Essa função genérica pode receber qualquer closure que implemente
o trait Fn(): isto é, closures que não recebem argumentos e retornam
(). (Assim como com funções, o tipo de retorno pode ser omitido se
for (); Fn() é uma abreviação para Fn() -> ().)
Agora, o que acontece se passarmos nossa closure insegura para
essa função genérica?
let my_str = "hello".to_string();
let f = || drop(my_str);
call_twice(f);
Mais uma vez, a closure vai dropar my_str quando for chamado.
Chamar duas vezes seria um double free. Mas, novamente, o Rust
não se deixa enganar:
error: expected a closure that implements the `Fn` trait, but
this closure only implements `FnOnce`
|
8 | let f = || drop(my_str);
| ^^^^^^^^------^
| | |
| | closure is `FnOnce` because it moves the variable `my_str`
| | out of its environment
| this closure implements `FnOnce`, not `Fn`
9 | call_twice(f);
| ---------- the requirement to implement `Fn` derives from here
Essa mensagem de erro nos diz mais sobre como o Rust lida com
“closures que matam”. Elas poderiam ter sido totalmente banidas da
linguagem, mas às vezes as closures de limpeza são úteis. Então,
em vez disso, o Rust restringe seu uso. Closures que dropam
valores, como f, não têm permissão para ter Fn. Elas não são,
literalmente, uma Fn de forma alguma. Elas implementam um trait
menos poderoso, FnOnce, o trait de closures que podem ser
chamados uma vez.
Na primeira vez que você chama uma closure FnOnce, a closure em si
é consumida. É como se os dois traits, Fn e FnOnce, tivessem sido
definidos assim:
// Pseudocódigo para traits `Fn` e `FnOnce` sem argumentos
trait Fn() -> R {
fn call(&self) -> R;
}
trait FnOnce() -> R {
fn call_once(self) -> R;
}
Assim como uma expressão aritmética como a + b é uma abreviação
para uma chamada de método, Add::add(a, b), o Rust trata closure()
como abreviação para um dos dois métodos de trait mostrados no
exemplo anterior. Para uma closure Fn, closure() expande para
closure.call(). Esse método recebe self por referência, então a closure
não é movida. Mas, se a closure for segura apenas para chamar uma
vez, então closure() expande-se para closure.call_once(). Esse método
recebe self por valor, então a closure é consumida.
É claro que estamos deliberadamente criando problemas aqui
utilizando drop(). Na prática, você entrará nessa situação por
acidente. Isso não acontece com frequência, mas de vez em quando
você escreverá algum código de closure que inadvertidamente utiliza
um valor:
let dict = produce_glossary();
let debug_dump_dict = || {
for (key, value) in dict { // ops!
println!("{:?} - {:?}", key, value);
}
};
Então, quando você chamar debug_dump_dict() mais de uma vez,
receberá uma mensagem de erro como esta:
error: use of moved value: `debug_dump_dict`
|
19 | debug_dump_dict();
| ----------------- `debug_dump_dict` moved due to this call
20 | debug_dump_dict();
| ^^^^^^^^^^^^^^^ value used here after move
|
note: closure cannot be invoked more than once because it moves the variable
`dict` out of its environment
|
13 | for (key, value) in dict {
| ^^^^
Para depurar isso, temos de descobrir por que a closure é uma
FnOnce. Qual valor está sendo utilizado aqui? O compilador
prestativamente aponta que é dict, que neste caso é o único que
estamos referenciando. Ah, aí está o bug: estamos gastando dict
iterando por ele diretamente. Deveríamos estar fazendo um loop por
&dict, em vez de um simples dict, para acessar os valores por
referência:
let debug_dump_dict = || {
for (key, value) in &dict { // não utiliza dict
println!("{:?} - {:?}", key, value);
}
};
Isso corrige o erro; a função agora é uma Fn e pode ser chamada
quantas vezes você quiser.
FnMut
Existe mais um tipo de closure, o tipo que contém dados mutáveis
ou referências mut.
O Rust considera valores não-mut seguros para compartilhar entre
threads. Mas não seria seguro compartilhar closures não-mut que
contêm dados mut: chamar tal closure de vários threads pode levar a
todo tipo de condição de concorrência (race condition), pois vários
threads tentam ler e gravar os mesmos dados ao mesmo tempo.
Portanto, o Rust tem mais uma categoria de closure, FnMut, a
categoria de closures que escrevem, gravam ou modificam valores.
Closures FnMut são chamadas por referência mut, como se fossem
definidas assim:
// Pseudocódigo para os traits `Fn`, `FnMut` e `FnOnce`
trait Fn() -> R {
fn call(&self) -> R;
}
trait FnMut() -> R {
fn call_mut(&mut self) -> R;
}
trait FnOnce() -> R {
fn call_once(self) -> R;
}
Qualquer closure que exija acesso mut a um valor, mas não dropa
nenhum valor, é uma closure FnMut. Por exemplo:
let mut i = 0;
let incr = || {
i += 1; // incr toma emprestada uma referência mut a i
println!("Ding! i is now: {}", i);
};
call_twice(incr);
A maneira como escrevemos call_twice requer uma Fn. Como a incr é
uma FnMut e não uma Fn, esse código falha ao compilar. Mas há uma
solução fácil. Para entender a correção, vamos dar um passo atrás e
resumir o que você aprendeu sobre as três categorias de closures do
Rust.
• Fn é a família de closures e funções que você pode chamar várias
vezes sem restrição. Essa categoria mais elevada também inclui
todas as funções fn.
• FnMut é a família de closures que podem ser chamadas várias
vezes se a própria closure for declarada mut.
• FnOnceé a família de closures que podem ser chamadas uma vez,
se o chamador for o proprietário da closure.
Cada Fn atende aos requisitos de FnMut, e cada FnMut atende aos
requisitos de FnOnce. Conforme mostrado na Figura 14.2, elas não
são três categorias separadas.
Em vez disso, Fn() é um subtrait de FnMut(), que é um subtrait de
FnOnce(). Isso torna Fn a categoria mais exclusiva e poderosa. FnMut e
FnOnce são categorias mais amplas que incluem closures com
restrições de uso.
Callbacks
Muitas bibliotecas utilizam chamadas de retorno (callbacks) como
parte de sua API: funções fornecidas pelo usuário, para a biblioteca
chamar posteriormente. Na verdade, você já viu algumas APIs como
essa neste livro. De volta ao Capítulo 2, utilizamos o framework actix-
web para escrever um servidor web simples. Uma parte importante
desse programa era o roteador, que se parecia com isto:
App::new()
.route("/", web::get().to(get_index))
.route("/gcd", web::post().to(post_gcd))
O objetivo do roteador é rotear as solicitações recebidas da internet
para a parte do código Rust que lida com esse tipo específico de
solicitação. Nesse exemplo, get_index e post_gcd eram os nomes das
funções que declaramos em outra parte do programa, utilizando a
palavra-chave fn. Mas poderíamos ter passado closures, assim:
App::new()
.route("/", web::get().to(|| {
HttpResponse::Ok()
.content_type("text/html")
.body("<title>GCD Calculator</title>...")
}))
.route("/gcd", web::post().to(|form: web::Form<GcdParameters>| {
HttpResponse::Ok()
.content_type("text/html")
.body(format!("The GCD of {} and {} is {}.",
form.n, form.m, gcd(form.n, form.m)))
}))
Isso é porque actix-web foi escrito para aceitar qualquer Fn thread-safe
como argumento.
Como podemos fazer isso em nossos próprios programas? Vamos
tentar escrever nosso próprio roteador muito simples do zero, sem
utilizar nenhum código de actix-web. Podemos começar declarando
alguns tipos para representar solicitações e respostas HTTP:
struct Request {
method: String,
url: String,
headers: HashMap<String, String>,
body: Vec<u8>
}
struct Response {
code: u32,
headers: HashMap<String, String>,
body: Vec<u8>
}
Agora, o trabalho de um roteador é simplesmente armazenar uma
tabela que mapeia URLs para chamadas de retorno, para que a
chamada de retorno correta possa ser feita sob demanda. (Para
simplificar, permitiremos apenas que os usuários criem rotas que
correspondam a um único URL exato.)
struct BasicRouter<C> where C: Fn(&Request) -> Response {
routes: HashMap<String, C>
}
impl<C> BasicRouter<C> where C: Fn(&Request) -> Response {
/// Cria um roteador vazio
fn new() -> BasicRouter<C> {
BasicRouter { routes: HashMap::new() }
}
/// Adiciona uma rota ao roteador
fn add_route(&mut self, url: &str, callback: C) {
self.routes.insert(url.to_string(), callback);
}
}
Infelizmente, cometemos um erro. Você notou isso?
Esse roteador funciona bem, desde que adicionemos apenas uma
rota a ele:
let mut router = BasicRouter::new();
router.add_route("/", |_| get_form_response());
Isso compila e executa. Infelizmente, se adicionarmos outra rota:
router.add_route("/gcd", |req| get_gcd_response(req));
então obtemos erros:
error: mismatched types
|
41 | router.add_route("/gcd", |req| get_gcd_response(req));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| expected closure, found a different closure
|
= note: expected type `[closure@closures_bad_router.rs:40:27: 40:50]`
found type `[closure@closures_bad_router.rs:41:30: 41:57]`
note: no two closures, even if identical, have the same type
help: consider boxing your closure and/or using it as a trait object
Nosso erro estava em como definimos o tipo BasicRouter:
struct BasicRouter<C> where C: Fn(&Request) -> Response {
routes: HashMap<String, C>
}
Declaramos involuntariamente que cada BasicRouter tem um único tipo
de chamada de retorno C e todas as chamadas de retorno no HashMap
são desse tipo. Lá atrás, em “Qual utilizar”, na página 307,
mostramos um tipo Salad que tinha o mesmo problema:
struct Salad<V: Vegetable> {
veggies: Vec<V>
}
A solução aqui é a mesma da salada: como queremos suportar uma
variedade de tipos, precisamos utilizar boxes e objetos trait:
type BoxedCallback = Box<dyn Fn(&Request) -> Response>;
struct BasicRouter {
routes: HashMap<String, BoxedCallback>
}
Cada box pode conter um tipo diferente de closure, portanto um
único HashMap pode conter todos os tipos de chamada de retorno.
Observe que o parâmetro de tipo C sumiu.
Isso requer alguns ajustes nos métodos:
impl BasicRouter {
// Cria um roteador vazio
fn new() -> BasicRouter {
BasicRouter { routes: HashMap::new() }
}
1 N.R.: Loop apertado (tight loop) é um tipo de loop que tem poucas instruções e itera
muitas vezes.
15
capítulo
Iteradores
Criando iteradores
A documentação da biblioteca padrão Rust explica em detalhes que
tipo de iteradores cada tipo fornece, mas a biblioteca segue algumas
convenções gerais para ajudá-lo a se orientar e encontrar o que
precisa.
Implementações de IntoIterator
Quando um tipo implementa IntoIterator, você mesmo pode chamar
seu método into_iter, assim como um loop for seria:
// Você normalmente deve utilizar HashSet, mas sua ordem de iteração é
// não determinista, então BtreeSet funciona melhor em exemplos
use std::collections::BTreeSet;
let mut favorites = BTreeSet::new();
favorites.insert("Lucy in the Sky With Diamonds".to_string());
favorites.insert("Liebesträume No. 3".to_string());
fn dump<T, U>(t: T)
where T: IntoIterator<Item=U>,
U: Debug
{
for u in t {
println!("{:?}", u);
}
}
Você não pode escrever essa função genérica utilizando iter e iter_mut,
já que não são métodos de nenhum trait: a maioria dos tipos
iteráveis simplesmente tem métodos com esses nomes.
from_fn e sucessores
Uma maneira simples e geral de produzir uma sequência de valores
é fornecer uma closure que os retorne.
Dada uma função que retorna Option<T>, std::iter::from_fn retorna um
iterador que simplesmente chama a função para produzir seus itens.
Por exemplo:
use rand::random; // Nas dependências de Cargo.toml: rand = "0.7"
use std::iter::from_fn;
assert_eq!(fibonacci().take(8).collect::<Vec<_>>(),
vec![1, 1, 2, 3, 5, 8, 13, 21]);
Uma nota de advertência: os métodos from_fn e successors são flexíveis
o suficiente para que você possa transformar praticamente qualquer
uso de iteradores em uma única chamada para um ou outro,
passando closures complexas para obter o comportamento
necessário. Mas fazer isso negligencia a oportunidade que os
iteradores fornecem para esclarecer como os dados fluem pela
computação e utiliza nomes padrão para padrões comuns.
Certifique-se de estar familiarizado com os outros métodos
iteradores deste capítulo antes de se apoiar nesses dois; muitas
vezes há maneiras melhores de fazer o trabalho.
Métodos drain
Muitos tipos de coleção fornecem um método drain que utiliza uma
referência mutável para a coleção e retorna um iterador que passa a
posse de cada elemento para o consumidor. No entanto, ao contrário
do método into_iter(), que pega a coleção por valor e a consome, drain
simplesmente pega emprestada uma referência mutável à coleção e,
quando o iterador é dropado, ele remove todos os elementos
restantes da coleção e a deixa vazia.
Em tipos que podem ser indexados por um intervalo, como Strings,
vetores e VecDeques, o método drain pega uma série de elementos para
remover, em vez de drenar toda a sequência:
let mut outer = "Earth".to_string();
let inner = String::from_iter(outer.drain(1..4));
assert_eq!(outer, "Eh");
assert_eq!(inner, "art");
Se você precisar drenar toda a sequência, utilize todo o intervalo, ..,
como o argumento.
Adaptadores iteradores
Depois de ter um iterador em mãos, o trait Iterator fornece uma
ampla seleção de métodos adaptadores, ou simplesmente
adaptadores, que consomem um iterador e criam um novo com
comportamentos úteis. Para ver como os adaptadores funcionam,
começaremos com dois dos adaptadores mais populares, map e filter.
Em seguida, abordaremos o restante da caixa de ferramentas do
adaptador, abrangendo quase todas as formas que você imaginar
para criar sequências de valores a partir de outras sequências:
truncamento, salto, combinação, reversão, concatenação, repetição
e muito mais.
map e filter
O adaptador map do trait Iterator permite que você transforme um
iterador aplicando uma closure a seus itens. O adaptador filter
permite filtrar itens de um iterador, utilizando uma closure para
decidir quais manter e quais dropar.
Por exemplo, suponha que você esteja iterando sobre linhas de texto
e queira omitir os espaços em branco iniciais e finais de cada linha.
O método str::trim da biblioteca padrão dropa os espaços em branco à
esquerda e à direita de uma única &str, retornando uma nova &str
reduzida que empresta da original. Você pode utilizar o adaptador
map para aplicar str::trim a cada linha do iterador:
let text = " ponies \n giraffes\niguanas \nsquid".to_string();
let v: Vec<&str> = text.lines()
.map(str::trim)
.collect();
assert_eq!(v, ["ponies", "giraffes", "iguanas", "squid"]);
A chamada a text.lines() retorna um iterador que produz as linhas da
string. Chamar map nesse iterador retorna um segundo iterador que
aplica str::trim a cada linha e produz os resultados como seus itens.
Por fim, collect reúne esses itens em um vetor.
O iterador que map retorna é, naturalmente, um candidato para uma
adaptação posterior. Se você deseja excluir iguanas do resultado,
pode escrever o seguinte:
let text = " ponies \n giraffes\niguanas \nsquid".to_string();
let v: Vec<&str> = text.lines()
.map(str::trim)
.filter(|s| *s != "iguanas")
.collect();
assert_eq!(v, ["ponies", "giraffes", "squid"]);
Aqui, filter retorna um terceiro iterador que produz apenas os itens do
iterador map para o qual a closure |s| *s != "iguanas" retorna true. Uma
cadeia de adaptadores iteradores é como um pipeline no shell do
Unix: cada adaptador tem um único propósito e fica claro como a
sequência está sendo transformada à medida que se lê da esquerda
para a direita.
As assinaturas desses adaptadores são as seguintes:
fn map<B, F>(self, f: F) -> impl Iterator<Item=B>
where Self: Sized, F: FnMut(Self::Item) -> B;
filter_map e flat_map
O adaptador map funciona bem em situações em que cada item de
entrada produz um item de saída. Mas e se você quiser excluir
certos itens da iteração em vez de processá-los ou substituir itens
únicos por zero ou mais itens? Os adaptadores filter_map e flat_map
garantem essa flexibilidade.
O adaptador filter_map é semelhante a map exceto que permite que
sua closure transforme o item em um novo item (como map faz) ou
dropar o item da iteração. Assim, é um pouco como uma
combinação de filter e map. A sua assinatura é a seguinte:
fn filter_map<B, F>(self, f: F) -> impl Iterator<Item=B>
where Self: Sized, F: FnMut(Self::Item) -> Option<B>;
Isso é o mesmo que assinatura de map, exceto que aqui a closure
retorna Option<B>, não simplesmente B. Quando a closure voltar None,
o item é dropado da iteração; quando voltar Some(b), então b é o
próximo item o iterador filter_map produz.
Por exemplo, suponha que você queira verificar uma string em busca
de palavras separadas por espaços em branco que possam ser
analisadas como números e processar os números, dropando as
outras palavras. Você pode escrever:
use std::str::FromStr;
let text = "1\nfrond .25 289\n3.1415 estuary\n";
for number in text
.split_whitespace()
.filter_map(|w| f64::from_str(w).ok())
{
println!("{:4.2}", number.sqrt());
}
Isso imprime o seguinte:
1.00
0.50
17.00
1.77
A closure dada a filter_map tenta analisar cada fatia separada por
espaços em branco utilizando f64::from_str. Isso retorna um Result<f64,
ParseFloatError>, que .ok() se transforma em um Option<f64>: um erro de
análise torna-se None, enquanto um resultado de análise bem-
sucedido torna-se Some(v). O iterador filter_map dropa todos os valores
None e produz o valor v para cada Some(v).
Mas qual é o ponto em fundir map e filter em uma única operação
como essa, em vez de apenas utilizar esses adaptadores
diretamente? O adaptador filter_map mostra seu valor em situações
como a que acabamos de mostrar, quando a melhor maneira de
decidir se deve incluir o item na iteração é realmente tentar
processá-lo. Você pode fazer a mesma coisa com apenas filter e map,
mas é algo um pouco desajeitado:
text.split_whitespace()
.map(|w| f64::from_str(w))
.filter(|r| r.is_ok())
.map(|r| r.unwrap())
Você pode pensar no adaptador flat_map como continuando na
mesma linha que map e filter_map, exceto que agora a closure pode
retornar não apenas um item (como com map) ou zero ou um item
(como com filter_map), mas uma sequência de qualquer número de
itens. O iterador flat_map produz a concatenação das sequências que
a closure retorna.
A assinatura de flat_map é mostrada aqui:
fn flat_map<U, F>(self, f: F) -> impl Iterator<Item=U::Item>
where F: FnMut(Self::Item) -> U, U: IntoIterator;
A closure passada para flat_map deve retornar um iterável, mas
qualquer tipo de iterável serve.1
Por exemplo, suponha que temos uma tabela mapeando países para
suas principais cidades. Dada uma lista de países, como podemos
iterar por suas principais cidades?
use std::collections::HashMap;
let mut major_cities = HashMap::new();
major_cities.insert("Japan", vec!["Tokyo", "Kyoto"]);
major_cities.insert("The United States", vec!["Portland", "Nashville"]);
major_cities.insert("Brazil", vec!["São Paulo", "Brasília"]);
major_cities.insert("Kenya", vec!["Nairobi", "Mombasa"]);
major_cities.insert("The Netherlands", vec!["Amsterdam", "Utrecht"]);
let countries = ["Japan", "Brazil", "Kenya"];
for &city in countries.iter().flat_map(|country| &major_cities[country]) {
println!("{}", city);
}
Isso imprime o seguinte:
Tokyo
Kyoto
São Paulo
Brasília
Nairobi
Mombasa
Uma maneira de ver isso seria dizer que, para cada país,
recuperamos o vetor de suas cidades, concatenamos todos os
vetores em uma única sequência e a imprimimos.
Mas lembre-se de que os iteradores são preguiçosos: são apenas as
chamadas de loop for para o método next do iterador flat_map que
fazem com que o trabalho seja realizado. A sequência concatenada
completa nunca é construída na memória. Em vez disso, o que
temos aqui é uma pequena máquina de estado que extrai do
iterador de cidades, um item de cada vez, até que se esgote e só
então produz um novo iterador de cidades para o próximo país. O
efeito é o de um loop aninhado, mas empacotado para uso como um
iterador.
flatten
O adaptador flatten concatena os itens de um iterador, assumindo que
cada item é iterável:
use std::collections::BTreeMap;
// Uma tabela mapeando cidades para seus parques: cada valor é um vetor
let mut parks = BTreeMap::new();
parks.insert("Portland", vec!["Mt. Tabor Park", "Forest Park"]);
parks.insert("Kyoto", vec!["Tadasu-no-Mori Forest", "Maruyama Koen"]);
parks.insert("Nashville", vec!["Percy Warner Park", "Dragon Park"]);
assert_eq!(all_parks,
vec!["Tadasu-no-Mori Forest", "Maruyama Koen", "Percy Warner Park",
"Dragon Park", "Mt. Tabor Park", "Forest Park"]);
O nome “flatten” vem da imagem de achatar uma estrutura de dois
níveis em uma estrutura de um nível: o BTreeMap e seus Vecs de
nomes são achatados em um iterador produzindo todos os nomes.
A assinatura de flatten é a seguinte:
fn flatten(self) -> impl Iterator<Item=Self::Item::Item>
where Self::Item: IntoIterator;
Em outras palavras, os itens do iterador subjacente devem eles
próprios implementar IntoIterator de modo que seja efetivamente uma
sequência de sequências. O método flatten então retorna um iterador
sobre a concatenação dessas sequências. Naturalmente, isso é feito
preguiçosamente, derivando um novo item de self somente quando
terminarmos de iterar pelo último.
O método flatten é utilizado de algumas maneiras surpreendentes. Se
você tem um Vec<Option<...>> e deseja iterar apenas pelos valores
Some, flatten funciona lindamente:
assert_eq!(vec![None, Some("day"), None, Some("one")]
.into_iter()
.flatten()
.collect::<Vec<_>>(),
vec!["day", "one"]);
Isso funciona porque a própria Option implementa IntoIterator,
representando uma sequência de elementos zero ou um. Os
elementos None não contribuem em nada para a iteração, enquanto
cada elemento Some contribui com um único valor. Da mesma forma,
você pode utilizar flatten para iterar por valores Option<Vec<...>>: None se
comporta da mesma forma que um vetor vazio.
Result também implementa IntoIterator, com Err representando uma
sequência vazia, então aplicar flatten a um iterador de valores Result
efetivamente espreme todos os Errs e os descarta, resultando em um
fluxo de valores de sucesso desencapsulados. Não recomendamos
ignorar erros em seu código, mas esse é um truque interessante que
as pessoas utilizam quando acham que sabem o que está
acontecendo.
Você pode se encontrar usando flatten quando o que você realmente
precisa é flat_map. Por exemplo, o método str::to_uppercase da biblioteca
padrão, que converte uma string em letras maiúsculas, funciona
mais ou menos assim:
fn to_uppercase(&self) -> String {
self.chars()
.map(char::to_uppercase)
.flatten() // há uma maneira melhor
.collect()
}
A razão pela qual flatten é necessário é que ch.to_uppercase() não retorna
um único caractere, mas um iterador produzindo um ou mais
caracteres. Mapear cada caractere em seu equivalente em
maiúsculas resulta em um iterador de iteradores de caracteres e o
flatten cuida de juntá-los todos em algo que podemos finalmente
coletar com collect dentro de uma String.
Mas essa combinação de map e flatten é tão comum que Iterator fornece
o adaptador flat_map apenas para esse caso. (Na verdade, flat_map foi
adicionado à biblioteca padrão antes de flatten.) Portanto, o código
anterior poderia ser escrito:
fn to_uppercase(&self) -> String {
self.chars()
.flat_map(char::to_uppercase)
.collect()
}
take e take_while
Os adaptadores take e take_while do trait Iterator permitem que você
termine uma iteração após um certo número de itens ou quando
uma closure decide interromper as coisas. Suas assinaturas são as
seguintes:
fn take(self, n: usize) -> impl Iterator<Item=Self::Item>
where Self: Sized;
skip e skip_while
Os métodos skip e skip_while do trait Iterator são o complemento de take
e take_while: eles dropam um certo número de itens desde o início de
uma iteração ou dropam itens até que uma closure encontre um
aceitável e, em seguida, passam os itens restantes inalterados. Suas
assinaturas são as seguintes:
fn skip(self, n: usize) -> impl Iterator<Item=Self::Item>
where Self: Sized;
peekable
Um iterador peekable permite que você espie o próximo item que
será produzido sem realmente o consumir. Você pode transformar
qualquer iterador em um iterador peekable chamando o método
peekable do trait Iterator:
fn peekable(self) -> std::iter::Peekable<Self>
where Self: Sized;
Aqui, Peekable<Self> é um struct que implementa Iterator<Item=Self::Item> e
Self é o tipo do iterador subjacente.
Um iterador Peekable tem um método adicional peek que retorna um
Option<&Item>: None se o iterador subjacente terminar, e Some(r) caso
contrário, em que r é uma referência compartilhada para o próximo
item. (Observe que, se o tipo de item do iterador já for uma
referência a algo, isso acaba sendo uma referência a uma
referência.)
Chamar peek tenta derivar o próximo item do iterador subjacente e,
se houver, o armazena em cache até a próxima chamada a next.
Todos os outros métodos Iterator em Peekable conhecem esse cache:
por exemplo, iter.last() em um iterador peekable iter sabe verificar o
cache depois de esgotar o iterador subjacente.
Iteradores peekable são essenciais quando você não consegue
decidir quantos itens consumir de um iterador até que tenha ido
longe demais. Por exemplo, se você estiver analisando números de
um fluxo de caracteres, não poderá decidir onde o número termina
até ver o primeiro caractere não numérico que o segue:
use std::iter::Peekable;
fn parse_number<I>(tokens: &mut Peekable<I>) -> u32
where I: Iterator<Item=char>
{
let mut n = 0;
loop {
match tokens.peek() {
Some(r) if r.is_digit(10) => {
n = n * 10 + r.to_digit(10).unwrap();
}
_ => return n
}
tokens.next();
}
}
let mut chars = "226153980,1766319049".chars().peekable();
assert_eq!(parse_number(&mut chars), 226153980);
// Observe, `parse_number` não consumiu a vírgula! Então nós faremos isso
assert_eq!(chars.next(), Some(','));
assert_eq!(parse_number(&mut chars), 1766319049);
assert_eq!(chars.next(), None);
A função parse_number utiliza peek para verificar o próximo caractere e
o consome somente se for um dígito. Se não for um dígito ou o
iterador estiver esgotado (ou seja, se peek retorna None), retornamos
o número que analisamos e deixamos o próximo caractere no
iterador, pronto para ser consumido.
fuse
Uma vez que Iterator retornou None, o trait não especifica como ele
deve se comportar se você chamar seu método next novamente. A
maioria dos iteradores apenas retorna None de novo, mas não todos.
Se o seu código conta com esse comportamento, você pode se
surpreender.
O adaptador fuse pega qualquer iterador e produz um que
definitivamente continuará retornando None uma vez que tenha feito
isso pela primeira vez:
struct Flaky(bool);
inspect
O adaptador inspect é útil para depurar sequências (pipelines) de
adaptadores de iteradores, mas não é muito utilizado em código de
produção. Ele simplesmente aplica uma closure a uma referência
compartilhada para cada item e, em seguida, passa o item. A closure
não pode afetar os itens, mas pode fazer coisas como imprimi-los ou
fazer afirmações sobre eles.
Esse exemplo mostra um caso em que a conversão de uma string
para letras maiúsculas altera seu comprimento:
let upper_case: String = "große".chars()
.inspect(|c| println!("before: {:?}", c))
.flat_map(|c| c.to_uppercase())
.inspect(|c| println!(" after: {:?}", c))
.collect();
assert_eq!(upper_case, "GROSSE");
O equivalente maiúsculo da letra alemã minúscula “ß” é “SS” e é por
isso que char::to_uppercase retorna um iterador sobre caracteres, não
um único caractere de substituição. O código anterior utiliza flat_map
para concatenar todas as sequências que to_uppercase retorna em uma
única String, imprimindo o seguinte conforme faz isso:
before: 'g'
after: 'G'
before: 'r'
after: 'R'
before: 'o'
after: 'O'
before: 'ß'
after: 'S'
after: 'S'
before: 'e'
after: 'E'
chain
O adaptador chain junta um iterador a outro. Mais precisamente,
i1.chain(i2) retorna um iterador que consome itens de i1 até que se
esgote e, em seguida, consome itens de i2.
A assinatura do adaptador chain é a seguinte:
fn chain<U>(self, other: U) -> impl Iterator<Item=Self::Item>
where Self: Sized, U: IntoIterator<Item=Self::Item>;
Em outras palavras, você pode encadear um iterador com qualquer
iterável que produza o mesmo tipo de item.
Por exemplo:
let v: Vec<i32> = (1..4).chain([20, 30, 40]).collect();
assert_eq!(v, [1, 2, 3, 20, 30, 40]);
Um iterador chain é reversível, se ambos os iteradores subjacentes
forem:
let v: Vec<i32> = (1..4).chain([20, 30, 40]).rev().collect();
assert_eq!(v, [40, 30, 20, 3, 2, 1]);
Um iterador chain acompanha quando cada um dos dois iteradores
subjacentes retorna None e dirige as chamadas a next e next_back para
um ou outro, conforme apropriado.
enumerate
O adaptador enumerate do trait Iterator anexa um índice corrente à
sequência, utilizando um iterador que produz itens A, B, C, ... e
retornando um iterador que produz pares (0, A), (1, B), (2, C), .... Parece
trivial à primeira vista, mas é utilizado com uma frequência
surpreendente.
Os consumidores podem utilizar esse índice para distinguir um item
do outro e estabelecer o contexto no qual processar cada um. Por
exemplo, o plotter de conjunto de Mandelbrot no Capítulo 2 divide a
imagem em oito faixas horizontais e atribui cada uma a um thread
diferente. Esse código utiliza enumerate para dizer a cada thread a qual
parte da imagem sua faixa corresponde.
Ele começa com um buffer retangular de pixels:
let mut pixels = vec![0; columns * rows];
A seguir, utiliza chunks_mut para dividir a imagem em faixas
horizontais, uma por thread:
let threads = 8;
let band_rows = rows / threads + 1;
...
let bands: Vec<&mut [u8]> = pixels.chunks_mut(band_rows * columns).collect();
E então itera sobre as faixas, iniciando um thread para cada uma:
for (i, band) in bands.into_iter().enumerate() {
let top = band_rows * i;
// inicia um thread para renderizar linhas `top..top + band_rows`
...
}
Cada iteração obtém um par (i, band), em que band é a fatia &mut [u8]
do buffer de pixels em que o thread deve desenhar e i é o índice
dessa faixa na imagem geral, cortesia do adaptador enumerate. Dados
os limites do gráfico e o tamanho das faixas, isso é informação
suficiente para o thread determinar qual parte da imagem lhe foi
atribuído e, portanto, o que desenhar em band.
Você pode pensar nos pares (index, item) que enumerate produz como
análogos aos pares (key, value) que você obtém ao iterar por um
HashMap ou outra coleção associativa. Se você estiver iterando sobre
uma fatia ou vetor, o index é a “chave” sob a qual o item aparece.
zip
O adaptador zip combina dois iteradores em um único iterador que
produz pares contendo um valor de cada iterador, como um zíper
unindo seus dois lados em uma única costura. O iterador
compactado termina quando um dos dois iteradores subjacentes
termina.
Por exemplo, você pode obter o mesmo efeito que o adaptador
enumerate compactando o intervalo ilimitado 0.. com o outro iterador:
let v: Vec<_> = (0..).zip("ABCD".chars()).collect();
assert_eq!(v, vec![(0, 'A'), (1, 'B'), (2, 'C'), (3, 'D')]);
Nesse sentido, você pode pensar em zip como uma generalização de
enumerate: enquanto enumerate anexa índices à sequência, zip anexa os
itens de qualquer iterador arbitrário. Sugerimos antes que enumerate
pode ajudar a fornecer contexto para itens de processamento; zip é
uma maneira mais flexível de fazer o mesmo.
O argumento para zip não precisa ser um iterador em si; pode ser
qualquer iterável:
use std::iter::repeat;
by_ref
Ao longo desta seção, adicionamos adaptadores a iteradores. Depois
de fazer isso, você pode remover o adaptador novamente?
Normalmente, não: os adaptadores tomam posse do iterador
subjacente e não fornecem nenhum método para devolvê-lo.
O método by_ref de um iterador pega emprestada uma referência
mutável ao iterador para que você possa aplicar adaptadores à
referência. Quando terminar de consumir itens desses adaptadores,
você os dropa, o empréstimo termina e você recupera o acesso ao
seu iterador original.
Por exemplo, no início do capítulo, mostramos como utilizar take_while
e skip_while para processar as linhas de cabeçalho e o corpo de uma
mensagem de e-mail. Mas e se você quiser fazer as duas coisas,
utilizando o mesmo iterador subjacente? Utilizando by_ref, podemos
utilizar take_while para lidar com os cabeçalhos e, quando terminar,
recuperar o iterador subjacente, que take_while deixou exatamente em
posição de lidar com o corpo da mensagem:
let message = "To: jimb\r\n\
From: id\r\n\
\r\n\
Oooooh, donuts!!\r\n";
let mut lines = message.lines();
println!("Headers:");
for header in lines.by_ref().take_while(|l| !l.is_empty()) {
println!("{}" , header);
}
println!("\nBody:");
for body in lines {
println!("{}" , body);
}
A chamada a lines.by_ref() toma emprestada uma referência mutável
ao iterador e é dessa referência que o iterador take_while toma posse.
Esse iterador sai do escopo no final do primeiro loop for, o que
significa que o empréstimo terminou, então você pode utilizar lines
novamente no segundo loop for. Isso imprime o seguinte:
Headers:
To: jimb
From: id
Body:
Oooooh, donuts!!
A definição do adaptador by_ref é trivial: ele retorna uma referência
mutável ao iterador. Então, a biblioteca padrão inclui essa pequena
implementação estranha:
impl<'a, I: Iterator + ?Sized> Iterator for &'a mut I {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
(**self).next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
(**self).size_hint()
}
}
Em outras palavras, se I é algum tipo de iterador, então &mut I
também é um iterador, cujos métodos next e size_hint concedem ao
seu referente. Quando você chama um adaptador em uma
referência mutável para um iterador, o adaptador assume a posse da
referência, não o próprio iterador. Isso é apenas um empréstimo que
termina quando o adaptador sai do escopo.
cloned, copied
O adaptador cloned pega um iterador que produz referências e
retorna um iterador que produz valores clonados dessas referências,
muito parecido com iter.map(|item| item.clone()). Naturalmente, o tipo de
referência deve implementar Clone. Por exemplo:
let a = ['1', '2', '3', '∞'];
assert_eq!(a.iter().next(), Some(&'1'));
assert_eq!(a.iter().cloned().next(), Some('1'));
O adaptador copied é a mesma ideia, mas mais restritiva: o tipo de
referência deve implementar Copy. Uma chamada como iter.copied() é
aproximadamente o mesmo que iter.map(|r| *r). Como todo tipo que
implementa Copy também implementa Clone, cloned é estritamente mais
geral, mas, dependendo do tipo de item, uma chamada clone pode
fazer quantidades arbitrárias de alocação e cópia. Se você está
assumindo que isso nunca aconteceria porque seu tipo de item é
algo simples, é melhor utilizar copied para fazer o verificador de tipos
verificar suas suposições.
cycle
O adaptador cycle retorna um iterador que repete infinitamente a
sequência produzida pelo iterador subjacente. O iterador subjacente
deve implementar std::clone::Clone de modo que cycle possa salvar seu
estado inicial e reutilizá-lo sempre que o ciclo recomeçar.
Por exemplo:
let dirs = ["North", "East", "South", "West"];
let mut spin = dirs.iter().cycle();
assert_eq!(spin.next(), Some(&"North"));
assert_eq!(spin.next(), Some(&"East"));
assert_eq!(spin.next(), Some(&"South"));
assert_eq!(spin.next(), Some(&"West"));
assert_eq!(spin.next(), Some(&"North"));
assert_eq!(spin.next(), Some(&"East"));
Ou, para um uso realmente gratuito de iteradores:
use std::iter::{once, repeat};
Consumindo iteradores
Até agora, cobrimos a criação de iteradores e sua adaptação em
novos iteradores; aqui finalizamos o processo mostrando formas de
consumi-los.
Naturalmente, você pode consumir um iterador com um loop for ou
com uma chamada next explicitamente, mas há muitas tarefas
comuns que você não deveria ter de escrever tudo várias vezes. O
trait Iterator fornece uma ampla seleção de métodos para cobrir
muitas delas.
fn main() {
let stdin = std::io::stdin();
println!("{}", stdin.lock().lines().count());
}
Os métodos sum e product calculam a soma ou produto dos itens do
iterador, que devem ser inteiros ou números de ponto flutuante:
fn triangle(n: u64) -> u64 {
(1..=n).sum()
}
assert_eq!(triangle(20), 210);
max, min
Os métodos min e max em Iterator retornam o menor ou o maior item
que o iterador produz. O tipo de item do iterador deve implementar
std::cmp::Ord para que os itens possam ser comparados uns com os
outros. Por exemplo:
assert_eq!([-2, 0, 1, 0, -2, -5].iter().max(), Some(&1));
assert_eq!([-2, 0, 1, 0, -2, -5].iter().min(), Some(&-5));
Esses métodos retornam uma Option<Self::Item> para que possam
retornar None se o iterador não produzir itens.
Conforme explicado em “Comparações de equivalência”, na
página 341, tipos de ponto flutuante f32 e f64 do Rust implementam
apenas std::cmp::PartialOrd, e não implementam std::cmp::Ord, então você
não pode utilizar os métodos min e max para calcular o menor ou o
maior de uma sequência de números de ponto flutuante. Esse não é
um aspecto popular do design do Rust, mas é deliberado: não está
claro o que tais funções devem fazer com os valores IEEE NaN.
Simplesmente os ignorar correria o risco de mascarar problemas
mais sérios no código.
Se você sabe como gostaria de lidar com valores NaN, pode utilizar o
max_by e min_by em vez disso, métodos iteradores, que permitem
fornecer sua própria função de comparação.
max_by, min_by
Os métodos max_by e min_by retornam o item máximo ou mínimo que
o iterador produz, conforme determinado por uma função de
comparação que você fornece:
use std::cmp::Ordering;
// Compara dois valores f64. Gera um pânico se receber um NaN
fn cmp(lhs: &f64, rhs: &f64) -> Ordering {
lhs.partial_cmp(rhs).unwrap()
}
let numbers = [1.0, 4.0, 2.0];
assert_eq!(numbers.iter().copied().max_by(cmp), Some(4.0));
assert_eq!(numbers.iter().copied().min_by(cmp), Some(1.0));
let numbers = [1.0, 4.0, std::f64::NAN, 2.0];
assert_eq!(numbers.iter().copied().max_by(cmp), Some(4.0)); // pânico
Os métodos max_by e min_by passam itens para a função de
comparação por referência para que possam funcionar de forma
eficiente com qualquer tipo de iterador, de modo que cmp espera
tomar seus argumentos por referência, mesmo que tenhamos
utilizado copied para obter um iterador que produza itens f64.
max_by_key, min_by_key
Os métodos max_by_key e min_by_key em Iterator permitem selecionar o
item máximo ou mínimo conforme determinado por uma closure
aplicado a cada item. A closure pode selecionar algum campo do
item ou realizar uma computação nos itens. Como você geralmente
está interessado em dados associados a algum mínimo ou máximo,
não apenas ao extremo em si, essas funções costumam ser mais
úteis do que min e max. Suas assinaturas são as seguintes:
fn min_by_key<B: Ord, F>(self, f: F) -> Option<Self::Item>
where Self: Sized, F: FnMut(&Self::Item) -> B;
any e all
Os métodos any e all aplicam uma closure a cada item que o iterador
produz e retornam true se a closure voltar true para qualquer item
(any), ou para todos os itens (all):
let id = "Iterator";
assert!( id.chars().any(char::is_uppercase));
assert!(!id.chars().all(char::is_uppercase));
Esses métodos consomem apenas os itens necessários para
determinar a resposta. Por exemplo, se a closure retornar true para
um determinado item, então any retorna true imediatamente, sem
extrair mais itens do iterador.
fold e rfold
O método fold é uma ferramenta muito geral para acumular algum
tipo de resultado em toda a sequência de itens que um iterador
produz. Dado um valor inicial, que chamaremos de acumulador, e
uma closure, fold aplica repetidamente a closure ao acumulador atual
e ao próximo item do iterador. O valor que a closure retorna é
tomado como o novo acumulador, a ser passado para a closure com
o próximo item. O valor final do acumulador é o que fold ela mesma
retorna. Se a sequência estiver vazia, fold simplesmente retorna o
acumulador inicial.
Muitos dos outros métodos para consumir os valores de um iterador
podem ser escritos como usos de fold:
let a = [5, 6, 7, 8, 9, 10];
assert_eq!(a.iter().fold(0, |n, _| n+1), 6); // contagem
assert_eq!(a.iter().fold(0, |n, i| n+i), 45); // soma
assert_eq!(a.iter().fold(1, |n, i| n*i), 151200); // produto
// max
assert_eq!(a.iter().cloned().fold(i32::min_value(), std::cmp::max),
10);
A assinatura do método fold é a seguinte:
fn fold<A, F>(self, init: A, f: F) -> A
where Self: Sized, F: FnMut(A, Self::Item) -> A;
Aqui, A é o tipo do acumulador. O argumento init é um A, assim como
o primeiro argumento e o valor de retorno da closure, e o valor de
retorno do próprio fold.
Observe que os valores do acumulador são movidos para dentro e
para fora da closure, então você pode utilizar fold com tipos não-Copy
de acumuladores:
let a = ["Pack", "my", "box", "with",
"five", "dozen", "liquor", "jugs"];
try_fold e try_rfold
O método try_fold é o mesmo que fold, exceto que a iteração pode sair
antecipadamente, sem consumir todos os valores do iterador. O valor
retornado pela closure que você passa para try_fold indica se deve
retornar imediatamente ou continuar consumindo os itens do
iterador.
Sua closure pode retornar qualquer um dos vários tipos, indicando
como o fold deve proceder:
• Se sua closure retornar Result<T, E>, talvez porque faça E/S ou
realize alguma outra operação falível, então retornar Ok(v) instrui
try_fold a continuar, com v como o novo valor do acumulador.
Retornar Err(e) faz com que o fold pare imediatamente. O valor
final do fold é um Result carregando o valor final do acumulador, ou
o erro retornado pela closure.
• Se sua closure retornar Option<T>, então Some(v) indica que o fold
deve continuar com v como o novo valor do acumulador e None
indica que a iteração deve parar imediatamente. O valor final do
fold também é uma Option.
• Por fim, a closure pode retornar um valor std::ops::ControlFlow. Esse
tipo é uma enumeração com duas variantes, Continue(c) e Break(b), o
que significa continuar com o novo valor do acumulador c, ou
parar mais cedo. O resultado do fold é um valor ControlFlow:
Continue(v) se o fold consumiu todo o iterador, gerando o valor final
do acumulador v; ou Break(b), se a closure retornou esse valor.
Continue(c) e Break(b) comportam-se exatamente como Ok(c) e Err(b). A
vantagem de utilizar ControlFlow em vez de Result é que isso torna
seu código um pouco mais legível quando uma saída antecipada
não indica um erro, mas apenas que a resposta está pronta
antecipadamente. Mostramos um exemplo disso a seguir.
Eis um programa que soma números lidos de sua entrada padrão:
use std::error::Error;
use std::io::prelude::*;
use std::str::FromStr;
fn main() -> Result<(), Box<dyn Error>> {
let stdin = std::io::stdin();
let sum = stdin.lock()
.lines()
.try_fold(0, |sum, line| -> Result<u64, Box<dyn Error>> {
Ok(sum + u64::from_str(&line?.trim())?)
})?;
println!("{}", sum);
Ok(())
}
O iterador lines em fluxos de entrada bufferizada produz itens do tipo
Result<String, std::io::Error> e a análise de String como um inteiro pode
falhar também. Utilizar try_fold aqui deixa a closure retornar
Result<u64, ...>,
então podemos utilizar o operador ? para propagar
falhas desde a closure até a função main.
Devido a try_fold ser tão flexível, ele é utilizado para implementar
muitos dos outros métodos de consumo de Iterator. Por exemplo, eis
uma implementação de all:
fn all<P>(&mut self, mut predicate: P) -> bool
where P: FnMut(Self::Item) -> bool,
Self: Sized
{
use std::ops::ControlFlow::*;
self.try_fold((), |_, item| {
if predicate(item) { Continue(()) } else { Break(()) }
}) == Continue(())
}
Observe que isso não pode ser escrito com fold: all que promete parar
de consumir itens do iterador subjacente assim que o predicate
retornar falso, mas fold sempre consome todo o iterador.
Se você estiver implementando seu próprio tipo de iterador, vale a
pena investigar se seu iterador pode implementar try_fold mais
eficientemente do que a definição padrão do trait Iterator. Se você
pode acelerar try_fold, todos os outros métodos criados nele também
serão beneficiados.
O método try_rfold, como o próprio nome sugere, é o mesmo que
try_fold, exceto que ele consome valores na parte de trás, em vez da
frente, e requer um iterador de duas extremidades.
nth, nth_back
O método nth pega um índice n, ignora muitos itens do iterador e
retorna o próximo item ou None se a sequência terminar antes desse
ponto. Chamar .nth(0) é equivalente a .next().
Ele não toma posse do iterador da mesma forma que um adaptador
faria, então você pode chamá-lo várias vezes:
let mut squares = (0..10).map(|i| i*i);
assert_eq!(squares.nth(4), Some(16));
assert_eq!(squares.nth(0), Some(25));
assert_eq!(squares.nth(6), None);
Sua assinatura é mostrada aqui:
fn nth(&mut self, n: usize) -> Option<Self::Item>
where Self: Sized;
O método nth_back é praticamente o mesmo, exceto pelo fato de que
extrai da parte de trás de um iterador de duas extremidades.
Chamar .nth_back(0) é equivalente a .next_back(): retorna o último item,
ou None se o iterador estiver vazio.
last
O método last retorna o último item que o iterador produz, ou None se
estiver vazio. A sua assinatura é a seguinte:
fn last(self) -> Option<Self::Item>;
Por exemplo:
let squares = (0..10).map(|i| i*i);
assert_eq!(squares.last(), Some(81));
Isso consome todos os itens do iterador começando pela frente,
mesmo que o iterador seja reversível. Se você tiver um iterador
reversível e não precisar consumir todos os seus itens, basta
escrever iter.next_back().
assert_eq!(big_city_with_volcano_park,
Some(("Portland", "Mt. Tabor Park")));
// A coleta de um mapa requer pares (chave, valor); portanto, para este exemplo,
// zipa a sequência de strings com uma sequência de inteiros.
let args: HashMap<String, usize> = std::env::args().zip(0..).collect();
let args: BTreeMap<String, usize> = std::env::args().zip(0..).collect();
// e assim por diante
Naturalmente, collect em si não sabe como construir todos esses
tipos. Em vez disso, quando algum tipo de coleção como Vec ou
HashMap sabe como se construir a partir de um iterador, ele
implementa o trait std::iter::FromIterator, para a qual collect é apenas um
verniz conveniente:
trait FromIterator<A>: Sized {
fn from_iter<T: IntoIterator<Item=A>>(iter: T) -> Self;
}
Se um tipo de coleção implementa FromIterator<A>, então sua função
associada ao tipo from_iter constrói um valor desse tipo a partir de um
iterável produzindo itens do tipo A.
No caso mais simples, a implementação poderia simplesmente
construir uma coleção vazia e adicionar os itens do iterador um por
um. Por exemplo, implementação de FromIterator por
std::collections::LinkedList funciona dessa forma.
Contudo, alguns tipos podem fazer melhor do que isso. Por exemplo,
construir um vetor de algum iterador iter pode ser tão simples
quanto:
let mut vec = Vec::new();
for item in iter {
vec.push(item)
}
vec
Mas isso não é o ideal: à medida que o vetor cresce, pode ser
necessário expandir seu buffer, exigindo uma chamada para o
alocador de heap e uma cópia dos elementos existentes. Os vetores
tomam medidas algorítmicas para manter esse overhead baixo, mas,
se houvesse alguma maneira de simplesmente alocar um buffer do
tamanho certo para começar, não haveria necessidade de
redimensionar.
É aqui que entra o método size_hint do trait Iterator:
trait Iterator {
...
fn size_hint(&self) -> (usize, Option<usize>) {
(0, None)
}
}
Esse método retorna um limite inferior e um limite superior opcional
para o número de itens que o iterador produzirá. A definição padrão
retorna zero como o limite inferior e se recusa a nomear um limite
superior, dizendo, na verdade, “Não faço ideia”, mas muitos
iteradores podem fazer melhor do que isso. Um iterador sobre um
Range, por exemplo, sabe exatamente quantos itens produzirá, assim
como um iterador em um Vec ou HashMap. Esses iteradores fornecem
suas próprias definições especializadas para size_hint.
Esses limites são exatamente as informações de que a
implementação de FromIterator por Vec precisa para dimensionar o
buffer do novo vetor corretamente desde o início. As inserções ainda
verificam se o buffer é grande o suficiente; portanto, mesmo que a
dica esteja incorreta, apenas o desempenho é afetado, não a
segurança. Outros tipos podem seguir etapas semelhantes: por
exemplo, HashSet e HashMap também utilizam Iterator::size_hint para
escolher um tamanho inicial apropriado para sua tabela hash.
Uma observação sobre inferência de tipo: no início desta seção, é
um pouco estranho ver a mesma chamada, std::env::args().collect(),
produzir quatro tipos diferentes de coleções, dependendo de seu
contexto. O tipo de retorno de collect é seu parâmetro de tipo, então
as duas primeiras chamadas são equivalentes ao seguinte:
let args = std::env::args().collect::<Vec<String>>();
let args = std::env::args().collect::<HashSet<String>>();
Mas, enquanto houver apenas um tipo que poderia funcionar como
collect, a inferência de tipo do Rust vai fornecê-lo para você. Quando
você escrever explicitamente o tipo de args, garante que esse é o
caso.
Trait Extend
Se um tipo implementa o trait std::iter::Extend, então seu método extend
adiciona os itens de um iterável à coleção:
let mut v: Vec<i32> = (0..5).map(|i| 1 << i).collect();
v.extend([31, 57, 99, 163]);
assert_eq!(v, [1, 2, 4, 8, 16, 31, 57, 99, 163]);
Todas as coleções padrão implementam Extend, então todos eles têm
esse método; assim como String. Arrays e fatias, que têm um
comprimento fixo, não.
A definição do trait é a seguinte:
trait Extend<A> {
fn extend<T>(&mut self, iter: T)
where T: IntoIterator<Item=A>;
}
Obviamente, isso é muito parecido com std::iter::FromIterator: que cria
uma nova coleção, enquanto Extend estende uma coleção existente.
De fato, várias implementações de FromIterator na biblioteca padrão
simplesmente criam uma nova coleção vazia e chamam extend para
preenchê-la. Por exemplo, a implementação de FromIterator para
std::collections::LinkedList funciona assim:
impl<T> FromIterator<T> for LinkedList<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let mut list = Self::new();
list.extend(iter);
list
}
}
partition
O método partition divide os itens de um iterador entre duas coleções,
utilizando uma closure para decidir onde cada item deve entrar:
let things = ["doorknob", "mushroom", "noodle", "giraffe", "grapefruit"];
for_each e try_for_each
O método for_each simplesmente aplica uma closure a cada item:
["doves", "hens", "birds"].iter()
.zip(["turtle", "french", "calling"])
.zip(2..5)
.rev()
.map(|((item, kind), quantity)| {
format!("{} {} {}", quantity, kind, item)
})
.for_each(|gift| {
println!("You have received: {}", gift);
});
Isso imprime:
You have received: 4 calling birds
You have received: 3 french hens
You have received: 2 turtle doves
Isso é muito semelhante a um simples loop for, no qual você também
pode utilizar estruturas de controle como break e continue. Mas longas
cadeias de chamadas de adaptador como essa são um pouco
estranhas em loops for:
for gift in ["doves", "hens", "birds"].iter()
.zip(["turtle", "french", "calling"])
.zip(2..5)
.rev()
.map(|((item, kind), quantity)| {
format!("{} {} {}", quantity, kind, item)
})
{
println!("You have received: {}", gift);
}
O padrão sendo vinculado, gift, pode acabar bem longe do corpo do
loop no qual é utilizado.
Se sua closure precisa ser falível ou sair mais cedo, você pode
utilizar try_for_each:
...
.try_for_each(|gift| {
writeln!(&mut output_file, "You have received: {}", gift)
})?;
struct TreeNode<T> {
element: T,
left: BinaryTree<T>,
right: BinaryTree<T>
}
A maneira clássica de percorrer uma árvore binária é
recursivamente, utilizando o stack (pilha) de chamadas de função
para acompanhar seu lugar na árvore e os nós ainda a serem
visitados. Mas, ao implementar Iterator para BinaryTree<T>, cada
chamada a next deve produzir exatamente um valor e retornar. Para
acompanhar os nós da árvore que ele ainda não produziu, o iterador
deve manter seu próprio stack (pilha). Eis um tipo de iterador
possível para BinaryTree:
use self::BinaryTree::*;
// Depois de `node`, a próxima coisa que produzimos deve ser o filho mais à
// esquerda na subárvore direita de `node`, então inserimos o caminho daqui
// para baixo. Nosso método auxiliar acaba sendo exatamente o que precisamos
self.push_left_edge(&node.right);
Visão geral
A Tabela 16.1 mostra as oito coleções padrão do Rust. Todos elas
são tipos genéricos.
Tabela 16.1: Resumo das coleções padrão
Tipo de coleção semelhante em...
Coleção Descrição
C++ Java Python
Vec<T> Array expansível vector ArrayList list
VecDeque<T> Fila dupla (buffer circular deque ArrayDeque collections.dequ
expansível) e
LinkedList<T> Lista duplamente list LinkedList –
vinculada
BinaryHeap<T> Heap máximo priority_queue PriorityQueu heapq
where T: Ord e
HashMap<K, V> Tabela hash de chave- unordered_ma HashMap dict
where K: Eq + valor p
Hash
BTreeMap<K, V> Tabela de valores-chave map TreeMap –
where K: Ord ordenados
HashSet<T> Conjunto não ordenado, unordered_set HashSet set
where T: Eq + baseado em hash
Hash
BTreeSet<T> Conjunto ordenado set TreeSet –
where T: Ord
Acessando elementos
Obter elementos de um array, fatia ou vetor por índice é simples:
// Obtém uma referência a um elemento
let first_line = &lines[0];
// Obtém uma cópia de um elemento
let fifth_number = numbers[4]; // requer Copy
let second_line = lines[1].clone(); // requer Clone
// Obtém uma referência a uma fatia
let my_ref = &buffer[4..12];
// Obtém uma cópia de uma fatia
let my_copy = buffer[4..12].to_vec(); // requer Clone
Todas essas formas geram um pânico se um índice estiver fora dos
limites.
O Rust é exigente quanto a tipos numéricos e não faz exceções para
vetores. Comprimentos e índices vetoriais são do tipo usize. Tentar
utilizar um u32, u64 ou isize como um índice vetorial é um erro. Você
pode utilizar uma conversão n as usize para converter conforme
necessário; veja “Coerções de tipo”, na página 185.
Vários métodos fornecem acesso fácil a elementos específicos de um
vetor ou fatia (observe que todos os métodos de fatia também estão
disponíveis em arrays e vetores):
slice.first()
Retorna uma referência ao primeiro elemento de slice, caso existam.
O tipo de retorno é Option<&T>, então o valor de retorno é None se
slice estiver vazio e Some(&slice[0]) se não estiver vazio:
if let Some(item) = v.first() {
println!("We got one! {}", item);
}
slice.last()
Semelhante, mas retorna uma referência ao último elemento.
slice.get(index)
Retorna referência Some a slice[index], se existir. Se slice tem menos
que index+1 elementos, isso retorna None:
let slice = [0, 1, 2, 3];
assert_eq!(slice.get(2), Some(&2));
assert_eq!(slice.get(4), None);
Iteração
Vetores, arrays e fatias são iteráveis, seja por valor ou por
referência, seguindo o padrão descrito em “Implementações de
IntoIterator”, na página 405:
• Iterar por um Vec<T> ou array [T; N] produz itens do tipo T. Os
elementos são movidos para fora do vetor ou array um a um,
consumindo-o.
• Iterar por um valor do tipo &[T; N], &[T] ou &Vec<T> – isto é, uma
referência a um array, fatia ou vetor – produz itens do tipo &T,
referências aos elementos individuais, que não são movidos.
• Iterar por um valor do tipo &mut [T; N], &mut [T] ou &mut Vec<T>
produz itens do tipo &mut T.
Arrays, fatias e vetores também têm métodos .iter() e .iter_mut()
(descritos em “Métodos iter e iter_mut”, na página 404) para criar
iteradores que produzem referências a seus elementos.
Abordaremos algumas maneiras mais sofisticadas de iterar por uma
fatia em “Dividindo”, na página 461.
assert_eq!(&byte_vec, b"Misp");
Isso funciona porque .insert() retorna false quando o conjunto já
contém o item que estamos inserindo.
vec.dedup_by(same)
O mesmo que vec.dedup(),
mas utiliza a função ou closure same(&mut
elem1, &mut elem2), em vez de operador ==, para verificar se dois
elementos devem ser considerados iguais.
vec.dedup_by_key(key)
O mesmo que vec.dedup(),
mas trata dois elementos como iguais se
key(&mut elem1) == key(&mut elem2).
Por exemplo, se errors é um Vec<Box<dyn Error>>, você pode escrever:
// Remove erros com mensagens redundantes
errors.dedup_by_key(|err| err.to_string());
De todos os métodos abordados nesta seção, apenas .resize() nunca
clona valores. Os outros trabalham movendo valores de um lugar
para outro.
Unindo
Dois métodos funcionam em arrays de arrays, o que significa
qualquer array, fatia ou vetor cujos elementos sejam arrays, fatias
ou vetores:
slices.concat()
Retorna um novo vetor feito pela concatenação de todas as fatias:
assert_eq!([[1, 2], [3, 4], [5, 6]].concat(),
vec![1, 2, 3, 4, 5, 6]);
slices.join(&separator)
O mesmo, exceto que uma cópia do valor separator é inserida entre
as fatias:
assert_eq!([[1, 2], [3, 4], [5, 6]].join(&0),
vec![1, 2, 0, 3, 4, 0, 5, 6]);
Dividindo
É fácil acabar ficando com muitas referências não-mut em um array,
fatia ou vetor de uma só vez:
let v = vec![0, 1, 2, 3];
let a = &v[i];
let b = &v[j];
let mid = v.len() / 2;
let front_half = &v[..mid];
let back_half = &v[mid..];
Obter várias referências mut não é tão fácil:
let mut v = vec![0, 1, 2, 3];
let a = &mut v[i];
let b = &mut v[j]; // erro: não é possível emprestar `v` como mutável
// mais de uma vez por vez
Preenchimento
Existem dois métodos convenientes para substituir o conteúdo de
fatias mutáveis:
slice.fill(value)
Preenche a fatia com clones de value.
slice.fill_with(function)
Preenche a fatia com valores criados pela chamada da função dada.
Isso é especialmente útil para tipos que implementam Default, mas
não são Clone, como Option<T> ou Vec<T> quando T não é Clone.
Ordenando e pesquisando
As fatias oferecem três métodos de ordenação:
slice.sort()
Classifica os elementos em ordem crescente. Esse método está
presente apenas quando o tipo de elemento implementa Ord.
slice.sort_by(cmp)
Classifica os elementos de slice utilizando uma função ou closure cmp
para especificar a ordem de classificação. cmp deve implementar
Fn(&T, &T) -> std::cmp::Ordering.
A implementação manual de cmp é tediosa, a menos que você
delegue a um método .cmp():
students.sort_by(|a, b| a.last_name.cmp(&b.last_name));
Para ordenar por um campo, utilizando um segundo campo como
desempate, compare as tuplas:
students.sort_by(|a, b| {
let a_key = (&a.last_name, &a.first_name);
let b_key = (&b.last_name, &b.first_name);
a_key.cmp(&b_key)
});
slice.sort_by_key(key)
Classifica os elementos de slice em ordem crescente por uma chave
de ordenação, dada pela função ou closure key. O tipo de key deve
implementar Fn(&T) -> K em que K: Ord.
Isso é útil quando T contém um ou mais campos ordenáveis, de
forma que possa ser ordenada de várias maneiras:
// Classifica por média de notas, a mais baixa primeiro
students.sort_by_key(|s| s.grade_point_average());
Observe que esses valores de chave de ordenação não são
armazenados em cache durante a ordenação, portanto a função key
pode ser chamada mais de n vezes.
Por razões técnicas, key(element) não pode retornar nenhuma
referência emprestada do elemento. Isso não vai funcionar:
students.sort_by_key(|s| &s.last_name); // erro: não é possível inferir o tempo de vida
O Rust não consegue calcular os tempos de vida. Mas, nesses
casos, é fácil recorrer a .sort_by().
Todos os três métodos executam uma ordenação estável.
Para ordenar na ordem inversa, você pode utilizar sort_by com uma
closure cmp que troca os dois argumentos. Tomar argumentos |b, a|
em vez de |a, b| efetivamente produz a ordem oposta. Ou você pode
simplesmente chamar para o método .reverse() após a ordenação:
slice.reverse()
Inverte uma fatia no mesmo lugar (sem alocar memória adicional).
Depois que uma fatia é ordenada, ela pode ser pesquisada com
eficiência:
slice.binary_search(&value), slice.binary_search_by(&value, cmp),
slice.binary_search_by_key(&value, key)
Todas as buscas por value no dado slice ordenado. Observe que value
é passado por referência.
O tipo de retorno desses métodos é Result<usize, usize>. Eles retornam
Ok(index) se slice[index] for igual a value na ordem de classificação
especificada. Se não houver esse índice, eles retornam
Err(insertion_point) de forma que inserir value em insertion_point preservaria
a ordem.
Obviamente, uma pesquisa binária só funciona se a fatia estiver de
fato ordenada na ordem especificada. Caso contrário, os resultados
são arbitrários – entra lixo, sai lixo.
Como f32 e f64 têm valores NaN, eles não implementam Ord e não
podem ser utilizados diretamente como chaves com os métodos de
ordenação e pesquisa binária. Para obter métodos semelhantes que
funcionam em dados de ponto flutuante, utilize o crate ord_subset.
Existe um método para pesquisar um vetor que não está ordenado:
slice.contains(&value)
Retorna true se algum elemento de slice é igual a value. Isso
simplesmente verifica cada elemento da fatia até que uma
correspondência seja encontrada. Novamente, value é passado por
referência.
Para encontrar a localização de um valor em uma fatia, como
array.indexOf(value) em JavaScript, utilize um iterador:
slice.iter().position(|x| *x == value)
Isso retorna um Option<usize>.
Comparando fatias
Se um tipo T suporta os operadores == e != (o trait PartialEq, descrito
em “Comparações de equivalência”, na página 341), então arrays
[T; N], fatias [T] e vetores Vec<T> também os suportam. Duas fatias
são iguais se tiverem o mesmo comprimento e seus elementos
correspondentes forem iguais. O mesmo vale para arrays e vetores.
Se T suporta os operadores <, <=, > e >= (o trait PartialOrd, descrito
em “Comparações Ordenadas”, na página 345), então arrays, fatias
e vetores de T também os suportam. As comparações de fatias são
lexicográficas.
Dois métodos de conveniência realizam comparações comuns de
fatias:
slice.starts_with(other)
Retorna true se slice começa com uma sequência de valores que são
iguais aos elementos da fatia other:
assert_eq!([1, 2, 3, 4].starts_with(&[1, 2]), true);
assert_eq!([1, 2, 3, 4].starts_with(&[2, 3]), false);
slice.ends_with(other)
Semelhante, mas verifica o final de slice:
assert_eq!([1, 2, 3, 4].ends_with(&[3, 4]), true);
Elementos aleatórios
Números aleatórios não são integrados na biblioteca padrão do Rust.
O crate rand, que os fornece, oferece esses dois métodos para obter
saída aleatória de um array, fatia ou vetor:
slice.choose(&mut rng)
Retorna uma referência a um elemento aleatório de uma fatia.
Assim como slice.first() e slice.last(), isso retorna um Option<&T> que é
None somente se a fatia estiver vazia.
slice.shuffle(&mut rng)
Reordena aleatoriamente os elementos de uma fatia no lugar. A
fatia deve ser passada por referência mut.
Estes são métodos do trait rand::Rng, então você precisa de um Rng,
um gerador de números aleatórios, para chamá-los. Felizmente, é
fácil conseguir um chamando rand::thread_rng(). Para embaralhar o
vetor my_vec, podemos escrever:
use rand::seq::SliceRandom;
use rand::thread_rng;
my_vec.shuffle(&mut thread_rng());
O Rust descarta erros de invalidação
A maioria das linguagens de programação convencionais tem
coleções e iteradores e todas elas têm alguma variação nesta regra:
não modifique uma coleção enquanto estiver iterando por ela. Por
exemplo, o equivalente Python de um vetor é uma lista:
my_list = [1, 3, 5, 7, 9]
Suponha que tentamos remover todos os valores maiores que 4 de
my_list:
for index, val in enumerate(my_list):
if val > 4:
del my_list[index] # bug: modificando a lista durante a iteração
print(my_list)
(A função enumerate é o equivalente do Python do Rust método
.enumerate(), descrito em “enumerate”, na página 426.)
Esse programa, surpreendentemente, imprime [1, 3, 7]. Mas sete é
maior que quatro. Como isso escapou? Esse é um erro de
invalidação: o programa modifica os dados enquanto itera sobre
eles, invalidando o iterador. Em Java, o resultado seria uma exceção;
em C++ é um comportamento indefinido. Em Python, embora o
comportamento seja bem definido, não é intuitivo: o iterador pula
um elemento. val nunca é 7.
Vamos tentar reproduzir esse bug no Rust:
fn main() {
let mut my_vec = vec![1, 3, 5, 7, 9];
VecDeque<T>
Vecsuporta adicionar e remover elementos de forma eficiente apenas
no final. Quando um programa precisa de um local para armazenar
valores que estão “esperando na fila”, Vec pode ser lento.
O Rust std::collections::VecDeque<T> é um deque, uma fila de duas
extremidades. Ele suporta operações eficientes de adicionar e
remover na frente e atrás:
deque.push_front(value)
Adiciona um valor na frente da fila.
deque.push_back(value)
Adiciona um valor no final. (Esse método é utilizado muito mais do
que .push_front(), porque a convenção usual para filas é que os
valores são adicionados na parte de trás e removidos na frente,
como pessoas esperando em uma fila.)
deque.pop_front()
Remove e retorna o valor da frente da fila, retornando um Option<T>
que é None se a fila estiver vazia, como vec.pop().
deque.pop_back()
Remove e retorna o valor na parte de trás, retornando novamente
um Option<T>.
deque.front(), deque.back()
Funciona como vec.first() e vec.last(). Retornam uma referência ao
elemento no início ou no fim da fila. O valor de retorno é um
Option<&T> que é None se a fila estiver vazia.
deque.front_mut(), deque.back_mut()
Funciona como vec.first_mut() e vec.last_mut(), retornando Option<&mut T>.
A implementação de VecDeque é um buffer circular, conforme
mostrado na Figura 16.3.
Como um Vec, possui uma única alocação de heap onde os
elementos são armazenados. Diferente de Vec, os dados nem sempre
começam no início dessa região e podem “contornar” no final,
conforme mostrado. Os elementos desse deque, em ordem, são ['A',
'B', 'C', 'D', 'E']. VecDeque tem campos privados, rotulados start e stop na
figura, que ele utiliza para lembrar onde no buffer os dados
começam e terminam.
Adicionar um valor à fila, em qualquer extremidade, significa
reivindicar um dos slots não utilizados, ilustrados como os blocos
mais escuros, contornando ou alocando um pedaço maior de
memória, se necessário.
VecDeque gerencia o contorno, para que você não precise pensar
nisso. A Figura 16.3 é uma visão dos bastidores de como Rust torna
.pop_front() velozes.
BinaryHeap<T>
Um BinaryHeap é uma coleção cujos elementos são mantidos
frouxamente organizados para que o maior valor sempre apareça na
frente da fila. Eis os três métodos mais utilizados de BinaryHeap:
heap.push(value)
Adiciona um valor ao heap.
heap.pop()
Remove e retorna o maior valor do heap. Retorna um Option<T> que
é None se o heap estiver vazio.
heap.peek()
Retorna uma referência ao maior valor no heap. O tipo de retorno é
Option<&T>.
heap.peek_mut()
Retorna um PeekMut<T>, que atua como uma referência mutável ao
maior valor no heap e fornece a função associada ao tipo pop() para
remover esse valor do heap. Utilizando esse método, podemos
escolher remover ou não remover do heap com base no valor
máximo:
use std::collections::binary_heap::PeekMut;
if let Some(top) = heap.peek_mut() {
if *top > 10 {
PeekMut::pop(top);
}
}
BinaryHeaptambém suporta um subconjunto dos métodos em Vec,
incluindo BinaryHeap::new(), .len(), .is_empty(), .capacity(), .clear() e .append(&mut
heap2).
Por exemplo, suponha que preenchemos um BinaryHeap com um
monte de números:
use std::collections::BinaryHeap;
let mut heap = BinaryHeap::from(vec![2, 3, 8, 6, 9, 5, 4]);
O valor 9 está na parte superior do heap:
assert_eq!(heap.peek(), Some(&9));
assert_eq!(heap.pop(), Some(9));
Remover o valor 9 também reorganiza ligeiramente os outros
elementos de tal modo que 8 agora está na frente, e assim por
diante:
assert_eq!(heap.pop(), Some(8));
assert_eq!(heap.pop(), Some(6));
assert_eq!(heap.pop(), Some(5));
...
É claro, BinaryHeap não se limita a números. Ele pode conter qualquer
tipo de valor que implemente o trait Ord interno.
Isso torna BinaryHeap útil como uma fila de trabalho. Você pode definir
um struct de tarefa que implementa Ord com base na prioridade para
que as tarefas de maior prioridade tenham precedência (Greater)
sobre as tarefas de menor prioridade. Em seguida, crie um BinaryHeap
para manter todas as tarefas pendentes. Seu método .pop() sempre
retornará o item mais importante, a tarefa na qual seu programa
deve trabalhar a seguir.
Observação: BinaryHeap é iterável e tem um método .iter(), mas os
iteradores produzem os elementos do heap em uma ordem
arbitrária, não do maior para o menor. Para consumir valores de um
BinaryHeap em ordem de prioridade, utilize um loop while:
while let Some(task) = heap.pop() {
handle(task);
}
Entradas
Ambos HashMap e BTreeMap têm um tipo Entry correspondente. O
objetivo das entradas é eliminar pesquisas de mapa redundantes.
Por exemplo, eis um código para obter ou criar um registro de aluno:
// Já temos um registro para esse aluno?
if !student_map.contains_key(name) {
// Não: crie um
student_map.insert(name.to_string(), Student::new());
}
// Agora um registro definitivamente existe
let record = student_map.get_mut(name).unwrap();
...
Isso funciona bem, mas acessa student_map duas ou três vezes,
fazendo sempre a mesma pesquisa.
A ideia com as entradas é que façamos a pesquisa apenas uma vez,
produzindo um valor Entry que é então utilizado para todas as
operações subsequentes. Esse código de uma única linha é
equivalente a todo o código anterior, exceto que faz a pesquisa
apenas uma vez:
let record = student_map.entry(name.to_string()).or_insert_with(Student::new);
O valor Entry retornado por student_map.entry(name.to_string()) age como
uma referência mutável a um local dentro do mapa que está
ocupado por um par chave-valor ou vago, o que significa que ainda
não há entrada lá. Se estiver vago, o método .or_insert_with() da
entrada insere um novo Student. A maioria dos usos de entradas é
assim: curto e eficaz.
Todos os valores Entry são criados pelo mesmo método:
map.entry(key)
Retorna uma Entry para a dada key. Se não houver tal chave no
mapa, isso retornará uma Entry vaga.
Esse método pega seu argumento self por referência mut e retorna
uma Entry com um tempo de vida correspondente:
pub fn entry<'a>(&'a mut self, key: K) -> Entry<'a, K, V>
O tipo Entry tem um parâmetro de tempo de vida 'a porque é
efetivamente um tipo sofisticado de referência mut emprestada ao
mapa. Contanto que Entry exista, o método tem acesso exclusivo ao
mapa.
De volta a “Structs contendo referências”, na página 147, vimos
como armazenar referências em um tipo e como isso afeta os
tempos de vida. Agora estamos vendo como isso se parece da
perspectiva do usuário. Isso é o que está acontecendo com Entry.
Infelizmente, não é possível passar uma referência do tipo &str a
esse método se o mapa tiver chaves String. O método .entry(), nesse
caso, requer uma String real.
Valores de Entry fornecem três métodos para lidar com entradas
vagas:
map.entry(key).or_insert(value)
Assegura que map contém uma entrada com a dada key, inserindo
uma nova entrada com o dado value se necessário. Ele retorna uma
referência mut ao valor novo ou existente.
Suponha que precisamos contar votos. Podemos escrever:
let mut vote_counts: HashMap<String, usize> = HashMap::new();
for name in ballots {
let count = vote_counts.entry(name).or_insert(0);
*count += 1;
}
.or_insert() retorna uma referência mut, então o tipo de count é &mut
usize.
map.entry(key).or_default()
Assegura que map contém uma entrada com a chave fornecida,
inserindo uma nova entrada com o valor retornado por
Default::default() se necessário. Isso só funciona para tipos que
implementam Default. Como or_insert, esse método retorna uma
referência mut ao valor novo ou existente.
map.entry(key).or_insert_with(default_fn)
Isso é o mesmo, exceto que, se precisar criar uma nova entrada,
chama default_fn() para produzir o valor padrão. Se já houver uma
entrada para key no map, então default_fn não é utilizado.
Suponha que queremos saber quais palavras aparecem em quais
arquivos. Podemos escrever:
// Esse mapa contém, para cada palavra, o conjunto de arquivos em que ela aparece
let mut word_occurrence: HashMap<String, HashSet<String>> =
HashMap::new();
for file in files {
for word in read_words(file)? {
let set = word_occurrence
.entry(word)
.or_insert_with(HashSet::new);
set.insert(file.clone());
}
}
Entrytambém fornece uma maneira conveniente de modificar apenas
os campos existentes.
map.entry(key).and_modify(closure)
Chama closure se uma entrada com a chave key existir, passando uma
referência mutável ao valor. Retorna a Entry, para que seja
encadeada com outros métodos.
Por exemplo, poderíamos utilizar isso para contar o número de
ocorrências de palavras em uma string:
// Esse mapa contém todas as palavras em uma determinada string,
// com o número de vezes que ocorrem
let mut word_frequency: HashMap<&str, u32> = HashMap::new();
for c in text.split_whitespace() {
word_frequency.entry(c)
.and_modify(|count| *count += 1)
.or_insert(1);
}
O tipo Entry é um enum, definido assim para HashMap (e da mesma
forma para BTreeMap):
// (in std::collections::hash_map)
pub enum Entry<'a, K, V> {
Occupied(OccupiedEntry<'a, K, V>),
Vacant(VacantEntry<'a, K, V>)
}
Os tipos OccupiedEntry e VacantEntry têm métodos para inserir, remover e
acessar entradas sem repetir a pesquisa inicial. Você pode encontrá-
los na documentação on-line. Os métodos extras podem
ocasionalmente ser utilizados para eliminar uma ou duas pesquisas
redundantes, mas .or_insert() e .or_insert_with() cobrem os casos comuns.
Iteração em mapas
Há várias maneiras de iterar por um mapa:
• Iterar por valor (for (k, v) in map) produz pares (K, V). Isso consome o
mapa.
• Iterar por uma referência compartilhada (for (k, v) in &map) produz
pares (&K, &V).
• Iterar por uma referência mut (for (k, v) in &mut map) produz pares (&K,
&mut V). (Novamente, não há como obter acesso mut às chaves
armazenadas em um mapa, pois as entradas são organizadas por
suas chaves.)
Assim como os vetores, os mapas têm métodos .iter() e .iter_mut() que
retornam iteradores por referência, exatamente como iterar por &map
ou &mut map. Além disso:
map.keys()
Retorna um iterador apenas sobre as chaves, por referência.
map.values()
Retorna um iterador sobre os valores, por referência.
map.values_mut()
Retorna um iterador sobre os valores, por referência mut.
HashSet<T> e BTreeSet<T>
Conjuntos são coleções de valores organizados para testes rápidos
de associação:
let b1 = large_vector.contains(&"needle"); // lento, verifica todos os elementos
let b2 = large_hash_set.contains(&"needle"); // rápido, pesquisa em hash
Um conjunto nunca contém várias cópias do mesmo valor.
Mapas e conjuntos têm métodos diferentes, mas, nos bastidores, um
conjunto é como um mapa com apenas chaves, em vez de pares
chave-valor. Na verdade, os dois tipos de conjunto do Rust,
HashSet<T>e BTreeSet<T>, são implementados como encapsuladores
simples em torno de HashMap<T, ()> e BTreeMap<T, ()>.
HashSet::new(), BTreeSet::new()
Cria novos conjuntos.
iter.collect()
Pode ser utilizado para criar um novo conjunto a partir de qualquer
iterador. Se iter produz qualquer valor mais de uma vez, as
duplicatas são dropadas.
HashSet::with_capacity(n)
Cria um HashSet vazio com espaço para pelo menos n valores.
HashSet<T> e BTreeSet<T> têm todos os métodos básicos em comum:
set.len()
Retorna o número de valores em set.
set.is_empty()
Retorna true se o conjunto não contém elementos.
set.contains(&value)
Retorna true se o conjunto contém o dado value.
set.insert(value)
Adiciona um value ao conjunto. Retorna true se um valor foi
adicionado, false se já era um membro do conjunto.
set.remove(&value)
Remove um value do conjunto. Retorna true se um valor foi
removido, false se já não era um membro do conjunto.
set.retain(test)
Remove todos os elementos que não passam no teste fornecido. O
argumento test é uma função ou closure que implementa FnMut(&T) ->
bool. Para cada elemento de set, isso chama test(&value) e, se retornar
false, o elemento é removido do conjunto e dropado.
Exceto pelo desempenho, é como escrever:
set = set.into_iter().filter(test).collect();
Assim como nos mapas, os métodos que procuram um valor por
referência são tipos genéricos que podem ser emprestados de T.
Para detalhes, consulte “Borrow e BorrowMut”, na página 369.
Iteração em conjuntos
Há duas maneiras de iterar por conjuntos:
• Iterar por valor (“for v in set”) produz os membros do conjunto (e
consome o conjunto).
• Iterar por referência compartilhada (“for v in &set”) produz
referências compartilhadas aos membros do conjunto.
A iteração de um conjunto por referência mut não é suportada. Não
há como obter uma referência mut a um valor armazenado em um
conjunto.
set.iter()
Retorna um iterador sobre os membros de set por referência.
Iteradores de HashSet, como iteradores de HashMap, produzem seus
valores em uma ordem arbitrária. Iteradores de BTreeSet produzem
valores em ordem, como um vetor ordenado.
Hash
é o trait de biblioteca padrão para tipos hasheáveis.
std::hash::Hash
Chaves HashMap e elementos HashSet devem implementar tanto Hash
como Eq.
A maioria dos tipos internos que implementam Eq também
implementa Hash. Os tipos inteiros, char e String são todos hasheáveis;
assim como tuplas, arrays, fatias e vetores, desde que seus
elementos sejam passíveis de hash.
Um princípio da biblioteca padrão é que um valor deve ter o mesmo
código hash independentemente de onde você o armazene ou de
como você o aponte. Portanto, uma referência tem o mesmo código
hash do valor que ela referencia e um Box tem o mesmo código hash
que o valor empacotado. Um vetor vec tem o mesmo código hash da
fatia que contém todos os seus dados, &vec[..]. Uma String tem o
mesmo código hash que uma &str com os mesmos caracteres.
Estruturas e enums não implementam Hash por padrão, mas uma
implementação pode ser derivada:
/// O número de identificação de um objeto na coleção do Museu Britânico
#[derive(Clone, PartialEq, Eq, Hash)]
enum MuseumNumber {
...
}
Isso funciona desde que os campos do tipo sejam todos hasheáveis.
Se você implementar PartialEq manualmente para um tipo, também
deve implementar Hash à mão. Por exemplo, suponha que temos um
tipo que representa tesouros históricos inestimáveis:
struct Artifact {
id: MuseumNumber,
name: String,
cultures: Vec<Culture>,
date: RoughTime,
...
}
Dois Artifacts são considerados iguais se tiverem o mesmo ID:
impl PartialEq for Artifact {
fn eq(&self, other: &Artifact) -> bool {
self.id == other.id
}
}
impl Eq for Artifact {}
Como comparamos artefatos puramente com base em seu ID,
devemos fazer o hash deles da mesma maneira:
use std::hash::{Hash, Hasher};
impl Hash for Artifact {
fn hash<H: Hasher>(&self, hasher: &mut H) {
// Delega o hashing para o MuseumNumber
self.id.hash(hasher);
}
}
(Caso contrário, HashSet<Artifact> não funcionaria corretamente; como
em todas as tabelas hash, ela requer que hash(a) == hash(b) se a == b.)
Isso nos permite criar um HashSet de Artifacts:
let mut collection = HashSet::<Artifact>::new();
Como mostra esse código, mesmo quando implementamos Hash
manualmente, você não precisa saber nada sobre algoritmos hash.
.hash() recebe uma referência a um Hasher, que representa o algoritmo
de hash. Você simplesmente alimenta esse Hasher com todos os
dados relevantes para o operador ==. O Hasher calcula um código
hash para qualquer coisa que você fornecer.
UTF-8
Os tipos String e str do Rust representam texto utilizando a forma de
codificação UTF-8. UTF-8 codifica um caractere como uma sequência
de um a quatro bytes (Figura 17.1).
Figura 17.1: Codificação UTF-8.
Há duas restrições em sequências UTF-8 bem formadas. Primeiro,
apenas a codificação mais curta para qualquer ponto de código é
considerada bem formada; você não pode gastar quatro bytes
codificando um ponto de código que caberia em três. Essa regra
garante que haja exatamente uma codificação UTF-8 para um
determinado ponto de código. Em segundo lugar, um UTF-8 bem
formado não deve codificar números de 0xd800 a 0xdfff ou além de
0x10ffff: esses são reservados para fins não caractere ou totalmente
fora do intervalo do Unicode.
A Figura 17.2 mostra alguns exemplos.
Direcionalidade do texto
Enquanto textos em latim, cirílico e tailandês são escritos da
esquerda para a direita, outros textos como em hebraico e árabe são
escritos da direita para a esquerda. O Unicode armazena os
caracteres na ordem em que normalmente seriam escritos ou lidos,
de modo que os bytes iniciais de uma string contendo, digamos,
texto em hebraico codificam o caractere que seria escrito à direita:
assert_eq!(""ערב טוב.chars().next(), Some(';))'ע
Caracteres (char)
Um char do Rust é um valor de 32 bits contendo um ponto de código
Unicode. Garante-se que um char caia no intervalo de 0 a 0xd7ff ou no
intervalo 0xe000 a 0x10ffff; todos os métodos para criar e manipular os
valores garantem que isso seja verdade. O tipo char implementa Copy
e Clone, com todos os traits usuais para comparação, hashing e
formatação.
Uma fatia de string pode produzir um iterador sobre seus caracteres
com slice.chars():
assert_eq!("カニ".chars().next(), Some('カ'));
Nas descrições a seguir, a variável ch é sempre do tipo char.
Classificando caracteres
O tipo char tem métodos para classificar caracteres em algumas
categorias comuns, conforme listado na Tabela 17.1. Todos eles
derivam suas definições do Unicode.
Tabela 17.1: Métodos de classificação para tipo char
Método Descrição Exemplos
ch.is_numeric() Um caractere numérico. Isso inclui as '4'.is_numeric()
categorias gerais do Unicode “Number; digit” '⌉'.is_numeric()
e “Number; letter” mas não “Number; other”. '⑧'.is_numeric()
ch.is_alphabetic() Um caractere alfabético: Propriedade 'q'.is_alphabetic()
“Alphabetic” derivada do Unicode. '七'.is_alphabetic()
ch.is_alphanumeric( Numérico ou alfabético, conforme definido '9'.is_alphanumeric()
) anteriormente. '饂'.is_alphanumeric()
!'*'.is_alphanumeric()
ch.is_whitespace() Um caractere de espaço em branco: ' '.is_whitespace()
Propriedade de caractere Unicode '\n'.is_whitespace()
“WSpace=Y”. '\u{A0}'.is_whitespace(
)
ch.is_control() Um caractere de controle: Categoria geral '\n'.is_control()
“Other, control” do Unicode. '\u{85}'.is_control()
Manipulando dígitos
Para lidar com dígitos, você pode utilizar os seguintes métodos:
ch.to_digit(radix)
Decide se ch é um dígito na base radix. Se for, ele retorna Some(num),
em que num é um u32. Caso contrário, retorna None. Isso reconhece
apenas dígitos ASCII, não a classe mais ampla de caracteres
abrangida por char::is_numeric. O parâmetro radix pode variar de 2 a
36. Para bases maiores que 10, as letras ASCII de qualquer um dos
casos são consideradas dígitos com valores de 10 a 35.
std::char::from_digit(num, radix)
Uma função que converte o valor u32 do dígito num para um char se
possível. Se num pode ser representado como um único dígito em
radix, from_digit retorna Some(ch), em que ch é o dígito. Quando radix é
maior que 10, ch pode ser uma letra minúscula. Caso contrário,
retorna None.
Esse é o inverso de to_digit. Se std::char::from_digit(num, radix) é Some(ch),
então ch.to_digit(radix) é Some(num). Se ch é um dígito ASCII ou uma
letra minúscula, o inverso também é válido.
ch.is_digit(radix)
Retorna true se ch é um dígito ASCII na base radix. Isso é equivalente
a ch.to_digit(radix) != None.
Assim, por exemplo:
assert_eq!('F'.to_digit(16), Some(15));
assert_eq!(std::char::from_digit(15, 16), Some('f'));
assert!(char::is_digit('f', 16));
String e str
Os tipos String e str do Rust têm a garantia de conter apenas UTF-8
bem formado. A biblioteca garante isso restringindo as formas que
você pode criar valores String e str e as operações que você pode
executar neles, de modo que os valores sejam bem formados
quando introduzidos e permaneçam assim enquanto você trabalha
com eles. Todos os seus métodos protegem essa garantia: nenhuma
operação segura neles pode introduzir UTF-8 malformado. Isso
simplifica o código que funciona com o texto.
O Rust coloca métodos de manipulação de texto em qualquer str ou
String dependendo se o método precisa de um buffer redimensionável
ou contenta-se com apenas utilizar o texto no local. Como String
desreferencia para &str, todo método definido em str também está
disponível diretamente em String. Essa seção apresenta métodos de
ambos os tipos, agrupados por funcionalidade.
Esses métodos indexam o texto por posição de byte e medem seu
comprimento em bytes, em vez de caracteres. Na prática, dada a
natureza do Unicode, a indexação por caractere não é tão útil
quanto parece e os posições de bytes são mais rápidos e simples. Se
você tentar utilizar uma posição de byte que caia no meio da
codificação UTF-8 de algum caractere, o método gera um pânico,
portanto você não pode introduzir UTF-8 malformado dessa maneira.
Uma String é implementada como uma capa em torno de um Vec<u8>,
o que garante que o conteúdo do vetor seja sempre UTF-8 bem
formado. O Rust nunca mudará String para utilizar uma representação
mais complicada, então você pode assumir que String compartilha
características de desempenho de Vec.
Nessas explicações, as variáveis têm os tipos dados na Tabela 17.3.
Tabela 17.3: Tipos de variáveis utilizadas nas explicações
Variáv
Tipo presumido
el
string String
Variáv
Tipo presumido
el
slice &str ou algo que não referencia uma, como String ou Rc<String>
ch char
n usize, um comprimento
i, j usize, um deslocamento de byte
range Uma variedade de usize deslocamentos de byte, totalmente delimitados como i..j,
ou parcialmente delimitados como i.., ..j ou ..
patter Qualquer tipo de padrão: char, String, &str, &[char] ou FnMut(char) -> bool
n
Inspeção simples
Esses métodos obtêm informações básicas de fatias de string:
slice.len()
O comprimento do slice, em bytes.
slice.is_empty()
Verdadeiro se slice.len() == 0.
slice[range]
Retorna uma fatia emprestando a porção dada de slice. Intervalos
parcialmente limitados e ilimitados estão OK; por exemplo:
let full = "bookkeeping";
assert_eq!(&full[..4], "book");
assert_eq!(&full[5..], "eeping");
assert_eq!(&full[2..4], "ok");
assert_eq!(full[..].len(), 11);
assert_eq!(full[5..].contains("boo"), false);
Observe que você não pode indexar uma fatia de string com uma
única posição, como slice[i]. Buscar um único caractere em um
determinado deslocamento de byte é um pouco desajeitado: você
deve produzir um iterador de chars sobre a fatia e pedir para analisar
o UTF-8 de um caractere:
let parenthesized = "Rust (饂)";
assert_eq!(parenthesized[6..].chars().next(), Some('饂'));
Contudo, você raramente precisa fazer isso. O Rust tem maneiras
muito melhores de iterar por fatias, que descrevemos em “Iterando
por texto”, na página 504.
slice.split_at(i)
Retorna uma tupla de duas fatias compartilhadas emprestadas de
slice: a porção até o deslocamento de byte i e a parte depois dela.
Em outras palavras, isso retorna (slice[..i], slice[i..]).
slice.is_char_boundary(i)
Verdadeiro se o deslocamento de bytes i cai entre os limites de
caracteres e, portanto, é adequado como um deslocamento para
slice.
Naturalmente, as fatias podem ser comparadas quanto à igualdade,
ordenadas e hasheadas. A comparação ordenada simplesmente trata
a string como uma sequência de pontos de código Unicode e os
compara em ordem lexicográfica.
Pesquisando e substituindo
O Rust tem alguns métodos para procurar padrões em fatias e
possivelmente os substituir por um novo texto:
slice.contains(pattern)
Retorna verdadeiro se slice contém uma correspondência para pattern.
slice.starts_with(pattern), slice.ends_with(pattern)
Retorna verdadeiro se, no slice, o texto inicial ou final corresponde a
pattern:
assert!("2017".starts_with(char::is_numeric));
slice.find(pattern), slice.rfind(pattern)
Retorna Some(i) se slice contém uma correspondência para pattern, em
que i é o deslocamento de byte no qual o padrão aparece. O
método find retorna a primeira correspondência, rfind o último:
let quip = "We also know there are known unknowns";
assert_eq!(quip.find("know"), Some(8));
assert_eq!(quip.rfind("know"), Some(31));
assert_eq!(quip.find("ya know"), None);
assert_eq!(quip.rfind(char::is_uppercase), Some(0));
slice.replace(pattern, replacement)
Retorna uma nova String formada pela substituição “gulosa” de todas
as correspondências de pattern por replacement:
assert_eq!("The only thing we have to fear is fear itself"
.replace("fear", "spin"),
"The only thing we have to spin is spin itself");
assert_eq!("`Borrow` and `BorrowMut`"
.replace(|ch:char| !ch.is_alphanumeric(), ""),
"BorrowandBorrowMut");
Como a substituição é “gulosa”, o comportamento de .replace() em
correspondências sobrepostas pode ser surpreendente. Aqui, há
quatro instâncias do padrão, "aba", mas o segundo e o quarto não
correspondem mais depois que o primeiro e o terceiro são
substituídos:
assert_eq!("cabababababbage"
.replace("aba", "***"),
"c***b***babbage")
slice.replacen(pattern, replacement, n)
Isso faz o mesmo, mas substitui no máximo as primeiras n
correspondências.
Iterando por texto
A biblioteca padrão fornece várias maneiras de iterar pelo texto de
uma fatia. A Figura 17.3 mostra alguns exemplos.
Você pode pensar nas famílias split e match como sendo
complementares umas das outras: as divisões são os intervalos entre
as correspondências.
A maioria desses métodos retorna iteradores que são reversíveis
(isto é, eles implementam DoubleEndedIterator): chamando seu método
de adaptador .rev() fornece um iterador que produz os mesmos itens,
mas na ordem inversa.
slice.chars()
Retorna um iterador sobre os caracteres de slice.
slice.char_indices()
Retorna um iterador sobre slice e seus deslocamentos de bytes:
assert_eq!("élan".char_indices().collect::<Vec<_>>(),
vec![(0, 'é'), // tem uma codificação UTF-8 de dois bytes
(2, 'l'),
(3, 'a'),
(4, 'n')]);
Observe que isso não é equivalente a .chars().enumerate(), pois fornece
o deslocamento de byte de cada caractere dentro da fatia, em vez
de apenas numerar os caracteres.
Figura 17.3: Algumas maneiras de iterar por uma fatia.
slice.bytes()
Retorna um iterador sobre os bytes individuais de slice, expondo a
codificação UTF-8:
assert_eq!("élan".bytes().collect::<Vec<_>>(),
vec![195, 169, b'l', b'a', b'n']);
slice.lines()
Retorna um iterador sobre as linhas de slice. As linhas são
terminadas por "\n" ou "\r\n". Cada item gerado é uma &str pedindo
emprestado de slice. Os itens não incluem os caracteres de final de
linha.
slice.split(pattern)
Retorna um iterador sobre as partes de slice separados por
correspondências de pattern. Isso produz strings vazias entre
correspondências imediatamente adjacentes, bem como para
correspondências no início e no final de slice.
O iterador retornado não é reversível se pattern é uma &str. Esses
padrões podem produzir diferentes sequências de
correspondências, dependendo de qual direção você varre, o que os
iteradores reversíveis são proibidos de fazer. Em vez disso, você
pode utilizar o método rsplit, descrito a seguir.
slice.rsplit(pattern)
Esse método é o mesmo, mas varre slice do fim para o início,
produzindo correspondências nessa ordem.
slice.split_terminator(pattern), slice.rsplit_terminator(pattern)
Estes são semelhantes, exceto que o padrão pattern é tratado como
um terminador, não um separador: se pattern corresponde ao final de
slice, os iteradores não produzem uma fatia vazia representando a
string vazia entre essa correspondência e o final da fatia, como split
e rsplit fazem. Por exemplo:
// Os caracteres ':' são separadores aqui. Observe o "" final
assert_eq!("jimb:1000:Jim Blandy:".split(':').collect::<Vec<_>>(),
vec!["jimb", "1000", "Jim Blandy", ""]);
// Os caracteres '\n' são terminadores aqui
assert_eq!("127.0.0.1 localhost\n\
127.0.0.1 www.reddit.com\n"
.split_terminator('\n').collect::<Vec<_>>(),
vec!["127.0.0.1 localhost",
"127.0.0.1 www.reddit.com"]);
// Note, sem "" final!
slice.split_whitespace(), slice.split_ascii_whitespace()
Retorna um iterador sobre as partes separadas por espaços em
branco de slice. Uma sequência de vários caracteres de espaço em
branco é considerada um único separador. O espaço em branco à
direita é ignorado.
O método split_whitespace utiliza a definição Unicode de espaço em
branco, conforme implementado pelo método is_whitespace em char.
Em vez disso, o método split_ascii_whitespace utiliza char::is_ascii_whitespace,
que reconhece apenas caracteres de espaço em branco ASCII.
let poem = "This is just to say\n\
I have eaten\n\
the plums\n\
again\n";
assert_eq!(poem.split_whitespace().collect::<Vec<_>>(),
vec!["This", "is", "just", "to", "say",
"I", "have", "eaten", "the", "plums",
"again"]);
slice.matches(pattern)
Retorna um iterador sobre as correspondências para pattern em fatia.
slice.rmatches(pattern) é o mesmo, mas itera do fim para o início.
slice.match_indices(pattern), slice.rmatch_indices(pattern)
Estes são semelhantes, exceto que os itens gerados são pares
(offset, match), e que offset é o deslocamento de byte no qual a
correspondência começa e match é a fatia correspondente.
slice.strip_prefix(pattern), slice.strip_suffix(pattern)
Se slice começa com pattern, strip_prefix retorna Some armazenando a
fatia com o texto correspondente removido. Caso contrário, retorna
None. O método strip_suffix é semelhante, mas verifica uma
correspondência no final da string.
Esses são como trim_start_matches e trim_end_matches, exceto que
retornam uma Option e apenas uma cópia de pattern é removida:
let slice = "banana";
assert_eq!(slice.strip_suffix("na"),
Some("bana"))
assert_eq!(usize::from_str("3628800"), Ok(3628800));
assert_eq!(f64::from_str("128.5625"), Ok(128.5625));
assert_eq!(bool::from_str("true"), Ok(true));
Adiando a alocação
Suponha que você queira que seu programa cumprimente o usuário.
No Unix, você poderia escrever:
fn get_name() -> String {
std::env::var("USER") // O Windows utiliza "USERNAME"
.unwrap_or("whoever you are".to_string())
}
Formatando valores
Ao longo do livro, utilizamos macros de formatação de texto como
println!:
println!("{:.3}µs: relocated {} at {:#x} to {:#x}, {} bytes",
0.84391, "object",
140737488346304_usize, 6299664_usize, 64);
Essa chamada produz a seguinte saída:
0.844µs: relocated object at 0x7fffffffdcc0 to 0x602010, 64 bytes
O literal string serve como um template para a saída: cada {...} no
template é substituído pela forma formatada de um dos seguintes
argumentos. A string do template deve ser uma constante para que
o Rust possa compará-la com os tipos de argumentos em tempo de
compilação. Cada argumento deve ser utilizado; caso contrário, o
Rust relata um erro em tempo de compilação.
Vários recursos de biblioteca padrão compartilham essa pequena
linguagem para formatar strings:
• A macro format! o utiliza para construir Strings.
• As macros println! e print! escrevem texto formatado no fluxo de
saída padrão.
• As macros writeln! e write! o escrevem em um fluxo de saída
designado.
• A macro panic! o utiliza para construir uma expressão (idealmente
informativa) de erro terminal.
Os recursos de formatação do Rust são projetados para serem
abertos. Você pode estender essas macros para suportar os próprios
tipos implementando os traits std::fmt de formatação do módulo. E
você pode utilizar a macro format_args! e o tipo std::fmt::Arguments para
fazer as próprias funções e macros suportarem a linguagem de
formatação.
Macros de formatação sempre emprestam referências
compartilhadas a seus argumentos; nunca tomam posse deles ou os
transformam.
As formas {...} da template são chamadas de parâmetros de formato
e assumem a forma {which:how}. Ambas as partes são opcionais; {} é
frequentemente utilizado.
O valor which seleciona qual argumento após o template deve ocupar
o lugar do parâmetro. Você pode selecionar argumentos por índice
ou por nome. Parâmetros sem valor which são simplesmente pareados
com os argumentos da esquerda para a direita.
O valor how diz como o argumento deve ser formatado: quanto
preenchimento, com qual precisão, em qual raiz numérica e assim
por diante. Se how estiver presente, o dois-pontos antes é
necessário. A Tabela 17.4 apresenta alguns exemplos.
Tabela 17.4: Exemplos de string formatada
String de template Lista de argumentos Resultado
"number of {}: {}" "elephants", 19 "number of elephants: 19"
"from {1} to {0}" "the grave", "the cradle" "from the cradle to the
grave"
"v = {:?}" vec![0,1,2,5,12,29] "v = [0, 1, 2, 5, 12, 29]"
"name = {:?}" "Nemo" "name = \"Nemo\""
"{:8.2} km/s" 11.186 " 11.19 km/s"
"{:20} {:02x} {:02x}" "adc #42", 105, 42 "adc #42 69 2a"
"{1:02x} {2:02x} {0}" "adc #42", 105, 42 "69 2a adc #42"
"{lsb:02x} insn="adc #42", lsb=105, "69 2a adc #42"
{msb:02x} {insn}" msb=42
"{:02?}" [110, 11, 9] "[110, 11, 09]"
"{:02x?}" [110, 11, 9] "[6e, 0b, 09]"
Formatando números
Quando o argumento de formatação tem um tipo numérico como
usize ou f64, o valor how do parâmetro tem as seguintes partes, todas
opcionais:
• Um preenchimento e alinhamento, que funcionam como fazem
com tipos textuais.
• Um caractere +, solicitando que o sinal do número seja sempre
mostrado, mesmo quando o argumento for positivo.
• Um caractere #, solicitando um prefixo radix explícito como 0x ou
0b. Veja o último item desta lista, “notação”.
• Um caractere 0, solicitando que a largura mínima do campo seja
satisfeita incluindo zeros à esquerda no número, em vez da
abordagem de preenchimento usual.
• Uma largura mínima de campo. Se o número formatado não tiver
pelo menos essa largura, o Rust o preencherá à esquerda (por
padrão) com espaços (por padrão) para criar um campo com a
largura especificada.
• Uma precisão para argumentos de ponto flutuante, indicando
quantos dígitos o Rust deve incluir após o ponto decimal. O Rust
arredonda ou estende com zeros conforme necessário para
produzir exatamente essa quantidade de dígitos fracionários. Se a
precisão for omitida, o Rust tenta representar com precisão o valor
utilizando o menor número de dígitos possível. Para argumentos
do tipo inteiro, a precisão é ignorada.
• Uma notação. Para tipos inteiros, isso pode ser b para binário, o
para octal, ou x ou X para hexadecimal com letras maiúsculas ou
minúsculas. Se você incluiu o caractere #, eles incluem um prefixo
radix explícito no estilo Rust, 0b, 0o, 0x ou 0X. Para tipos de ponto
flutuante, uma radix de e ou E exige notação científica, com
coeficiente normalizado, utilizando e ou E para o expoente. Se você
não especificar nenhuma notação, o Rust formata os números em
decimal.
A Tabela 17.6 mostra alguns exemplos de formatação do valor i32
1234.
use std::fs::OpenOptions;
use std::io::Write;
fn write_log_entry(entry: std::fmt::Arguments) {
if logging_enabled() {
// Mantenha as coisas simples por enquanto e apenas
// abra o arquivo a cada vez.
let mut log_file = OpenOptions::new()
.append(true)
.create(true)
.open("log-file-name")
.expect("failed to open log file");
log_file.write_fmt(entry)
.expect("failed to write to log");
}
}
Você pode chamar write_log_entry assim:
write_log_entry(format_args!("Hark! {:?}\n", mysterious_value));
Em tempo de compilação, a macro format_args! analisa a string do
template e a compara com os tipos de argumentos, relatando um
erro se houver algum problema. Em tempo de execução, ele avalia
os argumentos e constrói um valor Arguments carregando todas as
informações necessárias para formatar o texto: uma forma pré-
analisada do template, com referências compartilhadas aos valores
do argumento.
Construir um valor de Arguments é barato: é só juntar alguns
ponteiros. Nenhum trabalho de formatação ocorre ainda, apenas a
coleta das informações necessárias para fazê-lo posteriormente. Isso
pode ser importante: se o log não estiver ativado, qualquer tempo
gasto convertendo números em decimais, preenchendo valores e
assim por diante será desperdiçado.
O tipo File implementa o trait std::io::Write, cujo método write_fmt recebe
um Argument e faz a formatação. Ele grava os resultados no fluxo
subjacente.
Essa chamada a write_log_entry não é elegante. É aqui que uma macro
pode ajudar:
macro_rules! log { // o ! não é necessário após o nome nas definições de macro
($format:tt, $($arg:expr),*) => (
write_log_entry(format_args!($format, $($arg),*))
)
}
Cobrimos macros em detalhes no Capítulo 21. Por enquanto,
acredite que isso define uma nova macro log! que passa seus
argumentos para format_args! e depois chama sua função write_log_entry
no valor Arguments resultante. As macros de formatação como println!,
writeln! e format! têm mais ou menos a mesma ideia.
Você pode utilizar log! assim:
log!("O day and night, but this is wondrous strange! {:?}\n",
mysterious_value);
Idealmente, isso parece um pouco melhor.
Expressões regulares
O crate regex externo é a biblioteca oficial de expressões regulares do
Rust. Ele fornece as funções usuais de pesquisa e correspondência
de padrões. Ele tem um bom suporte para Unicode, mas também
pode pesquisar strings de bytes. Embora não suporte alguns
recursos que você encontrará com frequência em outros pacotes de
expressões regulares, como referências inversas e padrões look-
around, essas simplificações permitem que regex garanta que as
pesquisas tenham tempo linear no tamanho da expressão e no
comprimento do texto que está sendo pesquisado. Essas garantias,
entre outras, tornam regex seguro de utilizar mesmo com expressões
não confiáveis pesquisando texto não confiável.
Neste livro, forneceremos apenas uma visão geral de regex; você
deve consultar a documentação on-line para obter detalhes.
Apesar de o crate regex não estar em std, é mantido pela equipe da
biblioteca do Rust, o mesmo grupo responsável por std. Para utilizar
regex, coloque a seguinte linha na seção [dependencies] do arquivo
Cargo.toml do seu crate:
regex = "1"
Nas seções a seguir, presumiremos que você tenha feito essa
alteração.
lazy_static! {
static ref SEMVER: Regex
= Regex::new(r"(\d+)\.(\d+)\.(\d+)(-[-.[:alnum:]]*)?")
.expect("error parsing regex");
}
A macro se expande para uma declaração de uma variável estática
chamada SEMVER, mas seu tipo não é exatamente Regex. Em vez disso,
é um tipo gerado por macro que implementa Deref<Target=Regex> e,
portanto, expõe todos os mesmos métodos como um Regex. A
primeira vez que SEMVER é desreferenciado, o inicializador é avaliado
e o valor é salvo para uso posterior. Como SEMVER é uma variável
estática, não apenas uma variável local, o inicializador é executado
no máximo uma vez por execução do programa.
Com essa declaração em vigor, o uso de SEMVER é simples e direto:
use std::io::BufRead;
Normalização
A maioria dos usuários consideraria a palavra francesa para chá, thé,
com três caracteres. Contudo, o Unicode na verdade tem duas
maneiras de representar esse texto:
• Na forma composta, “thé” contém os três caracteres 't', 'h' e 'é', em
que 'é' é um único caractere Unicode com ponto de código 0xe9.
• Na forma decomposta, “thé” compreende os quatro caracteres 't',
'h', 'e' e '\u{301}', em que o 'e' é o caractere ASCII simples, sem
acento, e o ponto de código 0x301 é o caractere “COMBINING
ACUTE ACCENT” (acento agudo combinado), que adiciona um
acento agudo a qualquer caractere antes dele.
O Unicode não considera nem a forma composta nem a decomposta
de “é” como a forma “correta”; em vez disso, considera ambas
representações equivalentes do mesmo caráter abstrato. O Unicode
diz que ambas as formas devem ser exibidas da mesma maneira e
os métodos de entrada de texto podem produzir qualquer uma
delas; portanto, os usuários geralmente não saberão qual forma
estão vendo ou digitando. (O Rust permite que você utilize
caracteres Unicode diretamente em literais string, então pode
simplesmente escrever "thé" se não se importar com qual codificação
vai obter. Aqui utilizaremos o escape \u para maior clareza.)
Mas, considerados como valores &str ou String do Rust, "th\u{e9}" e
"the\u{301}" são completamente distintos. Eles têm comprimentos
diferentes, são comparados como desiguais, têm valores hash
diferentes e são ordenados de maneira diferente em relação a outras
strings:
assert!("th\u{e9}" != "the\u{301}");
assert!("th\u{e9}" > "the\u{301}");
Formas de normalização
O Unicode define quatro formas normalizadas, cada uma delas
apropriada para diferentes usos. Há duas perguntas a responder:
• Primeiro, você prefere que os caracteres sejam tão compostos
quanto possível ou tão decompostos quanto possível?
Por exemplo, a representação mais composta da palavra
vietnamita Phở é a string de três caracteres "Ph\u{1edf}", em que
tanto a marca tonal ̉ e a marca da vogal ̛ são aplicados ao
caractere de base “o” em um único caractere Unicode, '\u{1edf}',
que o Unicode diligentemente nomeia como LATIN SMALL LETTER
O WITH HORN AND HOOK ABOVE (letra latina o minúscula com
chifre e gancho acima).
A representação mais decomposta divide a letra base e suas duas
marcas em três caracteres Unicode separados: 'o', '\u{31b}'
(COMBINING HORN – chifre combinado) e '\u{309}' (COMBINING
HOOK ABOVE – gancho acima combinado), resultando em
"Pho\u{31b}\u{309}". (Sempre que as marcas de combinação
aparecem como caracteres separados, em vez de fazer parte de
um caractere composto, todas as formas normalizadas especificam
uma ordem fixa na qual devem aparecer; portanto, a normalização
é bem especificada mesmo quando os caracteres têm vários
acentos.)
A forma composta geralmente tem menos problemas de
compatibilidade, uma vez que corresponde mais às representações
que a maioria dos idiomas utilizava para seu texto antes de o
Unicode se estabelecer. Também pode funcionar melhor com
recursos de formatação de string ingênuos, como a macro format!
do Rust. A forma decomposta, por outro lado, pode ser melhor
para exibição de texto ou pesquisa, pois torna a estrutura
detalhada do texto mais explícita.
• A segunda pergunta é: se duas sequências de caracteres
representam o mesmo texto fundamental, mas diferem na forma
como o texto deve ser formatado, você deseja tratá-los como
equivalentes ou mantê-los distintos?
O Unicode possui caracteres separados para o dígito comum 5, o
dígito sobrescrito ⁵ (ou '\u{2075}') e o dígito circulado ⑤ (ou
'\u{2464}'), mas declara que todos os três são equivalentes de
compatibilidade. Da mesma forma, o Unicode possui um único
caractere para a ligadura ffi ('\u{fb03}'), mas declara que isso é o
equivalente de compatibilidade à sequência de três caracteres ffi.
A equivalência de compatibilidade faz sentido para pesquisas: uma
pesquisa por "difficult", utilizando apenas caracteres ASCII, deve
corresponder à string "di\u{fb03}cult", que utiliza a ligadura ffi. A
aplicação da decomposição de compatibilidade à última string
substituiria a ligadura pelas três letras simples "ffi", facilitando a
busca. Mas a normalização do texto para uma forma equivalente
de compatibilidade pode perder informações essenciais, portanto
não deve ser aplicada de forma descuidada. Por exemplo, seria
incorreto na maioria dos contextos armazenar "25" Como "25".
Unicode Normalization Form C e Normalization Form D (NFC e NFD)
utilizam as formas maximamente compostas e maximamente
decompostas de cada caractere, mas não tentam unificar sequências
equivalentes de compatibilidade. As formas de normalização NFKC e
NFKD são como NFC e NFD, mas normalizam todas as sequências de
equivalentes de compatibilidade para algum representante simples
de sua classe.
O “Modelo de caracteres para a World Wide Web” do World Wide
Web Consortium recomenda o uso de NFC para todo o conteúdo. O
anexo “Identificador Unicode e sintaxe de padrão” sugere o uso de
NFKC para identificadores em linguagens de programação e oferece
princípios para adaptar a forma quando necessário.
Leitores e escritores
Leitores são valores dos quais o programa pode ler bytes. Exemplos
incluem:
• Arquivos abertos utilizando std::fs::File::open(filename)
• std::net::TcpStreams, para receber dados pela rede
• std::io::stdin(), para leitura do fluxo de entrada padrão do processo
• std::io::Cursor<&[u8]> e valores std::io::Cursor<Vec<u8>>, que são leitores
que “leem” de um array de bytes ou vetor que já está na memória
Escritores são valores nos quais seu programa pode escrever (ou
enviar) bytes. Exemplos incluem:
• Arquivos abertos utilizando std::fs::File::create(filename)
• std::net::TcpStreams, para enviar dados pela rede
• std::io::stdout() e std::io:stderr(), para escrever no terminal
• Vec<u8>, um escritor cujos métodos write anexam ao vetor
• std::io::Cursor<Vec<u8>>, que é semelhante, mas permite ler e
escrever dados e acessar diferentes posições dentro do vetor
• std::io::Cursor<&mut [u8]>, que é muito parecido com
std::io::Cursor<Vec<u8>>, exceto que não pode aumentar o tamanho
do buffer, pois é apenas uma fatia de algum array de bytes
existente
Como existem traits padrão para leitores e escritores (std::io::Read e
std::io::Write), é bastante comum escrever código genérico que
funciona em uma variedade de canais de entrada ou saída. Por
exemplo, eis uma função que copia todos os bytes de qualquer leitor
para qualquer escritor:
use std::io::{self, Read, Write, ErrorKind};
const DEFAULT_BUF_SIZE: usize = 8 * 1024;
pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W)
-> io::Result<u64>
where R: Read, W: Write
{
let mut buf = [0; DEFAULT_BUF_SIZE];
let mut written = 0;
loop {
let len = match reader.read(&mut buf) {
Ok(0) => return Ok(written),
Ok(len) => len,
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
};
writer.write_all(&buf[..len])?;
written += len as u64;
}
}
Essa é a implementação de std::io::copy() da biblioteca padrão do Rust.
Por ser genérica, você pode usá-la para copiar dados de um File para
um TcpStream, de Stdin para um Vec<u8> na memória etc.
Se o código de tratamento de erros aqui não estiver claro, revise o
Capítulo 7. Utilizaremos o tipo Result constantemente nas próximas
páginas; é importante ter uma boa noção de como ele funciona.
Os três traits std::io Read, BufRead e Write, com Seek, são tão comumente
utilizados que há um módulo prelude contendo apenas esses traits:
use std::io::prelude::*;
Veremos isso uma ou duas vezes neste capítulo. O hábito é também
importar o próprio módulo std::io:
use std::io::{self, Read, Write, ErrorKind};
A palavra-chave self aqui declara io como um aliás para o módulo
std::io. Dessa maneira, std::io::Result e std::io::Error podem ser escritos de
forma mais concisa como io::Result e io::Error e assim por diante.
Leitores
tem vários métodos para ler dados. Todos eles recebem o
std::io::Read
próprio leitor por referência mut.
reader.read(&mut buffer)
Lê alguns bytes da fonte de dados e os armazena no buffer
determinado. O tipo do argumento buffer é &mut [u8]. Isso lê até
buffer.len() bytes.
O tipo de retorno é io::Result<u64>, que é um alias de tipo para
Result<u64, io::Error>. Se bem-sucedido, o valor u64 é o número de
bytes lidos – que pode ser igual ou menor que buffer.len(), mesmo
que haja mais dados por vir, conforme o capricho da fonte de
dados. Ok(0) significa que não há mais entrada a ler.
Em caso de erro, .read() retorna Err(err), em que err é um valor io::Error.
Um io::Error é imprimível, para benefício dos humanos; para
programas, tem um método .kind() que retorna um código de erro
do tipo io::ErrorKind. Os membros desse enum têm nomes como
PermissionDenied e ConnectionReset. A maioria indica erros graves que não
podem ser ignorados, mas um tipo de erro deve ser tratado de
forma especial. io::ErrorKind::Interrupted corresponde ao código de erro
Unix EINTR, o que significa que a leitura foi interrompida por um
sinal. A menos que o programa seja projetado para fazer algo
inteligente com os sinais, ele deve apenas tentar novamente a
leitura. O código para copy(), na seção anterior, mostra um exemplo
disso.
Como podemos ver, o método .read() é de nível muito baixo, até
mesmo herda peculiaridades do sistema operacional subjacente. Se
você estiver implementando o trait Read para um novo tipo de fonte
de dados, isso lhe dá muita margem de manobra. Se você estiver
tentando ler alguns dados, é um problema. Portanto, o Rust
fornece vários métodos de conveniência de alto nível. Todos eles
têm implementações padrão em termos de .read(). Todos eles tratam
ErrorKind::Interrupted, de modo que você não precise tratar.
reader.read_to_end(&mut byte_vec)
Lê todas as entradas restantes desse leitor, anexando-as a byte_vec,
que é um Vec<u8>. Retorna um io::Result<usize>, o número de bytes
lidos.
Não há limite quanto à quantidade de dados que esse método
armazenará no vetor, portanto não o utilize em uma fonte não
confiável. (Você pode impor um limite utilizando o método .take(),
descrito na lista a seguir.)
reader.read_to_string(&mut string)
Isso é a mesma coisa, mas anexa os dados à String determinada. Se
o fluxo não for UTF-8 válido, isso retornará um erro
ErrorKind::InvalidData.
Em algumas linguagens de programação, a entrada de bytes e a
entrada de caracteres são tratadas por tipos diferentes.
Atualmente, o UTF-8 é tão dominante que o Rust reconhece esse
padrão de fato e suporta UTF-8 em todos os lugares. Outros
conjuntos de caracteres são suportados com o crate de código-
fonte aberto encoding.
reader.read_exact(&mut buf)
Lê exatamente dados suficientes para preencher o buffer fornecido.
O tipo de argumento é &[u8]. Se o leitor ficar sem dados antes de
ler buf.len() bytes, isso retornará um erro ErrorKind::UnexpectedEof.
Esses são os principais métodos do trait Read. Além disso, existem
três métodos adaptadores que utilizam o reader por valor,
transformando-o em um iterador ou outro leitor:
reader.bytes()
Retorna um iterador sobre os bytes do fluxo de entrada. O tipo de
item é io::Result<u8>, portanto uma verificação de erro é necessária
para cada byte. Além disso, isso chama reader.read() uma vez por
byte, o que será muito ineficiente se o leitor não estiver
armazenado em um buffer.
reader.chain(reader2)
Retorna um novo leitor que produz todas as entradas de reader,
seguidas por todas as entradas de reader2.
reader.take(n)
Retorna um novo leitor que lê da mesma fonte que reader, mas
limita-se a n bytes de entrada.
Não há método para fechar um leitor. Leitores e escritores
normalmente implementam Drop para que sejam fechados
automaticamente.
Leitores bufferizados
Para eficiência, os leitores e escritores podem ser armazenados em
buffer, o que significa simplesmente que eles têm um fragmento da
memória (um buffer) que contém alguns dados de entrada ou saída
na memória. Isso economiza chamadas de sistema, como mostrado
na Figura 18.2. O aplicativo lê os dados do BufReader, nesse exemplo
chamando o método .read_line(). O BufReader por sua vez obtém a
entrada em blocos maiores do sistema operacional.
Essa imagem não está na escala. O tamanho padrão real de um
buffer de BufReader é de vários quilobytes, então um único read do
sistema pode atender a centenas de chamadas .read_line(). Isso é
importante porque as chamadas de sistema são lentas.
(Como mostra a figura, o sistema operacional também possui um
buffer, pelo mesmo motivo: as chamadas de sistema são lentas, mas
a leitura de dados de um disco é mais lenta ainda.)
Lendo linhas
Eis uma função que implementa o utilitário grep do Unix. Ela pesquisa
muitas linhas de texto, normalmente redirecionado de outro
comando, para uma determinada string:
use std::io;
use std::io::prelude::*;
let f = File::open(file)?;
grep(&target, BufReader::new(f))?; // também ok
Observe que um File não é automaticamente armazenado em buffer.
Fileimplementa Read, mas não BufRead. Contudo, é fácil criar um leitor
armazenado em buffer para um File, ou qualquer outro leitor não
armazenado em buffer. BufReader::new(reader) faz isso. (Para definir o
tamanho do buffer, utilize BufReader::with_capacity(size, reader).)
Na maioria das linguagens, os arquivos são armazenados em buffer
por padrão. Se você deseja entrada ou saída não armazenada em
buffer, precisa descobrir como desativar o armazenamento em
buffer. No Rust, File e BufReader são dois recursos de biblioteca
distintos, porque às vezes você quer arquivos sem buffer e às vezes
quer armazenar em buffer sem arquivos (por exemplo, pode querer
armazenar em buffer a entrada da rede).
O programa completo, incluindo tratamento de erros e algumas
análises grosseiras de argumentos, é mostrado aqui:
// grep - Pesquisa em stdin ou em alguns arquivos linhas
// que correspondem a uma determinada string
use std::error::Error;
use std::io::{self, BufReader};
use std::io::prelude::*;
use std::fs::File;
use std::path::PathBuf;
fn main() {
let result = grep_main();
if let Err(err) = result {
eprintln!("{}", err);
std::process::exit(1);
}
}
Coletando linhas
Vários métodos de leitores, incluindo .lines(), retornam iteradores que
produzem valores Result. A primeira vez que você quiser coletar todas
as linhas de um arquivo em um grande vetor, terá problemas para se
livrar do Results:
// ok, mas é não o que você quer
let results: Vec<io::Result<String>> = reader.lines().collect();
Escritores
Como vimos, a entrada é principalmente produzida utilizando
métodos. A saída é um pouco diferente.
Ao longo do livro, utilizamos println!() para produzir saída de texto
simples:
println!("Hello, world!");
Arquivos
Já vimos duas maneiras de abrir um arquivo:
File::open(filename)
Abre um arquivo existente para leitura. Retorna um io::Result<File> eé
um erro se o arquivo não existir.
File::create(filename)
Cria um novo arquivo para escrita. Se existir um arquivo com o
nome de arquivo fornecido, ele será truncado (o conteúdo será
apagado).
Observe que o tipo File está no módulo do sistema de arquivos, std::fs,
não std::io.
Quando nenhum desses é apropriado, utilize OpenOptions para
especificar o comportamento exato desejado:
use std::fs::OpenOptions;
let log = OpenOptions::new()
.append(true) // se o arquivo existir, adiciona ao final
.open("server.log")?;
let file = OpenOptions::new()
.write(true)
.create_new(true) // falha se o arquivo já existir
.open("new_file.txt")?;
Os métodos .append(), .write(), .create_new() e assim por diante são
projetados para serem encadeados desta maneira: cada um retorna
self. Esse padrão de design de encadeamento de métodos é comum
o suficiente para ter um nome no Rust: é chamado builder
(construtor). std::process::Command é outro exemplo. Para mais detalhes
sobre OpenOptions, consulte a documentação on-line.
Depois que File foi aberto, ele se comporta como qualquer outro
leitor ou escritor. Você pode adicionar um buffer, se necessário. O File
será fechado automaticamente quando você o dropar.
Posicionando
também implementa o trait Seek, o que significa que você pode
File
mudar a posição dentro de um File em vez de ler ou escrever em
uma única passagem do começo ao fim. Seek é definido assim:
pub trait Seek {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64>;
}
pub enum SeekFrom {
Start(u64),
End(i64),
Current(i64)
}
Graças ao enum, o método seek é bem expressivo: utilize
file.seek(SeekFrom::Start(0)) para voltar ao início e utilize
file.seek(SeekFrom::Current(-8)) para voltar alguns bytes e assim por diante.
O posicionamento (mudança da posição de leitura ou escrita) dentro
de um arquivo é lento. Se estiver utilizando um disco rígido ou um
disco de estado sólido (SSD), uma busca demora tanto quanto a
leitura de vários megabytes de dados.
io::stdout(), io::stderr()
Retorna os tipos de escritor Stdout e Stderr para os fluxos da saída
padrão e de erro padrão. Esses também têm mutexes e métodos
.lock().
Vec<u8>
Implementa Write. Escrever em um Vec<u8> estende o vetor com os
novos dados.
(String, porém, não implementa Write. Para construir uma string
utilizando Write, primeiro escreva em um Vec<u8> e depois utilize
String::from_utf8(vec) para converter o vetor em uma string.)
Cursor::new(buf)
Cria um Cursor, um leitor em buffer que lê de buf. É assim que você
cria um leitor que lê de uma String. O argumento buf pode ser
qualquer tipo que implementa AsRef<[u8]>, assim você também pode
passar um &[u8], &str ou Vec<u8>.
Cursores são triviais internamente. Eles têm apenas dois campos: o
próprio buf e um inteiro, o deslocamento em buf onde a próxima
leitura começará. A posição é inicialmente 0.
Cursores implementam Read, BufRead e Seek. Se o tipo de buf é &mut
[u8] ou Vec<u8>, então o Cursor também implementa Write. Escrever
em um cursor sobrescreve os bytes em buf começando na posição
atual. Se você tentar escrever depois do final de um &mut [u8],
obterá uma escrita parcial ou um io::Error. Utilizar um cursor para
escrever após o final de um Vec<u8> é correto, porém aumenta o
vetor. Cursor<&mut [u8]> e Cursor<Vec<u8>>, portanto implementam
todos os quatro traits std::io::prelude.
std::net::TcpStream
Representa uma conexão de rede TCP. Como o TCP permite a
comunicação bidirecional, ele é um leitor e um escritor.
A função associada ao tipo TcpStream::connct(("hostname", PORT)) tenta se
conectar a um servidor e retorna um io::Result<TcpStream>.
std::process::Command
Suporta lançar um processo filho e redirecionar dados para a sua
entrada padrão, assim:
use std::process::{Command, Stdio};
Arquivos e diretórios
Agora que mostramos como trabalhar com leitores e escritores, as
próximas seções discutem os recursos do Rust para trabalhar com
arquivos e diretórios, que residem nos módulos std::path e std::fs.
Todos esses recursos envolvem trabalhar com nomes de arquivo,
então começaremos com os tipos de nome de arquivo.
OsStr e Path
Inconvenientemente, seu sistema operacional não força os nomes de
arquivo a serem Unicode válidos. Eis dois comandos shell do Linux
que criam arquivos de texto. Somente o primeiro utiliza um nome de
arquivo UTF-8 válido:
$ echo "hello world" > ô.txt
$ echo "O brave new world, that has such filenames in't" > $'\xf4'.txt
Ambos os comandos são passados sem comentários, porque o
kernel do Linux não conhece UTF-8 de Ogg Vorbis. Para o kernel,
qualquer string de bytes (excluindo bytes nulos e barras) é um nome
de arquivo aceitável. É uma história semelhante no Windows: quase
qualquer string de “caracteres de largura” de 16 bits é um nome de
arquivo aceitável, mesmo strings que não são UTF-16 válidas. O
mesmo se aplica a outras strings manipuladas pelo sistema
operacional, como argumentos de linha de comando e variáveis de
ambiente.
Strings do Rust são sempre Unicode válidas. Os nomes de arquivo
são quase sempre Unicode na prática, mas o Rust de alguma forma
tem de lidar com o raro caso em que não são. É por isso que o Rust
tem std::ffi::OsStr e OsString.
OsStr é um tipo de string que é um superconjunto de UTF-8. Seu
trabalho é ser capaz de representar todos os nomes de arquivo,
argumentos de linha de comando e variáveis de ambiente no
sistema atual, sejam eles Unicode válidos ou não. No Unix, um OsStr
pode conter qualquer sequência de bytes. No Windows, um OsStr é
armazenado utilizando uma extensão de UTF-8 que pode codificar
qualquer sequência de valores de 16 bits, incluindo substitutos não
correspondidos.
Portanto, temos dois tipos de string: str para strings Unicode reais; e
OsStr para qualquer bobagem que seu sistema operacional possa
oferecer. Vamos apresentar mais um: std::path::Path, para nomes de
arquivo. Esse é puramente uma conveniência. Path é exatamente
como OsStr, mas adiciona muitos métodos úteis relacionados ao
nome de arquivo, que abordaremos na próxima seção. Utilize Path
para caminhos absolutos e relativos. Para um componente individual
de um caminho, utilize OsStr.
Por fim, para cada tipo de string, há um correspondente tipo
proprietário: um tipo String possui uma str alocada em heap, um
std::ffi::OsString possui uma OsStr alocada em heap e um std::path::PathBuf
possui uma Path alocada em heap. A Tabela 18.1 descreve algumas
das características de cada tipo.
Tabela 18.1: Tipos de nome de arquivo
str OsStr Path
Tipo não dimensionado, sempre passado por Sim Sim Sim
referência
Pode conter qualquer texto Unicode Sim Sim Sim
Parece com UTF-8, normalmente Sim Sim Sim
Pode conter dados não Unicode Não Sim Sim
Métodos de processamento de texto Sim Não Não
Métodos relacionados a nome de arquivo Não Não Sim
Equivalente alocado no heap com posse e String OsString PathBuf
expansível
Converte em tipo proprietário .to_string( .to_os_string( .to_path_buf(
) ) )
path.is_absolute(), path.is_relative()
Informam se o arquivo é absoluto, como o caminho do Unix
/usr/bin/advent ou o caminho do Windows C:\Arquivos de
programas, ou relativo, como src/main.rs.
path1.join(path2)
Associa dois caminhos, retornando um novo PathBuf:
let path1 = Path::new("/usr/share/dict");
assert_eq!(path1.join("words"),
Path::new("/usr/share/dict/words"));
Se path2 é um caminho absoluto, isso apenas retorna uma cópia de
path2,
assim esse método pode ser utilizado para converter qualquer
caminho em um caminho absoluto:
let abs_path = std::env::current_dir()?.join(any_path);
path.components()
Retorna um iterador sobre os componentes do caminho
especificado, da esquerda para a direita. O tipo de item desse
iterador é std::path::Component, uma enumeração que pode representar
todas as diferentes partes que podem aparecer em nomes de
arquivo:
pub enum Component<'a> {
Prefix(PrefixComponent<'a>), // uma letra de unidade ou compartilhamento (no
Windows)
RootDir, // o diretório raiz, `/` ou `\`
CurDir, // o diretório especial `.`
ParentDir, // o diretório especial `..`
Normal(&'a OsStr) // nomes simples de arquivo e diretório
}
Por exemplo, o caminho do Windows \\venice\Music\A Love
Supreme\04-Psalm.mp3 consiste em um Prefix representando
\\venice\Music, seguido por um RootDir e então dois componentes
Normal que representam A LoveSupreme e 04-Psalm.mp3.
Para obter detalhes, consulte a documentação on-line
(https://oreil.ly/mtHCk).
path.ancestors()
Retorna um iterador que vai de path até a raiz. Cada item gerado é
um Path: primeiro o próprio path, então o pai, depois o avô e assim
por diante:
let file = Path::new("/home/jimb/calendars/calendar-18x18.pdf");
assert_eq!(file.ancestors().collect::<Vec<_>>(),
vec![Path::new("/home/jimb/calendars/calendar-18x18.pdf"),
Path::new("/home/jimb/calendars"),
Path::new("/home/jimb"),
Path::new("/home"),
Path::new("/")]);
Isso é quase como chamar parent repetidamente até retornar None. O
item final é sempre um caminho raiz ou prefixo.
Esses métodos funcionam em strings na memória. Paths também têm
alguns métodos que consultam o sistema de arquivos: .exists(),
.is_file(), .is_dir(), .read_dir(), .canonicalize() etc. Consulte informações
adicionais na documentação on-line.
Existem três métodos para converter Paths em strings. Cada um
permite a possibilidade de UTF-8 inválido em Path:
path.to_str()
Converte um Path em uma string, como uma Option<&str>. Se path não
é UTF-8 válido, isso retorna None:
if let Some(file_str) = path.to_str() {
println!("{}", file_str);
} // ...caso contrário, ignore esse arquivo de nome estranho
path.to_string_lossy()
Isso é basicamente a mesma coisa, mas consegue retornar algum
tipo de string em todos os casos. Se path não é UTF-8 válido, esses
métodos criam uma cópia, substituindo cada sequência de bytes
Lendo diretórios
Para listar o conteúdo de um diretório, utilize std::fs::read_dir ou, de
forma equivalente, o método .read_dir() de um Path:
for entry_result in path.read_dir()? {
let entry = entry_result?;
println!("{}", entry.file_name().to_string_lossy());
}
Observe os dois usos de ? nesse código. A primeira linha verifica se
há erros ao abrir o diretório. A segunda linha verifica erros ao ler a
próxima entrada.
O tipo de entry é std::fs::DirEntry e é um struct com apenas alguns
métodos:
entry.file_name()
O nome de arquivo ou diretório, como um OsString.
entry.path()
Isso é a mesma coisa, mas com o caminho original associado a ele,
produzindo um novo PathBuf. Se o diretório que estamos listando
fosse "/home/jimb" e entry.file_name() é ".emacs", então entry.path() retornaria
PathBuf::from("/home/jimb/.emacs”).
entry.file_type()
Retorna um io::Result<FileType>. FileType tem métodos .is_file(), .is_dir() e
.is_symlink().
entry.metadata()
Obtém o restante dos metadados sobre essa entrada.
Os diretórios especiais . e .. não são listados ao ler um diretório.
Eis um exemplo mais substancial. O código a seguir copia
recursivamente uma árvore de diretórios de um local para outro no
disco:
use std::fs;
use std::io;
use std::path::Path;
Ok(())
}
Uma função distinta, copy_to, copia entradas de diretório individuais:
/// Copia o que estiver em `src` para o caminho de destino `dst`
fn copy_to(src: &Path, src_type: &fs::FileType, dst: &Path)
-> io::Result<()>
{
if src_type.is_file() {
fs::copy(src, dst)?;
} else if src_type.is_dir() {
copy_dir_to(src, dst)?;
} else {
return Err(io::Error::new(io::ErrorKind::Other,
format!("don't know how to copy: {}",
src.display())));
}
Ok(())
}
Redes
Um tutorial sobre redes está muito além do escopo deste livro. No
entanto, se você já conhece um pouco programação de rede, esta
seção o ajudará a começar a trabalhar com redes no Rust.
Para código de rede de baixo nível, comece com o módulo std::net,
que fornece suporte a múltiplas plataformas para rede TCP e UDP.
Use o crate native_tls para suporte SSL/TLS.
Esses módulos fornecem os blocos de construção simples de entrada
e saída bloqueantes na rede. Você pode escrever um servidor
simples em algumas linhas de código, utilizando std::net e gerando
um thread para cada conexão. Por exemplo, eis um servidor de
“eco”:
use std::net::TcpListener;
use std::io;
use std::thread::spawn;
/// Aceita conexões para sempre, gerando um thread para cada uma
fn echo_main(addr: &str) -> io::Result<()> {
let listener = TcpListener::bind(addr)?;
println!("listening on {}", addr);
loop {
// Espera que um cliente se conecte
let (mut stream, addr) = listener.accept()?;
println!("connection received from {}", addr);
fn main() {
echo_main("127.0.0.1:17007").expect("error: ");
}
Um servidor de eco simplesmente repete tudo o que você envia para
ele. Esse tipo de código não é tão diferente daquilo que você
escreveria no Java ou Python. (Discutiremos std::thread::spawn() no
próximo capítulo.)
Contudo, para servidores de alto desempenho, você precisará utilizar
entrada e saída assíncronas. O Capítulo 20 aborda o suporte do Rust
para programação assíncrona e mostra o código completo para um
cliente e servidor de rede.
Protocolos de nível superior são suportados por crates de terceiros.
Por exemplo, o crate reqwest oferece uma bela API para clientes HTTP.
Eis um programa de linha de comando completo que busca qualquer
documento com um http: ou https: URL e coloca-o no seu terminal.
Esse código foi escrito utilizando reqwest = "0.11", com o recurso
"blocking" ativado. reqwest também fornece uma interface assíncrona.
use std::error::Error;
use std::io;
Ok(())
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 {
eprintln!("usage: http-get URL");
return;
}
Paralelismo fork-join
Os casos de uso mais simples para threads surgem quando temos
várias tarefas completamente independentes que queremos fazer ao
mesmo tempo.
Por exemplo, suponha que estamos fazendo processamento de
linguagem natural em um grande corpus de documentos.
Poderíamos escrever um loop:
fn process_files(filenames: Vec<String>) -> io::Result<()> {
for document in filenames {
let text = load(&document)?; // lê o arquivo fonte
let results = process(text); // calcula estatísticas
save(&document, results)?; // grava o arquivo de saída
}
Ok(())
}
O programa seria executado como mostrado na Figura 19.1.
Figura 19.1: Execução de thread único de process_files().
Como cada documento é processado de maneira separada, é
relativamente fácil acelerar essa tarefa dividindo o corpus em partes
e processando cada parte em um thread distinto, conforme
mostrado na Figura 19.2.
Esse padrão é chamado de paralelismo fork-join (bifurcar-unir).
Bifurcar (fork) é iniciar um novo thread e juntar (join) um thread é
esperar ele terminar. Já vimos essa técnica: ela foi utilizada para
acelerar o programa Mandelbrot no Capítulo 2.
O paralelismo fork-join é atraente por alguns motivos:
• É muito simples. Fork-join é fácil de implementar e o Rust torna
mais fácil acertar.
• Evita gargalos. Não há bloqueio dos recursos compartilhados no
modelo fork-join. A única vez que qualquer thread precisa esperar
por outro é no final. Enquanto isso, cada thread pode executar
livremente. Isso ajuda a manter baixa a sobrecarga de alternância
de tarefas.
• A matemática do desempenho é simples. Na melhor das
hipóteses, iniciando quatro threads, podemos concluir nosso
trabalho em um quarto do tempo. A Figura 19.2 mostra um
motivo pelo qual não devemos esperar esse aumento de
velocidade ideal: talvez não sejamos capazes de distribuir o
trabalho uniformemente por todos os threads. Outro motivo de
cautela é que, às vezes, os programas fork-join devem passar
algum tempo após a junção dos threads combinando os
resultados computados pelos threads. Ou seja, isolar
completamente as tarefas pode gerar algum trabalho extra. Ainda
assim, além dessas duas coisas, qualquer programa vinculado à
CPU com unidades de trabalho isoladas pode esperar uma
melhoria significativa.
Figura 19.2: Processamento de arquivo multithread utilizando uma
abordagem fork-join.
• É fácil raciocinar sobre a exatidão do programa. Um programa
fork-join é determinista contanto que os threads estejam
realmente isolados, como os threads de computação no programa
Mandelbrot. O programa produz sempre o mesmo resultado,
independentemente das variações na velocidade do thread. É um
modelo de concorrência sem condições de corrida.
A principal desvantagem do fork-join é que ele requer unidades de
trabalho isoladas. Mais adiante neste capítulo, consideraremos
alguns problemas que não podem ser divididos tão claramente.
Por enquanto, vamos nos ater ao exemplo do processamento de
linguagem natural. Mostraremos algumas maneiras de aplicar o
padrão fork-join à função process_files.
spawn e join
A função std::thread::spawn inicia um novo thread:
use std::thread;
thread::spawn(|| {
println!("hello from a child thread");
});
Ela recebe um argumento, uma closure ou função FnOnce. O Rust
inicia um novo thread para executar o código dessa closure ou
função. O novo thread é um thread real do sistema operacional com
um stack próprio, assim como threads em C++, C# e Java.
Eis um exemplo mais substancial, utilizando spawn para implementar
uma versão paralela da função process_files anterior:
use std::{thread, io};
fn process_files_in_parallel(filenames: Vec<String>) -> io::Result<()> {
// Divide o trabalho em vários pedaços
const NTHREADS: usize = 8;
let worklists = split_vec_into_chunks(filenames, NTHREADS);
// Fork: Gera um thread para lidar com cada pedaço
let mut thread_handles = vec![];
for worklist in worklists {
thread_handles.push(
thread::spawn(move || process_files(worklist))
);
}
// Join: Aguarda até que todos os threads terminem
for handle in thread_handles {
handle.join().unwrap()?;
}
Ok(())
}
Vamos analisar essa função linha por linha.
fn process_files_in_parallel(filenames: Vec<String>) -> io::Result<()> {
Nossa nova função tem a mesma assinatura de tipo que a original
process_files, tornando-a um substituto drop-in útil.
// Divide o trabalho em vários pedaços
const NTHREADS: usize = 8;
let worklists = split_vec_into_chunks(filenames, NTHREADS);
Utilizamos uma função utilitária split_vec_into_chunks, não mostrada
aqui, para dividir o trabalho. O resultado, worklists, é um vetor de
vetores. Ele contém oito porções de tamanho uniforme do vetor
original filenames.
// Fork: Gera um thread para lidar com cada pedaço (chunk)
let mut thread_handles = vec![];
for worklist in worklists {
thread_handles.push(
thread::spawn(move || process_files(worklist))
);
}
Geramos um thread para cada worklist. spawn() retorna um valor
chamado JoinHandle, que utilizaremos mais tarde. Por enquanto,
colocamos todos os JoinHandles em um vetor.
Observe como obtemos a lista dos nomes de arquivo no thread de
trabalho:
• worklist é definido e preenchido pelo loop for, no thread pai.
• Assim que a closure move é criada, worklist é movido para a closure.
• spawn em seguida move a closure (incluindo o vetor worklist) para o
novo thread filho.
Esses movimentos são baratos. Como os movimentos de Vec<String>
que discutimos no Capítulo 4, Strings não são clonadas. Na verdade,
nada é alocado ou liberado. Os únicos dados movidos são o próprio
Vec: três palavras de máquina.
Quase todos os threads que você cria precisam de código e dados
para iniciar. Closures do Rust, convenientemente, podem conter
qualquer código que você desejar e quaisquer dados que você
quiser.
Prosseguindo:
// Join: Aguarda até que todos os threads terminem
for handle in thread_handles {
handle.join().unwrap()?;
}
Utilizamos o método .join() de JoinHandles que coletamos anteriormente
para aguardar a conclusão de todos os oito threads. A junção (join)
de threads geralmente é necessária para correta execução do
programa, porque um programa Rust é encerrado assim que main
retorna, mesmo se outros threads ainda estão em execução.
Destrutores não são chamados; os threads extras são simplesmente
eliminados. Se isso não é o que você deseja, certifique-se de juntar
todos os threads de seu interesse antes de retornar de main.
Se conseguirmos passar por esse loop, isso significa que todos os
oito threads filhos foram encerrados com sucesso. Nossa função,
portanto, termina retornando Ok(()):
Ok(())
}
// depois
fn process_files(filenames: Vec<String>, glossary: &GigabyteMap)
Esse glossary será grande, então estamos passando-o por referência.
Como podemos atualizar process_files_in_parallel para passar o glossário
para os threads de trabalho?
A mudança óbvia não funciona:
fn process_files_in_parallel(filenames: Vec<String>,
glossary: &GigabyteMap)
-> io::Result<()>
{
...
for worklist in worklists {
thread_handles.push(
spawn(move || process_files(worklist, glossary)) // erro
);
}
...
}
Simplesmente adicionamos um argumento glossary a nossa função e o
passamos para process_files. O Rust reclama:
error: explicit lifetime required in the type of `glossary`
|
38 | spawn(move || process_files(worklist, glossary)) // erro
| ^^^^^ lifetime `'static` required
O Rust está reclamando sobre o tempo de vida da closure que
estamos passando para spawn e a mensagem “útil” que o compilador
apresenta aqui não ajuda em nada.
spawn lança threads independentes. O Rust não tem como saber por
quanto tempo o thread filho será executado, então ele supõe o pior:
ele supõe que o thread filho pode continuar executando mesmo
depois que o thread pai terminou e todos os valores no thread pai
desapareceram. Obviamente, se o thread filho vai durar tanto
tempo, a closure que está executando também precisa durar tanto
tempo. Mas essa closure tem um tempo de vida limitado: depende
da referência glossary e as referências não duram para sempre.
Observe que o Rust está certo em rejeitar esse código! Da forma
como escrevemos essa função, é possível que um thread encontre
um erro de E/S, fazendo process_files_in_parallel sair antes que os outros
threads sejam encerrados. Threads filhos podem acabar tentando
utilizar o glossário depois de o thread principal o liberar. Seria uma
corrida – com comportamento indefinido quanto ao prêmio, caso o
thread principal vencesse. O Rust não pode permitir isso.
Parece que spawn é muito aberto para suportar o compartilhamento
de referências entre threads. Aliás, já vimos um caso como esse, em
“Closures que roubam”, na página 382. Lá, nossa solução foi
transferir a posse dos dados para o novo thread, utilizando uma
closure move. Isso não funcionará aqui, pois há muitos threads que
precisam utilizar os mesmos dados. Uma alternativa segura é usar
clone em todo o glossário para cada thread, mas, como é grande,
queremos evitar isso. Felizmente, a biblioteca padrão oferece outra
maneira: contagem de referência atômica.
Descrevemos Arc em “Rc e Arc: Posse compartilhada”, na página 125.
É hora de colocá-lo em uso:
use std::sync::Arc;
fn process_files_in_parallel(filenames: Vec<String>,
glossary: Arc<GigabyteMap>)
-> io::Result<()>
{
...
for worklist in worklists {
// Essa chamada para .clone() apenas clona o Arc e aumenta a
// contagem de referências. Ela não clona o GigabyteMap
let glossary_for_child = glossary.clone();
thread_handles.push(
spawn(move || process_files(worklist, &glossary_for_child))
);
}
...
}
Alteramos o tipo de glossary: para executar a análise em paralelo, o
chamador deve passar um Arc<GigabyteMap>, um ponteiro inteligente
para um GigabyteMap que foi movido para o heap, utilizando
Arc::new(giga_map).
Quando chamamos glossary.clone(), estamos criando uma cópia do
ponteiro inteligente Arc, não todo o GigabyteMap. Isso equivale a
incrementar uma contagem de referências.
Com essa alteração, o programa compila e roda, pois não depende
mais dos tempos de vida das referências. Desde que qualquer
thread possua um Arc<GigabyteMap>, ele manterá o mapa ativo, mesmo
que o thread pai saia mais cedo. Não haverá corridas de dados (data
races), porque os dados em um Arc são imutáveis.
Rayon
A função spawn da biblioteca padrão é uma primitiva importante, mas
não foi projetada especificamente para paralelismo fork-join. APIs de
fork-join melhores foram construídas sobre ela. Por exemplo, no
Capítulo 2 utilizamos a biblioteca Crossbeam para dividir algum
trabalho em oito threads. Os threads com escopo do Crossbeam
suportam o paralelismo fork-join naturalmente.
A biblioteca Rayon, de Niko Matsakis e Josh Stone, é outro exemplo.
Ela fornece duas maneiras de executar tarefas simultaneamente:
use rayon::prelude::*;
bands.into_par_iter()
.for_each(|(i, band)| {
let top = i;
let band_bounds = (bounds.0, 1);
let band_upper_left = pixel_to_point(bounds, (0, top),
upper_left, lower_right);
let band_lower_right = pixel_to_point(bounds, (bounds.0, top + 1),
upper_left, lower_right);
render(band, band_bounds, band_upper_left, band_lower_right);
});
}
Canais
Um canal é um conduto unidirecional para enviar valores de um
thread para outro. Em outras palavras, é uma fila thread-safe.
A Figura 19.5 ilustra como os canais são utilizados. Eles são algo
como pipes do Unix: uma extremidade é para enviar dados e a outra
para receber. As duas extremidades normalmente são possuídas por
dois segmentos diferentes. Mas, embora os pipes no Unix sejam
utilizados para enviar bytes, os canais são usados para enviar
valores no Rust. sender.send(item) insere um único valor no canal;
receiver.recv() remove um. A posse é transferida do thread de envio
para o thread de recebimento. Se o canal estiver vazio, receiver.recv()
bloqueia até um valor ser enviado.
Enviando valores
Nas próximas seções, utilizaremos canais para construir um
programa concorrente que cria um índice invertido, um dos
principais ingredientes de um mecanismo de pesquisa. Cada
mecanismo de pesquisa funciona em uma determinada coleção de
documentos. O índice invertido é o banco de dados que informa
quais palavras aparecem onde.
Mostraremos as partes do código relacionadas a threads e canais. O
programa completo (https://oreil.ly/yF3me) é curto, cerca de mil
linhas de código ao todo.
Nosso programa é estruturado como um pipeline, conforme
mostrado na Figura 19.7. Pipelines são apenas uma das muitas
maneiras de utilizar canais – discutiremos alguns outros usos mais
adiante –, mas são uma maneira simples e direta de introduzir
concorrência em um programa de thread única existente.
Utilizaremos um total de cinco threads, cada um realizando uma
tarefa distinta. Cada thread produz continuamente saída durante o
tempo de vida do programa. O primeiro thread, por exemplo,
simplesmente lê os documentos de origem do disco para a memória,
um por um. (Queremos um thread para fazer isso porque
escreveremos o código mais simples possível aqui, utilizando
fs::read_to_string, que é uma API que bloqueia. Não queremos que a
CPU fique ociosa sempre que o disco estiver funcionando.) A saída
dessa etapa é uma longa String por documento, assim esse thread é
conectado ao próximo thread por um canal de Strings.
Figura 19.7: O pipeline do construtor de índice, no qual as setas
representam valores enviados por meio de um canal entre um
thread e outro (a E/S de disco não é mostrada).
Inicialmente, nosso programa vai gerar o thread que lê os arquivos.
Suponha que documents é um Vec<PathBuf>, um vetor dos nomes de
arquivo. O código para iniciar nosso thread de leitura de arquivo se
parece com isto:
use std::{fs, thread};
use std::sync::mpsc;
if sender.send(text).is_err() {
break;
}
}
Ok(())
});
Os canais são parte do módulo std::sync::mpsc. Explicaremos o que
esse nome significa mais tarde; primeiro, vamos ver como esse
código funciona. Começamos criando um canal:
let (sender, receiver) = mpsc::channel();
A função channel retorna um par de valores: um remetente e um
destinatário. A estrutura de dados da fila subjacente é um detalhe
de implementação que a biblioteca padrão não expõe.
Os canais são tipados. Vamos utilizar esse canal para enviar o texto
de cada arquivo, assim temos um sender do tipo Sender<String> e um
receiver do tipo Receiver<String>. Poderíamos ter solicitado explicitamente
um canal de strings, escrevendo mpsc::channel::<String>(). Em vez disso,
deixamos a inferência de tipo do Rust resolver isso.
let handle = thread::spawn(move || {
Como antes, utilizamos std::thread::spawn para iniciar um thread. A
posse de sender (mas não de receiver) é transferida para o novo thread
por meio dessa closure move.
As próximas linhas do código simplesmente leem arquivos do disco:
for filename in documents {
let text = fs::read_to_string(filename)?;
Depois de ler um arquivo com sucesso, enviamos o texto para o
canal:
if sender.send(text).is_err() {
break;
}
}
sender.send(text)move o valor text para o canal. Com o tempo, ele será
transferido novamente para quem receber o valor. Se text contém
10 linhas de texto ou 10 megabytes, essa operação copia três
palavras de máquina (do tamanho de um struct String) e a chamada
receiver.recv() correspondente também copiará três palavras de
máquina.
Os métodos send e recv retornam Results, mas esses métodos falham
apenas se a outra extremidade do canal for dropada. Uma chamada
a send falha se o Receiver foi dropado porque, do contrário, o valor
permaneceria no canal para sempre: sem um Receiver, não há como
um thread recebê-lo. Da mesma forma, uma chamada a recv falha se
não houver valores esperando no canal e o Sender foi descartado
porque, do contrário, recv esperaria para sempre: sem um Sender, não
há como um thread enviar o próximo valor. Dropar a extremidade de
um canal é a maneira normal de “desligar”, fechando a conexão
quando você terminar.
Em nosso código, sender.send(text) só vai falhar se o thread do receptor
foi encerrado antecipadamente. Isso é típico do código que utiliza
canais. Se isso aconteceu deliberadamente ou devido a um erro, não
há problema se nosso thread de leitura se desligar silenciosamente.
Quando isso acontece, ou o thread termina de ler todos os
documentos, Ok(()) é retornado:
Ok(())
});
Observe que essa closure retorna um Result. Se o thread encontrar
um erro de E/S, ele será encerrado imediatamente e o erro será
armazenado na memória do thread JoinHandle.
Naturalmente, assim como qualquer outra linguagem de
programação, o Rust admite muitas outras possibilidades quando
chega o momento do tratamento de erros. Quando ocorre um erro,
podemos apenas imprimi-lo utilizando println! e passar para o próximo
arquivo. Podemos passar erros pelo mesmo canal que estamos
utilizando para dados, tornando-o um canal de Results – ou criar um
segundo canal apenas para erros. A abordagem que escolhemos
aqui é leve e responsável: podemos utilizar o operador ?, então não
há um monte de código, ou mesmo um try/catch explícito como
podemos ver no Java, e mesmo assim os erros não passarão
silenciosamente.
Por conveniência, nosso programa empacota todo esse código em
uma função que retorna tanto o receiver (que ainda não utilizamos)
como o JoinHandle do novo thread:
fn start_file_reader_thread(documents: Vec<PathBuf>)
-> (mpsc::Receiver<String>, thread::JoinHandle<io::Result<()>>)
{
let (sender, receiver) = mpsc::channel();
let handle = thread::spawn(move || {
...
});
(receiver, handle)
}
Observe que essa função inicia o novo thread e retorna
imediatamente. Escreveremos uma função como essa para cada
etapa do nosso pipeline.
Recebendo valores
Agora temos um thread executando um loop que envia valores.
Podemos gerar um segundo thread executando um loop que chama
receiver.recv():
while let Ok(text) = receiver.recv() {
do_something_with(text);
}
Mas Receivers são iteráveis, então há uma maneira melhor de escrever
isso:
for text in receiver {
do_something_with(text);
}
Esses dois loops são equivalentes. De qualquer maneira como
escrevemos isso, se o canal estiver vazio quando o controle atingir o
topo do loop, o thread receptor será bloqueado até que algum outro
thread envie um valor. O loop será encerrado normalmente quando o
canal estiver vazio e o Sender tiver sido dropado. Em nosso programa,
isso acontece naturalmente quando o thread leitor é encerrado. Esse
thread está executando uma closure que possui a variável sender;
quando a closure é encerrada, sender é dropada.
Agora podemos escrever o código para a segunda etapa do pipeline:
fn start_file_indexing_thread(texts: mpsc::Receiver<String>)
-> (mpsc::Receiver<InMemoryIndex>, thread::JoinHandle<()>)
{
let (sender, receiver) = mpsc::channel();
(receiver, handle)
}
Essa função gera um thread que recebe valores String de um canal
(texts) e envia valores InMemoryIndex para outro canal (sender/receiver). A
tarefa desse thread é pegar cada um dos arquivos carregados na
primeira etapa e transformar cada documento em um pequeno
índice invertido na memória de um único arquivo.
O loop principal desse thread é simples e direto. Todo o trabalho de
indexação de um documento é feito pela função
InMemoryIndex::from_single_document. Não mostraremos o código-fonte
aqui, mas ele divide a string de entrada nos limites do texto e, em
seguida, produz um mapa das palavras a listas de posições.
Essa etapa não executa E/S, portanto não precisa lidar com io::Errors.
Em vez de um io::Result<()>, retorna ().
Executando o pipeline
As três etapas restantes são semelhantes em termos de design.
Cada uma consome um Receiver criado pela etapa anterior. Nosso
objetivo para o restante do pipeline é mesclar todos os índices
pequenos em um único arquivo de índice grande no disco. A maneira
mais rápida que encontramos para fazer isso é em três etapas. Não
mostraremos o código aqui, apenas as assinaturas de tipo dessas
três funções. O código-fonte completo está on-line.
Primeiro, mesclamos os índices na memória até que fiquem pesados
(etapa 3):
fn start_in_memory_merge_thread(file_indexes: mpsc::Receiver<InMemoryIndex>)
-> (mpsc::Receiver<InMemoryIndex>, thread::JoinHandle<()>)
Gravamos esses índices grandes no disco (etapa 4):
fn start_index_writer_thread(big_indexes: mpsc::Receiver<InMemoryIndex>,
output_dir: &Path)
-> (mpsc::Receiver<PathBuf>, thread::JoinHandle<io::Result<()>>)
Por fim, se houver vários arquivos grandes, vamos mesclá-los
utilizando um algoritmo de mesclagem baseado em arquivo
(etapa 5):
fn merge_index_files(files: mpsc::Receiver<PathBuf>, output_dir: &Path)
-> io::Result<()>
Essa última etapa não retorna um Receiver, porque é o fim da linha.
Ela produz um único arquivo de saída no disco. Ela não retorna um
JoinHandle, porque não nos preocupamos em gerar um thread nessa
etapa. O trabalho é feito no thread do chamador.
Agora chegamos ao código que inicia os threads e verifica se há
erros:
fn run_pipeline(documents: Vec<PathBuf>, output_dir: PathBuf)
-> io::Result<()>
{
// Carrega todas as cinco etapas do pipeline
let (texts, h1) = start_file_reader_thread(documents);
let (pints, h2) = start_file_indexing_thread(texts);
let (gallons, h3) = start_in_memory_merge_thread(pints);
let (files, h4) = start_index_writer_thread(gallons, &output_dir);
let result = merge_index_files(files, &output_dir);
// Espera até que os threads sejam encerrados, parando em qualquer erro encontrado
let r1 = h1.join().unwrap();
h2.join().unwrap();
h3.join().unwrap();
let r4 = h4.join().unwrap();
Além de pipelines
Nesta seção, utilizamos pipelines como exemplos porque eles são
uma maneira boa e óbvia de utilizar canais. Todo mundo os
compreende. São concretos, práticos e determinísticos. Mas canais
são úteis para mais do que apenas pipelines. Eles também são uma
maneira rápida e fácil de oferecer qualquer serviço assíncrono a
outros threads no mesmo processo.
Por exemplo, suponha que você queira fazer log em seu próprio
thread, como na Figura 19.8. Outros threads podem enviar
mensagens de log para o thread de logging em um canal; como
você pode clonar o Sender do canal, muitos threads clientes podem
ter remetentes que enviam mensagens de log para o mesmo thread
de logging.
A execução de um serviço como o log em seu próprio thread tem
vantagens. O thread de registro em log pode alternar os arquivos de
log sempre que necessário. Ele não precisa fazer nenhuma
coordenação sofisticada com os outros threads. Esses threads não
serão bloqueados. As mensagens se acumularão inofensivamente no
canal por um momento até que o thread de registro em log volte a
funcionar.
Canais também podem ser utilizados para casos em que um thread
envia uma solicitação para outro thread e precisa obter algum tipo
de resposta de volta. A solicitação do primeiro thread pode ser um
struct ou uma tupla que inclui um Sender, uma espécie de envelope
autoendereçado que o segundo thread utiliza para enviar a resposta.
Isso não significa que a interação deve ser síncrona. O primeiro
thread decide se deseja bloquear e aguardar a resposta ou utilizar o
método .try_recv() para pesquisar nele.
As ferramentas que apresentamos até agora – fork-join para
computação altamente paralela, canais para componentes de
conexão flexível – são suficientes para uma ampla variedade de
aplicações. Mas ainda não terminamos.
O que é um mutex?
Um mutex (ou lock) é utilizado para forçar vários threads a se
revezarem ao acessar determinados dados. Apresentaremos os
mutexes do Rust na próxima seção. Primeiro, faz sentido lembrar
como que mutexes se parecem em outras linguagens. Um uso
simples de um mutex em C++ pode se parecer com isto:
// C++ code, not Rust
void FernEmpireApp::JoinWaitingList(PlayerId player) {
mutex.Acquire();
waitingList.push_back(player);
mutex.Release();
}
As chamadas mutex.Acquire() e mutex.Release() marcam o início e o fim de
uma seção crítica nesse código. Para cada mutex em um programa,
apenas um thread pode ser executado dentro de uma seção crítica
por vez. Se um thread estiver em uma seção crítica, todos os outros
threads que chamam mutex.Acquire() serão bloqueados até que o
primeiro thread alcance mutex.Release().
Dizemos que o mutex protege os dados: nesse caso, mutex protege
waitingList. É responsabilidade do programador, porém, garantir que
cada thread sempre adquira o mutex antes de acessar os dados e o
libere depois.
Mutexes são úteis por vários motivos:
• Elas impedem corridas de dados (data races), situações em que
threads competem para ler e escrever simultaneamente na mesma
memória. As corridas de dados são um comportamento indefinido
em C++ e Go. Linguagens gerenciadas como Java e C#
prometem não travar, mas os resultados das corridas de dados
ainda são (para resumir) sem sentido.
• Mesmo que não existissem corridas de dados, mesmo que todas
as leituras e escritas ocorressem uma a uma na ordem do
programa, sem um mutex, as ações de diferentes threads
poderiam se intercalar de maneiras arbitrárias. Imagine tentar
escrever um código que funcione mesmo que outros threads
modifiquem os dados durante a execução. Imagine tentar depurá-
los. Seria como se seu programa fosse assombrado.
• Mutexes suportam programação com invariantes, regras sobre os
dados protegidos que são verdadeiros por construção quando
você os configura e mantém por cada seção crítica.
Naturalmente, todos esses são realmente os mesmos motivos:
condições de corrida descontroladas tornam a programação
intratável. Mutexes trazem alguma ordem ao caos (embora não
tanto quanto canais ou fork-join).
No entanto, na maioria das linguagens, os mutexes são muito fáceis
de confundir. No C++, como na maioria das linguagens, os dados e
o bloqueio (lock) são objetos distintos. Idealmente, os comentários
explicam que cada thread deve adquirir o mutex antes de acessar os
dados:
class FernEmpireApp {
...
private:
// Lista de jogadores esperando para entrar em um jogo. Protegido por `mutex`
vector<PlayerId> waitingList;
Mutex<T>
Agora vamos mostrar uma implementação da lista de espera no
Rust. Em nosso servidor do jogo Fern Empire, cada jogador tem um
ID único:
type PlayerId = u32;
A lista de espera é apenas uma coleção de jogadores:
const GAME_SIZE: usize = 8;
/// Uma lista de espera nunca aumenta para mais de GAME_SIZE jogadores
type WaitingList = Vec<PlayerId>;
A lista de espera é armazenada como um campo do FernEmpireApp, um
singleton configurado em um Arc durante a inicialização do servidor.
Cada thread tem um Arc apontando para ele. Contém toda a
configuração compartilhada e outros fragmentos de que nosso
programa precisa. A maior parte é somente leitura. Como a lista de
espera é compartilhada e mutável, ela deve ser protegida por um
Mutex:
use std::sync::Mutex;
/// Todos os threads têm acesso compartilhado a esse grande struct de contexto
struct FernEmpireApp {
...
waiting_list: Mutex<WaitingList>,
...
}
Ao contrário do C++, no Rust os dados protegidos são armazenados
dentro do Mutex. A configuração do Mutex se parece com isto:
use std::sync::Arc;
mut e Mutex
Pode parecer estranho – certamente nos pareceu estranho no
começo – que nosso método join_waiting_list não recebe self por
referência mut. Sua assinatura de tipo é:
fn join_waiting_list(&self, player: PlayerId)
A coleção subjacente, Vec<PlayerId>, requer uma referência mut quando
você chama o método push. Sua assinatura de tipo é:
pub fn push(&mut self, item: T)
E, porém, esse código compila e funciona bem. O que está
acontecendo aqui?
No Rust, &mut significa acesso exclusivo. Um & simples significa
acesso compartilhado.
Estamos acostumados com tipos passando acesso &mut do pai para o
filho, do contêiner para o conteúdo. Você só espera chamar métodos
&mut self em starships[id].engine se tem uma referência &mut a starships para
começar (ou você possui starships, caso em que parabéns por ser Elon
Musk). Esse é o padrão, porque, se você não tiver acesso exclusivo
ao pai, o Rust geralmente não tem como garantir que você tenha
acesso exclusivo ao filho.
Mas Mutex tem um jeito: o bloqueio. Na verdade, um mutex é pouco
mais do que uma maneira de fazer exatamente isso, fornecer acesso
exclusivo (mut) aos dados internos, mesmo que muitos threads
possam ter acesso compartilhado (não-mut) ao próprio Mutex.
O sistema de tipos do Rust informa o que Mutex faz. Ele reforça
dinamicamente o acesso exclusivo, algo que geralmente é feito
estaticamente, em tempo de compilação, pelo compilador do Rust.
(Você deve se lembrar de que std::cell::RefCell faz a mesma coisa,
exceto sem tentar suportar vários threads. Mutex e RefCell são ambas
versões da mutabilidade interna, que abordamos na página 264.)
Deadlock
Um thread pode passar por um deadlock (impasse) ao tentar
adquirir um bloqueio que já está mantendo:
let mut guard1 = self.waiting_list.lock().unwrap();
let mut guard2 = self.waiting_list.lock().unwrap(); // deadlock
Suponha que a primeira chamada para self.waiting_list.lock() seja bem-
sucedida, conseguindo o bloqueio. A segunda chamada vê que o
bloqueio é mantido, então ela bloqueia, esperando que seja liberado.
Ela vai esperar para sempre. O thread em espera é o mesmo que
mantém o bloqueio.
Em outras palavras, o bloqueio em um Mutex não é um bloqueio
recursivo.
Aqui o bug é óbvio. Em um programa real, as duas chamadas lock()
podem estar em dois métodos diferentes, um dos quais chama o
outro. O código para cada método, considerado separadamente,
pareceria bem. Também existem outras maneiras de passar por um
deadlock, envolvendo vários threads que adquirem vários mutexes
de uma só vez. O sistema de empréstimo do Rust não pode protegê-
lo contra um deadlock. A melhor proteção é manter as seções
críticas pequenas: entre, faça seu trabalho e saia.
Também é possível passar por um deadlock com os canais. Por
exemplo, dois threads podem bloquear, cada um esperando para
receber uma mensagem do outro. Contudo, novamente, um bom
projeto de programa pode lhe dar muita confiança de que isso não
acontecerá na prática. Em um pipeline, como nosso construtor de
índice invertido, o fluxo de dados é acíclico. O deadlock é tão
improvável em um programa como em um pipeline de shell do Unix.
Mutexes envenenados
retorna um Result pelo mesmo motivo que JoinHandle::join()
Mutex::lock()
retorna: falhar elegantemente se outro thread gerou um pânico.
Quando escrevemos handle.join().unwrap(), estamos dizendo ao Rust que
propague o pânico de um thread para outro. A linguagem
mutex.lock().unwrap() é similar.
Se um thread gerar um pânico enquanto mantém um Mutex, o Rust
marca o Mutex como envenenado. Qualquer tentativa subsequente de
lock no Mutex envenenado obterá um resultado de erro. Nossa
chamada a .unwrap() diz ao Rust que gere um pânico se isso
acontecer, propagando o pânico do outro thread para este.
É ruim ter um mutex envenenado? Veneno parece mortal, mas esse
cenário não é necessariamente fatal. Como dissemos no Capítulo 7,
o pânico é seguro. Um thread que gerou um pânico deixa o restante
do programa em um estado seguro.
A razão pela qual mutexes são envenenados quando geram um
pânico, então, não é por medo de comportamento indefinido. Em
vez disso, a preocupação é que você provavelmente programou com
invariantes. Como seu programa gerou em pânico e saiu de uma
seção crítica sem terminar o que estava fazendo, talvez tendo
atualizado alguns campos dos dados protegidos, mas não outros, é
possível que as invariantes agora não funcionem. O Rust envenena o
mutex para evitar que outros threads entrem involuntariamente
nessa situação não funcional e a tornem pior. Você ainda pode
bloquear um mutex envenenado e acessar os dados internos, com
exclusão mútua totalmente aplicada; consulte na documentação
PoisonError::into_inner(). Mas você não fará isso por acaso.
/// Cria um novo canal cujo receptor pode ser compartilhado entre threads.
/// Isso retorna um remetente e um destinatário, assim como o `channel()`
/// de stdlib e às vezes funciona como um substituto drop-in.
pub fn shared_channel<T>() -> (Sender<T>, SharedReceiver<T>) {
let (sender, receiver) = channel();
(sender, SharedReceiver(Arc::new(Mutex::new(receiver))))
}
}
Estamos utilizando um Arc<Mutex<Receiver<T>>>. Os genéricos
realmente se acumularam. Isso acontece com mais frequência no
Rust do que no C++. Pode parecer confuso, mas muitas vezes,
como nesse caso, apenas ler os nomes pode ajudar a explicar o que
está acontecendo, conforme mostrado na Figura 19.11.
Atômicos
O módulo std::sync::atomic contém tipos atômicos para programação
concorrente sem bloqueio. Esses tipos são basicamente iguais aos
atômicos do Standard C++, com alguns extras:
• AtomicIsize e AtomicUsize são tipos inteiros compartilhados
correspondentes aos tipos isize e usize de thread único.
• AtomicI8, AtomicI16, AtomicI32, AtomicI64 e suas variantes sem sinal
como AtomicU8 são tipos inteiros compartilhados que correspondem
aos tipos de thread único i8, i16 etc.
• Um AtomicBool é um valor bool compartilhado.
• Um AtomicPtr<T> é um valor compartilhado do tipo de ponteiro
inseguro *mut T.
O uso adequado de dados atômicos está além do escopo deste livro.
Basta dizer que múltiplos threads podem ler e escrever um valor
atômico de uma só vez sem causar corridas de dados (data races).
Em vez dos operadores aritméticos e lógicos usuais, os tipos
atômicos expõem métodos que executam operações atômicas,
cargas individuais, armazenamentos, trocas e operações aritméticas
que acontecem com segurança, como uma unidade, mesmo que
outros threads também estejam executando operações atômicas que
mexem na mesma posição na memória. Incrementar um AtomicIsize
nomeado atom se parece com isto:
use std::sync::atomic::{AtomicIsize, Ordering};
let atom = AtomicIsize::new(0);
atom.fetch_add(1, Ordering::SeqCst);
Esses métodos podem compilar para instruções de linguagem de
máquina especializadas. Na arquitetura x86-64, essa chamada
.fetch_add() compila para uma instrução lock incq, em que um n += 1
ordinário pode compilar para uma instrução incq simples ou qualquer
número de variações sobre esse tema. O compilador do Rust
também precisa abrir mão de algumas otimizações em torno da
operação atômica, pois – ao contrário de uma carga ou
armazenamento normal – isso pode afetar legitimamente ou ser
afetado por outros threads imediatamente.
O argumento Ordering::SeqCst é uma ordenação de memória.
Ordenações de memória são algo como níveis de isolamento de
transação em um banco de dados. Elas dizem ao sistema o quanto
você se importa com noções filosóficas como causas anteriores a
efeitos e tempo sem loops, em oposição ao desempenho.
Ordenações de memória são cruciais para a exatidão do programa e
são difíceis de entender e ponderar. Felizmente, a penalidade de
desempenho ao escolher a consistência sequencial, a ordem de
memória mais estrita, costuma ser bastante baixa – ao contrário da
penalidade ao colocar um banco de dados SQL no modo SERIALIZABLE.
Então, na dúvida, utilize Ordering::SeqCst. O Rust herda várias outras
ordenações de memória da Standard C++ atômica, com várias
garantias mais fracas sobre a natureza da existência e causalidade.
Não vamos discuti-las aqui.
Um uso simples de atômicos é para cancelamento. Suponha que
temos um thread que está fazendo algum cálculo de longa duração,
como renderizar um vídeo, e queremos cancelá-lo de forma
assíncrona. O problema é comunicar ao thread que queremos que
ele seja encerrado. Podemos fazer isso por meio de um AtomicBool:
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
let cancel_flag = Arc::new(AtomicBool::new(false));
let worker_cancel_flag = cancel_flag.clone();
Esse código cria dois ponteiros inteligentes Arc<AtomicBool> que
apontam para o mesmo AtomicBool alocado no heap , cujo valor inicial
é false. O primeiro, nomeado cancel_flag, permanecerá no thread
principal. O segundo, worker_cancel_flag, será movido para o thread de
trabalho.
Eis o código para o thread de trabalho:
use std::thread;
use std::sync::atomic::Ordering;
let worker_handle = thread::spawn(move || {
for pixel in animation.pixels_mut() {
render(pixel); // traçado de raios - isso leva alguns microssegundos
if worker_cancel_flag.load(Ordering::SeqCst) {
return None;
}
}
Some(animation)
});
Depois de renderizar cada pixel, o thread verifica o valor do
sinalizador chamando o método .load():
worker_cancel_flag.load(Ordering::SeqCst)
Se no thread principal decidirmos cancelar o thread de trabalho,
armazenamos true em AtomicBool e esperamos o thread ser encerrado:
// Cancela a renderização
cancel_flag.store(true, Ordering::SeqCst);
// Descarta o resultado, que provavelmente é `None`
worker_handle.join().unwrap();
Naturalmente, existem outras maneiras de implementar isso. O
aqui poderia ser substituído por um Mutex<bool> ou um canal.
AtomicBool
A principal diferença é que atômicos têm sobrecarga mínima.
Operações atômicas nunca utilizam chamadas de sistema. Uma
carga ou armazenamento geralmente compila para uma única
instrução da CPU.
Atômicos são uma forma de mutabilidade interna, como Mutex ou
RwLock, então seus métodos recebem self por referência (não-mut)
compartilhada. Isso as torna úteis como variáveis globais simples.
Variáveis globais
Suponha que estamos escrevendo código de rede. Queremos usar
uma variável global, um contador que incrementamos toda vez que
servimos um pacote:
/// Número de pacotes que o servidor tratou com sucesso
static PACKETS_SERVED: usize = 0;
Isso compila bem. Há apenas um problema. PACKETS_SERVED não é
mutável, então nunca podemos alterá-lo.
O Rust faz tudo o que pode razoavelmente para desencorajar o
estado mutável global. Constantes declaradas com const são,
obviamente, imutáveis. Variáveis estáticas também são imutáveis
por padrão, assim não há como obter uma referência mut a uma.
Uma static pode ser declarada mut, mas acessá-la não é seguro. A
insistência do Rust quanto à segurança de threads é uma das
principais razões de todas essas regras.
O estado mutável global também tem consequências infelizes para
engenharia de software: ele tende a tornar as várias partes de um
programa mais estreitamente acopladas, mais difíceis de testar e
mais difíceis de alterar posteriormente. Mesmo assim, em alguns
casos simplesmente não há alternativa razoável, então é melhor
descobrir uma maneira segura de declarar variáveis estáticas
mutáveis.
A maneira mais simples de suportar o incremento de PACKETS_SERVED,
mantendo-o thread-safe, é torná-lo um inteiro atômico:
use std::sync::atomic::AtomicUsize;
static PACKETS_SERVED: AtomicUsize = AtomicUsize::new(0);
Depois que essa variável estática é declarada, incrementar a
contagem de pacotes é simples:
use std::sync::atomic::Ordering;
PACKETS_SERVED.fetch_add(1, Ordering::SeqCst);
Globais atômicos são limitadas a inteiros simples e booleanos.
Contudo, criar uma variável global de qualquer outro tipo equivale a
resolver dois problemas.
Primeiro, a variável deve ser thread-safe de alguma forma porque,
do contrário, ela não pode ser global: por segurança, as variáveis
estáticas devem ser tanto Sync como não-mut. Felizmente, já vimos a
solução para esse problema. O Rust tem tipos para compartilhar com
segurança valores que mudam: Mutex, RwLock e os tipos atômicos.
Esses tipos podem ser modificados mesmo quando declarados como
não mut. É o que eles fazem. (Ver “mut e Mutex”, na página 597.)
Segundo, os inicializadores estáticos só podem chamar funções
especificamente marcadas como const, que o compilador pode avaliar
durante o tempo de compilação. Em outras palavras, sua saída é
determinística; depende apenas de seus argumentos, não de
qualquer outro estado ou E/S. Dessa forma, o compilador pode
incorporar os resultados dessa computação como uma constante de
tempo de compilação. Isso é semelhante a constexpr do C++.
Os construtores para os tipos Atomic (AtomicUsize, AtomicBool e assim por
diante) são todos funções const, que permitiram criar um static
AtomicUsize anteriormente. Alguns outros tipos, como String, Ipv4Addr e
Ipv6Addr, têm construtores simples que também são const.
Você também pode definir suas próprias funções const simplesmente
prefixando a assinatura da função com const. O Rust limita o que
funções const podem fazer a um pequeno conjunto de operações, que
são suficientes para serem úteis e, mesmo assim, não permitir
resultados não determinísticos. Funções const não podem receber
tipos como argumentos genéricos, apenas tempos de vida, e não é
possível alocar memória ou operar em ponteiros brutos, mesmo em
blocos unsafe. Podemos, porém, utilizar operações aritméticas
(incluindo aritmética de encapsulamento e saturação), operações
lógicas que não causam curto-circuito e outras funções const. Por
exemplo, podemos criar funções convenientes para tornar a
definição de static e const mais fácil e reduzir a duplicação de código:
const fn mono_to_rgba(level: u8) -> Color {
Color {
red: level,
green: level,
blue: level,
alpha: 0xFF
}
}
const WHITE: Color = mono_to_rgba(255);
const BLACK: Color = mono_to_rgba(000);
Combinando essas técnicas, podemos ser tentados a escrever:
static HOSTNAME: Mutex<String> =
Mutex::new(String::new()); // erro: chamadas em variáveis estáticas são
// limitadas a funções constantes, structs de
// tupla e variantes de tupla
Infelizmente, embora AtomicUsize::new() e String::new() sejam const fn,
Mutex::new() não é. Para contornar essas limitações, precisamos utilizar
o crate lazy_static.
Apresentamos o crate lazy_static em “Construindo valores regex
preguiçosamente (laziy)”, na página 530. Definir uma variável com a
macro lazy_static! permite utilizar qualquer expressão que você quiser
para inicializá-la; ela é executada na primeira vez em que a variável
é desreferenciada e o valor é salvo para todos os usos
subsequentes.
Podemos declarar um HashMap global controlado por Mutex com
lazy_static desta maneira:
use lazy_static::lazy_static;
use std::sync::Mutex;
lazy_static! {
static ref HOSTNAME: Mutex<String> = Mutex::new(String::new());
}
A mesma técnica funciona para outras estruturas de dados
complexas, como HashMap e Deques. Também é bastante útil para
variáveis estáticas que não são mutáveis, mas simplesmente
requerem inicialização não trivial.
Utilizar lazy_static! impõe um pequeno custo de desempenho em cada
acesso aos dados estáticos. A implementação utiliza std::sync::Once,
uma primitiva de sincronização de baixo nível projetada para
inicialização única. Nos bastidores, sempre que uma variável estática
preguiçosa é acessada, o programa executa uma instrução de
carregamento atômico para verificar se a inicialização já ocorreu.
(Once é de propósito bastante especial, então não vamos abordá-la
em detalhes aqui. Geralmente, em vez disso, é mais conveniente
utilizar lazy_static!. Contudo, é útil para inicializar bibliotecas não Rust;
para um exemplo, ver “Uma interface segura para libgit2”, na
página 764.)
De síncrono a assíncrono
Considere o que acontece quando você chama a seguinte função
(não assíncrona, completamente tradicional):
use std::io::prelude::*;
use std::net;
fn cheapo_request(host: &str, port: u16, path: &str)
-> std::io::Result<String>
{
let mut socket = net::TcpStream::connect((host, port))?;
let request = format!("GET {} HTTP/1.1\r\nHost: {}\r\n\r\n", path, host);
socket.write_all(request.as_bytes())?;
socket.shutdown(net::Shutdown::Write)?;
let mut response = String::new();
socket.read_to_string(&mut response)?;
Ok(response)
}
Isso abre uma conexão TCP com um servidor da web, envia uma
solicitação HTTP básica em um protocolo desatualizado1. A
Figura 20.1 mostra a execução dessa função ao longo do tempo.
Esse diagrama mostra como o call stack (pilha de chamada) de
função se comporta à medida que o tempo passa da esquerda para
a direita. Cada chamada de função é um box, colocado sobre o
chamador. Obviamente, a função cheapo_request roda durante toda a
execução. Ela chama funções da biblioteca padrão do Rust como
Implementações TcpStream::connect e TcpStream de write_all e read_to_string.
Essas, por sua vez, chamam outras funções, mas, com o tempo, o
programa faz chamadas de sistema, solicitações ao sistema
operacional para realmente fazer algo, como abrir uma conexão TCP
ou ler ou escrever alguns dados.
Futuros
A abordagem do Rust para suportar operações assíncronas é
introduzir um trait, std::future::Future:
trait Future {
type Output;
// Por enquanto, leia `Pin<&mut Self>` como `&mut Self`
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
enum Poll<T> {
Ready(T),
Pending,
}
Um Future representa uma operação que você pode testar se foi
concluída. O método poll de um futuro nunca espera que a operação
termine: sempre retorna imediatamente. Se a operação estiver
concluída, poll retorna Poll::Ready(output), em que output é o resultado
final. Do contrário, retorna Pending. Se e quando vale a pena verificar
o futuro novamente, ele promete nos informar invocando um waker,
uma função callback fornecida no Context. Chamamos isso de “modelo
piñata” da programação assíncrona: a única coisa que você pode
fazer com um futuro é listá-lo em um poll até que um valor se
diferencie.
Todos os sistemas operacionais modernos incluem variantes das
chamadas de sistema que podemos utilizar para implementar esse
tipo de interface de verificação (polling). No Unix e Windows, por
exemplo, se você colocar um soquete de rede no modo sem
bloqueio, as leituras e escritas retornarão um erro se bloquearem;
você tem de tentar novamente mais tarde.
Portanto, uma versão assíncrona de read_to_string teria uma assinatura
mais ou menos assim:
fn read_to_string(&mut self, buf: &mut String)
-> impl Future<Output = Result<usize>>;
É a mesma assinatura que mostramos anteriormente, exceto pelo
tipo de retorno: a versão assíncrona retorna um futuro de um
Result<usize>. Você precisará verificar esse futuro até obter um
Ready(result) a partir dele. Cada vez que é verificada, a leitura
prossegue até onde conseguir. O result final fornece o valor de
sucesso ou um valor de erro, assim como uma operação de E/S
comum. Este é o padrão geral: a versão assíncrona de qualquer
função recebe os mesmos argumentos da versão síncrona, mas o
tipo de retorno tem um Future em torno dela.
Na verdade, chamar essa versão read_to_string não lê nada; a única
responsabilidade é construir e retornar um futuro que fará o trabalho
real quando verificado. Esse futuro deve conter todas as informações
necessárias para realizar a solicitação feita pela chamada. Por
exemplo, o futuro retornado por read_to_string deve lembrar o fluxo de
entrada em que foi chamado, e a String à qual deve anexar os dados
recebidos. De fato, como o futuro contém as referências self e buf, a
assinatura adequada para read_to_string deve ser:
fn read_to_string<'a>(&'a mut self, buf: &'a mut String)
-> impl Future<Output = Result<usize>> + 'a;
Isso adiciona tempos de vida para indicar que o futuro retornado só
pode existir enquanto os valores que self e buf pegam emprestados
também existirem.
O crate async-std fornece versões assíncronas de todos os recursos de
E/S de std, incluindo um trait Read assíncrono com um método
read_to_string. async-std segue de perto o design de std, reutilizando os
tipos de std em suas próprias interfaces sempre que possível,
portanto erros, resultados, endereços de rede e a maioria dos outros
dados associados são compatíveis entre os dois mundos.
Familiaridade com std ajuda a utilizar async-std e vice-versa.
Uma das regras do trait Future é que, uma vez que um futuro
retornou Poll::Ready, ele pode presumir que nunca mais será
verificado. Alguns futuros apenas retornam Poll::Pending para sempre
se forem verificados excessivamente; outros podem gerar um pânico
ou travar. (Eles não devem, porém, violar a memória ou a segurança
do thread ou causar um comportamento indefinido.) O método
adaptador fuse no trait Future transforma qualquer futuro em um que
simplesmente retorna Poll::Pending continuamente. Mas todas as
formas usuais de consumir futuros respeitam essa regra, então fuse
geralmente não é necessário.
Se a verificação (polling) parece ineficiente, não se preocupe. A
arquitetura assíncrona do Rust é cuidadosam ente projetada para
que desde que as funções básicas de E/S como read_to_string sejam
implementadas corretamente, você só vai verificar um futuro quando
valer a pena. Sempre que poll é chamado, algo em algum lugar deve
retornar Ready, ou pelo menos progredir em direção a esse objetivo.
Explicaremos como isso funciona em “Primitivas de futuro e
executores: Quando vale a pena verificar um futuro novamente?”, na
página 659.
Mas utilizar futuros parece um desafio: quando você faz uma
verificação, o que deve fazer ao receber Poll::Pending? Por enquanto,
terá de verificar algum outro trabalho que esse thread pode fazer,
sem esquecer de voltar a esse futuro mais tarde e verificá-lo
novamente. Todo o programa ficará cheio de tubulações que
monitoram quem está pendente e o que deve ser feito quando
estiver pronto. A simplicidade da nossa função cheapo_request é
arruinada.
Boas notícias! Não é.
Ok(response)
}
Isso é token por token igual à nossa versão original, exceto:
• A função começa com async fn em vez de fn.
• Ela utiliza as versões assíncronas do crate async_std de
TcpStream::connect, write_all e read_to_string. Todas retornam futuros de
seus resultados. (Os exemplos nesta seção utilizam a versão 1.7 de
async_std.)
• Após cada chamada que retorna um futuro, o código informa
.await. Embora isso pareça uma referência a um campo struct
chamado await, na verdade, é uma sintaxe especial incorporada à
linguagem para aguardar até que um futuro esteja pronto. Uma
expressão await é avaliada como o valor final do futuro. É assim
que a função obtém os resultados de connect, write_all e read_to_string.
Ao contrário de uma função comum, quando você chama uma
função assíncrona, ela retorna imediatamente, antes que o corpo
comece a execução. Obviamente, o valor de retorno final da
chamada ainda não foi calculado; o que você obtém é um futuro do
valor final. Assim, se você executar este código:
let response = cheapo_request(host, port, path);
então responseserá um futuro de um std::io::Result<String> e o corpo de
cheapo_request ainda não iniciou a execução. Você não precisa ajustar o
tipo de retorno de uma função assíncrona; o Rust trata
automaticamente async fn f(...) -> T como uma função que retorna um
futuro de um T, não um T diretamente.
O futuro retornado por uma função assíncrona agrupa todas as
informações que o corpo da função precisará para executar: os
argumentos da função, espaço para as variáveis locais e assim por
diante. (É como se você tivesse capturado o stack frame da
chamada como um valor Rust comum.) Assim, response deve conter
os valores passados para host, port e path, uma vez que o corpo de
cheapo_request vai precisar deles para executar.
O tipo específico do futuro é gerado automaticamente pelo
compilador, com base no corpo e argumentos da função. Esse tipo
não tem nome; tudo o que você sabe sobre ele é que implementa
Future<Output=R>, em que R é o tipo de retorno da função assíncrona.
Nesse sentido, futuros das funções assíncronas são como closures:
closures também possuem tipos anônimos, gerados pelo compilador,
que implementam os traits FnOnce, Fn e FnMut.
Quando você verifica pela primeira vez o futuro retornado por
cheapo_request, a execução começa na parte superior do corpo da
função e continua até o primeiro await do futuro retornado por
TcpStream::connect. A expressão await verifica o futuro connect e, se não
estiver pronto, retorna Poll::Pending para o próprio chamador: a
verificação do futuro de cheapo_request não pode passar desse primeiro
await até uma verificação do futuro de TcpStream::connect retornar
Poll::Ready. Portanto, um equivalente aproximado da expressão
TcpStream::connect(...).await pode ser:
{
// Nota: isso é pseudocódigo, não Rust inválido
let connect_future = TcpStream::connect(...);
'retry_point:
match connect_future.poll(cx) {
Poll::Ready(value) => value,
Poll::Pending => {
// Organizar a próxima `verificação` de `cheapo_request`'s
// future para retomar a execução em 'retry_point
...
return Poll::Pending;
}
}
}
Uma expressão await toma posse do futuro e então o verifica. Se
estiver pronto, o valor final do futuro é o valor da expressão await e a
execução continua. Caso contrário, retorna Poll::Pending ao próprio
chamador.
Mas, crucialmente, a próxima verificação do futuro de cheapo_request
não começa na parte superior da função novamente: em vez disso,
ela retoma a execução no meio da função no ponto em que está
prestes a verificar connect_future. Só avançamos para o restante da
função assíncrona depois de o futuro estar pronto.
À medida que o futuro de cheapo_request continua a ser verificado, ele
percorrerá o corpo da função de um await ao próximo, movendo-se
apenas quando o subfuturo que está esperando estiver pronto.
Assim, quantas vezes o futuro de cheapo_request deve ser verificado
depende do comportamento dos subfuturos e do próprio fluxo de
controle da função. O futuro de cheapo_request rastreia o ponto em que
o próximo poll deve retomar e todo o estado local – variáveis,
argumentos, temporários – de que a retomada precisará.
A capacidade de suspender a execução no meio da função e retomar
mais tarde é exclusiva das funções assíncronas. Quando uma função
comum retorna, o stack frame desaparece para sempre. Como as
expressões await dependem da capacidade de retomar, você só pode
usá-las dentro de funções assíncronas.
No momento em que este livro foi escrito, o Rust ainda não permitia
que traits tivessem métodos assíncronos. Somente funções livres e
funções inerentes a um tipo específico podem ser assíncronas.
Levantar essa restrição exigirá uma série de alterações na
linguagem. Enquanto isso, se você precisar definir traits que incluam
funções assíncronas, considere utilizar o crate async-trait, que fornece
uma solução alternativa baseada em macro.
results
}
Essa função chama cheapo_request em cada elemento de requests,
passando o futuro de cada chamada para spawn_local. Ela coleta o
JoinHandle resultante em um vetor e então aguarda cada um deles.
Sem problemas aguardar os manipuladores de junção em qualquer
ordem: como as solicitações já foram geradas, seus futuros serão
verificados conforme necessário sempre que esse thread chamar
block_on e não tem nada melhor para fazer. Todas as solicitações
serão executadas simultaneamente. Assim que estiverem completas,
many_requests retorna os resultados para seu chamador.
Blocos assíncronos
Além das funções assíncronas, o Rust também suporta blocos
assíncronos. Enquanto uma instrução de bloco comum retorna o
valor da última expressão, um bloco assíncrono retorna um futuro do
valor da última expressão. Você pode utilizar expressões await dentro
de um bloco assíncrono.
Um bloco assíncrono se parece com uma instrução de bloco comum,
precedido pela palavra-chave async:
let serve_one = async {
use async_std::net;
Ok(())
};
Isso falha com o seguinte erro:
error: type annotations needed
|
48 | let future = async {
| ------ consider giving `future` a type
...
60 | Ok(())
| ^^ cannot infer type for type parameter `E` declared
| on the enum `Result`
O Rust não pode dizer qual deve ser o tipo de retorno do bloco
assíncrono. O método read_line retorna Result<(), std::io::Error>, mas,
como o operador ? utiliza o trait From para converter o tipo de erro
em questão para o que a situação exigir, o tipo de retorno do bloco
assíncrono pode ser Result<(), E> para qualquer tipo E que implementa
From<std::io::Error>.
Versões futuras do Rust provavelmente adicionarão sintaxe para
indicar um tipo async de retorno do bloco. Por enquanto, você pode
contornar o problema especificando o tipo do bloco Ok final:
let future = async {
...
Ok::<(), std::io::Error>(())
};
Como Result é um tipo genérico que espera os tipos de sucesso e erro
como os parâmetros, podemos especificar esses parâmetros de tipo
ao utilizar Ok ou Err como mostrado aqui.
Construindo funções assíncronas a partir
de blocos assíncronos
Os blocos assíncronos fornecem outra maneira de obter o mesmo
efeito de uma função assíncrona, com um pouco mais de
flexibilidade. Por exemplo, poderíamos escrever nosso cheapo_request
de exemplo como uma função síncrona comum que retorna o futuro
de um bloco assíncrono:
use std::io;
use std::future::Future;
async move {
... use &*host, port, and path ...
}
}
Essa versão permite a captura de blocos assíncronos host e path como
valores String possuídos, não referências &str. Como o futuro possui
todos os dados de que precisa para ser executado, ele é válido para
o tempo de vida 'static. (Especificamos + 'static na assinatura mostrada
anteriormente, mas 'static é o padrão para tipos de retorno -> impl;
portanto, omiti-lo não teria efeito.)
Como essa versão de cheapo_request retorna futuros que são 'static,
podemos passá-los diretamente para spawn_local:
let join_handle = async_std::task::spawn_local(
cheapo_request("areweasyncyet.rs", 80, "/")
);
some_asynchronous_thing().await;
format!("Your splendid string: {}", string)
}
task::spawn(reluctant());
O futuro de uma função assíncrona precisa conter informações
suficientes para que a função continue de uma expressão await.
Nesse caso, futuro de reluctant deve utilizar string depois de await, então
o futuro vai, pelo menos às vezes, conter um valor Rc<String>. Como
ponteiros Rc não podem ser compartilhados com segurança entre
threads, o próprio futuro não pode ser Send. E como spawn só aceita
futuros que são Send, Rust reclama:
error: future cannot be sent between threads safely
|
17 | task::spawn(reluctant());
| ^^^^^^^^^^^ future returned by `reluctant` is not `Send`
|
|
127 | T: Future + Send + 'static,
| ---- required by this bound in `async_std::task::spawn`
|
= help: within `impl Future`, the trait `Send` is not implemented
for `Rc<String>`
note: future is not `Send` as this value is used across an await
|
10 | let string = Rc::new("ref-counted string".to_string());
| ------ has type `Rc<String>` which is not `Send`
11 |
12 | some_asynchronous_thing().await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
await occurs here, with `string` maybe used later
...
15 | }
| - `string` is later dropped here
Essa mensagem de erro é longa, mas contém muitos detalhes úteis:
• Isso explica por que o futuro precisa ser Send: task::spawn requer
isso.
• Explica qual valor não é Send: a variável local string, cujo tipo é
Rc<String>.
• Isso explica por que string afeta o futuro: está no escopo depois do
await indicado.
Há duas maneiras de corrigir esse problema. Uma delas é restringir
o escopo do valor não-Send para que não abranja nenhuma
expressão await e, portanto, não precisa ser salvo no futuro da
função:
async fn reluctant() -> String {
let return_value = {
let string = Rc::new("ref-counted string".to_string());
format!("Your splendid string: {}", string)
// A `Rc<String>` sai do escopo aqui...
};
return_value
}
Outra solução é simplesmente utilizar std::sync::Arc em vez de Rc. Arc
utiliza atualizações atômicas para gerenciar as contagens de
referências, o que o torna um pouco mais lento, mas ponteiros Arc
são Send.
Embora com o tempo você aprenderá a reconhecer e evitar tipos
não-Send, eles podem ser inicialmente um pouco surpreendentes.
(Pelo menos, seus autores frequentemente se surpreendiam.) Por
exemplo, o código Rust mais antigo às vezes utiliza tipos de
resultados genéricos como este:
// Não recomendado!
type GenericError = Box<dyn std::error::Error>;
type GenericResult<T> = Result<T, GenericError>;
Esse tipo GenericError utiliza um objeto trait em box para conter um
valor de qualquer tipo que implemente std::error::Error. Mas não impõe
restrições adicionais: se alguém tivesse um tipo não-Send que
implementasse Error, ele poderia converter um valor em box desse
tipo em um GenericError. Devido a essa possibilidade, GenericError não é
Send e o código a seguir não funcionará:
fn some_fallible_thing() -> GenericResult<i32> {
...
}
results
}
fn main() {
let requests = &["http://example.com".to_string(),
"https://www.red-bean.com".to_string(),
"https://en.wikipedia.org/wiki/Main_Page".to_string()];
[dependencies]
async-std = { version = "1.7", features = ["unstable"] }
tokio = { version = "1.0", features = ["sync"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
Dependemos de quatro crates:
• O crate async-std é a coleção de utilitários e primitivas de E/S
assíncronos que utilizamos ao longo do capítulo.
• O crate tokio é outra coleção de primitivas assíncronas como async-
std, um dos mais antigos e maduros. É amplamente utilizado e
mantém seu design e implementação de alto padrão, mas requer
um pouco mais de cuidado para utilizar do que async-std.
tokio é um crate grande, mas precisamos apenas de um
componente dele, assim o campo features = ["sync"] na linha de
dependência de Cargo.toml de tokio se resume às partes de que
precisamos, tornando isso uma dependência leve.
Quando o ecossistema de bibliotecas assíncronas era menos
maduro, as pessoas evitavam utilizar tanto tokio como async-std no
mesmo programa, mas os dois projetos têm cooperado para
garantir que isso funcione, desde que as regras documentadas de
cada crate sejam seguidas.
• Os crates serde e serde_json que vimos antes, no Capítulo 18. Eles
fornecem ferramentas convenientes e eficientes para gerar e
analisar JSON, que nosso protocolo de bate-papo utiliza para
representar dados na rede. Queremos utilizar alguns recursos
opcionais de serde, portanto nós os selecionamos ao fornecer a
dependência.
Toda a estrutura do aplicativo de bate-papo, cliente e servidor, se
parece com isto:
async-chat
├── Cargo.toml
└── src
├── lib.rs
├── utils.rs
└── bin
├── client.rs
└── server
├── main.rs
├── connection.rs
├── group.rs
└── group_table.rs
Esse layout de pacote utiliza um recurso do Cargo que abordamos
em “Diretório src/bin”, na página 224: além do crate principal da
biblioteca, src/lib.rs, com seu submódulo src/utils.rs, também inclui
dois executáveis:
• src/bin/client.rs é um executável de arquivo único para o cliente
de bate-papo.
• src/bin/server é o executável do servidor, distribuído em quatro
arquivos: main.rs contém a função main e há três submódulos,
connection.rs, group.rs e group_table.rs.
Apresentaremos o conteúdo de cada arquivo de origem ao longo do
capítulo, mas depois que todos estiverem no lugar, se você digitar
cargo build nessa árvore, que compila o crate da biblioteca e, em
seguida, constrói os dois executáveis. Cargo inclui automaticamente
o crate da biblioteca como uma dependência, tornando-o um local
conveniente para colocar as definições compartilhadas pelo cliente e
servidor. De forma similar, cargo check verifica toda a árvore de origem.
Para executar qualquer um dos executáveis, você pode utilizar
comandos como estes:
$ cargo run --release --bin server -- localhost:8088
$ cargo run --release --bin client -- localhost:8088
A opção --bin indica qual executável executar e quaisquer argumentos
após a opção -- são passados para o próprio executável. Nosso
cliente e servidor só querem saber o endereço do servidor e a porta
TCP.
O protocolo
O crate da biblioteca captura todo o nosso protocolo de bate-papo
nesses dois tipos, definidos em lib.rs:
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[test]
fn test_fromclient_json() {
use std::sync::Arc;
assert_eq!(serde_json::from_str::<FromClient>(&json).unwrap(),
from_client);
}
O enum FromClient representa os pacotes que um cliente pode enviar
para o servidor: ele pode pedir para entrar em um grupo e enviar
mensagens para qualquer grupo no qual tenha entrado. FromServer
representa o que o servidor pode enviar de volta: mensagens
postadas em algum grupo e mensagens de erro. Utilizar uma
contagem de referência Arc<String> em vez de uma String simples ajuda
o servidor a evitar criar cópias das strings à medida que gerencia
grupos e distribui mensagens.
Os atributos #[derive] dizem ao crate serde que gere implementações
dos traits Serialize e Deserialize para FromClient e FromServer. Isso permite
chamar serde_json::to_string para convertê-los em valores JSON, enviá-
los pela rede e, finalmente, chamar serde_json::from_str para convertê-
los de volta em suas formas do Rust.
O teste da unidade test_fromclient_json ilustra como isso é utilizado.
Considerando a implementação Serialize derivada de serde, podemos
chamar serde_json::to_string para transformar o valor FromClient específico
neste JSON:
{"Post":{"group_name":"Dogs","message":"Samoyeds rock!"}}
Então, a implementação Deserialize derivada analisa isso de volta em
um valor FromClient equivalente. Observe que os ponteiros Arc em
FromClient não têm efeito na forma serializada: as strings contadas por
referência aparecem diretamente como valores de membro do
objeto JSON.
Recebendo a entrada do usuário: Fluxos
assíncronos
A primeira responsabilidade do nosso cliente de bate-papo é ler os
comandos do usuário e enviar os pacotes correspondentes ao
servidor. O gerenciamento de uma interface de usuário adequada
está além do escopo deste capítulo, portanto faremos a coisa mais
simples possível que funcione: ler as linhas diretamente da entrada
padrão. O seguinte código é inserido em src/bin/client.rs:
use async_std::prelude::*;
use async_chat::utils::{self, ChatResult};
use async_std::io;
use async_std::net;
Ok(())
}
Isso chama async_std::io::stdin para obter um identificador assíncrono
na entrada padrão do cliente, envolve-o em um async_std::io::BufReader
para armazená-lo em buffer e, em seguida, chamar lines para
processar a entrada do usuário linha por linha. Ele tenta analisar
cada linha como um comando correspondente a algum valor
FromClient e, se for bem-sucedido, envia esse valor para o servidor. Se
o usuário inserir um comando não reconhecido, parse_command
imprime uma mensagem de erro e retorna None, assim send_commands
pode contornar o loop novamente. Se o usuário digitar uma
indicação de fim de arquivo, o fluxo lines retorna None e send_commands
retorna. Isso é muito parecido com o código que você escreveria em
um programa síncrono comum, exceto pelo fato de que ele utiliza
versões async_std dos recursos da biblioteca.
O método assíncrono lines de BufReader é interessante. Ele não pode
retornar um iterador, a maneira como a biblioteca padrão faz: o
método Iterator::next é uma função síncrona comum, portanto chamar
command_lines.next() bloquearia o thread até que a próxima linha esteja
pronta. Em vez disso, lines retorna um fluxo dos valores Result<String>.
Um fluxo é o análogo assíncrono de um iterador: ele produz uma
sequência de valores sob demanda, de maneira amigável a
assíncrono. Eis a definição do trait Stream, do módulo async_std::stream:
trait Stream {
type Item;
Ok(())
}
Essa função usa um soquete que recebe dados do servidor,
empacota um BufReader em volta (preste atenção, na versão do
async_std) e, em seguida, passa isso para receive_as_json a fim de obter
um fluxo dos valores FromServer de entrada. Então ele utiliza um loop
while let para lidar com as respostas recebidas, verificando resultados
de erro e imprimindo cada resposta do servidor para o usuário
visualizar.
task::block_on(async {
let socket = net::TcpStream::connect(address).await?;
socket.set_nodelay(true)?;
from_server.race(to_server).await?;
Ok(())
})
}
Tendo obtido o endereço do servidor a partir da linha de comando,
main tem uma série de funções assíncronas que quer chamar, então
ele encapsula o restante da função em um bloco assíncrono e passa
o futuro do bloco para async_std::task::block_on a fim de ser executado.
Uma vez estabelecida a conexão, queremos que as funções
send_commands e handle_replies sejam executadas em conjunto, para que
possamos ver as mensagens de outras pessoas chegarem enquanto
digitamos. Se inserimos o indicador de fim de arquivo ou se a
conexão com o servidor cair, o programa deve sair.
Dado o que fizemos em outras partes do capítulo, você pode esperar
um código como este:
let to_server = task::spawn(send_commands(socket.clone()));
let from_server = task::spawn(handle_replies(socket));
to_server.await?;
from_server.await?;
Mas, como aguardamos os dois identificadores de junção, isso
fornece um programa que é encerrado assim que ambas as tarefas
são concluídas. Queremos sair assim que qualquer um terminar. O
método race em futuros realiza isso. A chamada from_server.race(to_server)
retorna um novo futuro que verifica tanto from_server e to_server e
retorna Poll::Ready(v) assim que um deles estiver pronto. Ambos os
futuros devem ter o mesmo tipo de saída: o valor final é aquele do
futuro finalizado primeiro. O futuro incompleto é dropado.
O método race, com muitos outros recursos úteis, é definido no trait
async_std::prelude::FutureExt, que async_std::prelude torna visível para nós.
Nesse ponto, a única parte do código do cliente que não mostramos
é a função parse_command. Isso é código de tratamento de texto
bastante simples e direto, assim não mostraremos sua definição
aqui. Consulte o código completo no repositório Git para obter
detalhes.
mod connection;
mod group;
mod group_table;
use connection::serve;
fn main() -> ChatResult<()> {
let address = std::env::args().nth(1).expect("Usage: server ADDRESS");
let chat_group_table = Arc::new(group_table::GroupTable::new());
async_std::task::block_on(async {
// Esse código foi mostrado na introdução do capítulo
use async_std::{net, task};
Ok(())
})
}
fn log_error(result: ChatResult<()>) {
if let Err(error) = result {
eprintln!("Error: {}", error);
}
}
A função main do servidor se parece com a do cliente: faz um pouco
de configuração e depois chama block_on para executar um bloco
assíncrono que realiza o trabalho real. Para lidar com as conexões
recebidas dos clientes, ela cria um soquete TcpListener, cujo método
incoming retorna um fluxo de valores std::io::Result<TcpStream>.
Para cada conexão de entrada, geramos uma tarefa assíncrona
executando a função connection::serve. Cada tarefa também recebe uma
referência a um valor GroupTable que representa a lista atual de grupos
de bate-papo do nosso servidor, compartilhada por todas as
conexões por meio de um ponteiro de contagem de referências Arc.
Se connection::serve retorna um erro, registramos uma mensagem na
saída de erro padrão e deixamos a tarefa encerrar. Outras conexões
continuam funcionando normalmente.
use crate::group_table::GroupTable;
Ok(())
}
Isso é quase uma imagem espelhada da função handle_replies do
cliente: a maior parte do código é um loop tratando um fluxo de
entrada dos valores FromClient, construídos a partir de um fluxo TCP
em buffer com receive_as_json. Se ocorrer um erro, geramos um pacote
FromServer::Error para transmitir as más notícias de volta ao cliente.
Além das mensagens de erro, os clientes também querem receber
mensagens dos grupos de bate-papo aos quais ingressaram,
portanto a conexão com o cliente precisa ser compartilhada com
cada grupo. Poderíamos simplesmente criar para todos um clone do
TcpStream, mas, se duas dessas fontes tentarem gravar um pacote no
soquete ao mesmo tempo, a saída poderá ser intercalada e o cliente
acabará recebendo JSON distorcido. Precisamos providenciar acesso
simultâneo seguro à conexão.
Isso é gerenciado com o tipo Outbound, definido em
src/bin/server/connection.rs do seguinte modo:
use async_std::sync::Mutex;
impl Outbound {
pub fn new(to_client: TcpStream) -> Outbound {
Outbound(Mutex::new(to_client))
}
impl Group {
pub fn new(name: Arc<String>) -> Group {
let (sender, _receiver) = broadcast::channel(1000);
Group { name, sender }
}
struct MyPrimitiveFuture {
...
waker: Option<Waker>,
}
struct Shared<T> {
value: Option<T>,
waker: Option<Waker>,
}
O struct Shared deve servir como um ponto de encontro entre o
futuro e o thread que executa a closure, portanto pertence a um Arc
e é protegida com um Mutex. (Um mutex síncrono é bom aqui.)
Verificar o futuro verifica se value está presente e salva o waker em
waker se não estiver presente. O thread que executa a closure salva o
valor de retorno em value e então invoca waker, se presente.
Eis a definição completa de spawn_blocking:
pub fn spawn_blocking<T, F>(closure: F) -> SpawnBlocking<T>
where F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
let inner = Arc::new(Mutex::new(Shared {
value: None,
waker: None,
}));
std::thread::spawn({
let inner = inner.clone();
move || {
let value = closure();
let maybe_waker = {
let mut guard = inner.lock().unwrap();
guard.value = Some(value);
guard.waker.take()
};
SpawnBlocking(inner)
}
Depois de criar o valor Shared, isso gera um thread para executar a
closure, armazenar o resultado no campo value de Shared e invocar o
waker, se houver um.
Podemos implementar Future para SpawnBlocking do seguinte modo:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
guard.waker = Some(cx.waker().clone());
Poll::Pending
}
}
Verificar um SpawnBlocking verifica se o valor da closure está pronto,
tomando posse e retornando-o se estiver pronto. Caso contrário, o
futuro ainda estará pendente, assim salva um clone do waker do
contexto no campo waker do futuro.
Depois que Future retornou Poll::Ready, você não deve pesquisá-lo
novamente. Todas as formas usuais de consumir futuros, como await
e block_on, respeitam essa regra. Se um futuro SpawnBlocking for
verificado várias vezes, nada especialmente terrível acontece, mas
também não faz nenhum esforço para lidar com esse caso. Isso é
típico para futuros escritos à mão.
Implementando block_on
Além de podermos implementar primitivas de futuro, também temos
todas as peças de que precisamos para construir um executor
simples. Nesta seção, escreveremos nossa própria versão de block_on.
Será um pouco mais simples do que a versão de async_std; por
exemplo, ela não suporta spawn_local, variáveis locais de tarefa ou
invocações aninhadas (chamar block_on de código assíncrono). Mas é
suficiente executar nosso cliente e servidor de bate-papo.
Eis o código:
use waker_fn::waker_fn; // Cargo.toml: waker-fn = "1.1"
use futures_lite::pin; // Cargo.toml: futures-lite = "1.11"
use crossbeam::sync::Parker; // Cargo.toml: crossbeam = "0.8"
use std::future::Future;
use std::task::{Context, Poll};
fn block_on<F: Future>(future: F) -> F::Output {
let parker = Parker::new();
let unparker = parker.unparker().clone();
let waker = waker_fn(move || unparker.unpark());
let mut context = Context::from_waker(&waker);
pin!(future);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => return value,
Poll::Pending => parker.park(),
}
}
}
Isso é muito curto, mas há muita coisa acontecendo, então vamos
analisar uma parte de cada vez.
let parker = Parker::new();
let unparker = parker.unparker().clone();
O tipo Parker do crates crossbeam é uma primitiva de bloqueio simples:
chamar parker.park() bloqueia o thread até que alguém chame .unpark()
no Unparker correspondente, que você obtém de antemão chamando
parker.unparker(). Se você usar unpark em um thread que ainda não está
bloqueado por park, a próxima chamada para park retornará
imediatamente, sem bloquear. Nosso block_on utilizará o Parker para
esperar sempre que o futuro não estiver pronto, e o waker que
passamos para os futuros vai remover o bloqueio.
let waker = waker_fn(move || unparker.unpark());
A função waker_fn, a partir do crate de mesmo nome, cria um Waker de
uma determinada closure. Aqui criamos um Waker que, quando
invocado, chama a closure move || unparker.unpark(). Você também pode
criar wakers implementando o trait std::task::Wake, mas waker_fn é um
pouco mais conveniente aqui.
pin!(future);
Dada uma variável contendo um futuro do tipo F, a macro pin! toma
posse do futuro e declara uma nova variável de mesmo nome cujo
tipo é Pin<&mut F> e que pega emprestado o futuro. Isso nos dá o
Pin<&mut Self> exigido pelo método poll. Por motivos que explicaremos
na próxima seção, futuros de funções e blocos assíncronos devem
ser referenciados por meio de um Pin antes que possam ser
verificados.
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => return value,
Poll::Pending => parker.park(),
}
}
Por fim, o loop de verificação é bastante simples. Passando um
contexto que transporta nosso waker, verificamos o futuro até que
ele retorne Poll::Ready. Se retornar Poll::Pending, bloqueamos (park) o
thread, que bloqueia até waker ser invocado. Então tentamos
novamente.
A chamada as_mut permite verificar future sem renunciar à posse;
explicaremos melhor isso na próxima seção.
Fixando
Embora funções e blocos assíncronos sejam essenciais para escrever
código assíncrono claro, lidar com os futuros requer um pouco de
cuidado. O tipo Pin ajuda o Rust a garantir que sejam utilizados com
segurança.
Nesta seção, mostraremos por que os futuros de chamadas e blocos
de função assíncronos não podem ser manipulados tão livremente
quanto os valores Rust comuns. Então vamos mostrar como Pin serve
como um “selo de aprovação” em ponteiros com os quais podemos
contar para gerenciar esses futuros com segurança. Por fim,
mostraremos algumas maneiras de trabalhar com valores Pin.
Ponteiros fixados
O tipo Pin é um envoltório dos ponteiros para futuros que restringe
como os ponteiros podem ser utilizados para garantir que os futuros
não possam ser movidos depois de serem verificados. Essas
restrições podem ser suspensas para futuros que não se importam
em serem movidos, mas são essenciais para verificar com segurança
futuros de funções e blocos assíncronos.
Por ponteiro, queremos dizer qualquer tipo que implemente Deref e
possivelmente DerefMut. Um Pin em torno de um ponteiro é chamado
ponteiro fixado. Pin<&mut T> e Pin<Box<T>> são típicos.
A definição de Pin na biblioteca padrão é simples:
pub struct Pin<P> {
pointer: P,
}
Observe que o campo pointer não é pub. Isso significa que a única
maneira de construir ou utilizar um Pin é por meio de métodos
cuidadosamente escolhidos que o tipo fornece.
Dado o futuro de uma função ou bloco assíncrono, existem apenas
algumas maneiras de obter um ponteiro fixado para ele:
• A macro pin!, do crate futures-lite, mascara uma variável do tipo T
com um novo tipo Pin<&mut T>. A nova variável aponta para o valor
do original, que foi movido para um local temporário anônimo no
stack (pilha). Quando a variável sai do escopo, o valor é dropado.
Usamos pin! na nossa implementação block_on para fixar o futuro
que queríamos verificar.
• O construtor Box::pin da biblioteca padrão toma posse de um valor
de qualquer tipo T, move-o para o heap e retorna um Pin<Box<T>>.
• Pin<Box<T>> implementa From<Box<T>>, assim Pin::from(boxed) toma
posse de boxed e devolve a você um box fixado apontando para o
mesmo T no heap.
Todas as maneiras de obter um ponteiro fixado para esses futuros
implicam desistir da posse do futuro e não há como recuperá-la. O
próprio ponteiro fixado pode ser movido da maneira que você quiser,
é claro, mas mover um ponteiro não move seu referente. Portanto, a
posse de um ponteiro fixado para um futuro serve como prova de
que você desistiu permanentemente da capacidade de mover esse
futuro. Isso é tudo o que precisamos saber para que ele possa ser
verificado com segurança.
Depois de fixar um futuro, se quiser pesquisá-lo, todos os tipos
Pin<pointer to T> têm um método as_mut que desreferencia o ponteiro e
retorna o Pin<&mut T> que poll requer.
O método as_mut também pode ajudá-lo a verificar um futuro sem
abrir mão da posse. Nossa implementação block_on usou-o nesta
função:
pin!(future);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => return value,
Poll::Pending => parker.park(),
}
}
Aqui a macro pin! foi redeclarada future como um Pin<&mut F>, então
poderíamos apenas passar isso para poll. Mas as referências mutáveis
não são Copy, assim Pin<&mut F> também não pode ser Copy, o que
significa que chamar future.poll() diretamente tomaria posse de future,
deixando a próxima iteração do loop com uma variável não
inicializada. Para evitar isso, chamamos future.as_mut() para novamente
pegar emprestado um novo Pin<&mut F> para cada iteração do loop.
Não há como obter uma referência &mut a um futuro fixado: se fosse
possível, você poderia utilizar std::mem::replace ou std::mem::swap para
removê-la e colocar um futuro diferente em seu lugar.
A razão pela qual não precisamos nos preocupar em fixar futuros no
código assíncrono comum é que todas as maneiras mais comuns de
obter o valor de um futuro – aguardando ou passando para um
executor – tomam posse do futuro e gerenciam a fixação
internamente. Por exemplo, nossa implementação block_on toma
posse do futuro e utiliza a macro pin! para produzir o Pin<&mut F>
necessário para verificar. Uma expressão await também toma posse
do futuro e utiliza uma abordagem semelhante à macro pin!
internamente.
Trait Unpin
Entretanto, nem todos os futuros exigem esse tipo de tratamento
cuidadoso. Para qualquer implementação escrita à mão de Future para
um tipo comum, como nosso tipo SpawnBlocking mencionado
anteriormente, as restrições na construção e uso de ponteiros
fixados são desnecessárias.
Esses tipos duráveis implementam o trait do marcador Unpin:
trait Unpin { }
Quase todos os tipos no Rust implementam automaticamente Unpin,
utilizando suporte especial no compilador. Os futuros de função e
bloco assíncronos são as exceções a essa regra.
Para tipos Unpin, Pin não impõe qualquer tipo de restrição. Você pode
criar um ponteiro fixado a partir de um ponteiro comum com Pin::new
e recuperar o ponteiro com Pin::into_inner. O Pin por si só passa as
próprias implementações Deref e DerefMut para o ponteiro.
Por exemplo, String implementa Unpin, assim podemos escrever:
let mut string = "Pinned?".to_string();
let mut pinned: Pin<&mut String> = Pin::new(&mut string);
pinned.push_str(" Not");
Pin::into_inner(pinned).push_str(" so much.");
let new_home = string;
assert_eq!(new_home, "Pinned? Not so much.");
Mesmo depois de criar um Pin<&mut String>, temos acesso mutável
total à string e podemos movê-la para uma nova variável assim que
o Pin foi consumido por into_inner e a referência mutável desaparecer.
Assim, para os tipos que são Unpin – que são quase todos eles – Pin é
um envoltório enfadonho em torno de ponteiros para esse tipo.
Isso significa que, quando você implementa Future para seus próprios
tipos Unpin, sua implementação poll pode tratar self como se fosse &mut
Self, não Pin<&mut Self>. Fixar torna-se algo que você pode ignorar.
Pode ser surpreendente saber que Pin<&mut F> e Pin<Box<F>>
implementam Unpin, mesmo se F não implementar. Isso não soa bem
– como um Pin pode ser Unpin? – mas, se você pensar
cuidadosamente sobre o que cada termo significa, faz sentido.
Mesmo se for não seguro mover F depois de ter sido verificado, um
ponteiro para ele sempre é seguro de ser movido, verificado ou não.
Apenas o ponteiro se move; seu referente frágil permanece parado.
É útil entender isso quando você quer passar o futuro de uma
função ou bloco assíncrono para uma função que só aceita futuros
Unpin. (Essas funções são raras em async_std, mas menos raras em
outros lugares do ecossistema assíncrono.) Pin<Box<F>> é Unpin
mesmo se F não é, assim aplicar Box::pin a um futuro de uma função
ou bloco assíncrono fornece um futuro que você pode utilizar em
qualquer lugar, ao custo de uma alocação de heap.
Existem vários métodos inseguros para trabalhar com Pin que
permitem que você faça o que quiser com o ponteiro e seu alvo,
mesmo para tipos de alvo que não são Unpin. Mas, como explicado no
Capítulo 22, o Rust não pode verificar se esses métodos estão sendo
utilizados corretamente; você se torna responsável por garantir a
segurança do código que os utiliza.
1 Se você realmente precisa de um cliente HTTP, considere utilizar qualquer um dos muitos
crates excelentes como surf ou reqwest que farão o trabalho de forma adequada e
assíncrona.
2 Isso inclui a memória do kernel e conta as páginas físicas alocadas para o thread, não as
páginas virtuais ainda a serem alocadas. Os números são semelhantes no macOS e no
Windows.
3 Alternâncias de contexto no Linux também costumavam estar no intervalo de 0,2 µs, até
que o kernel foi forçado a utilizar técnicas mais lentas devido a falhas de segurança do
processador.
capítulo 21
Macros
Repetição
A macro vec! padrão tem duas formas:
// Repete um valor N vezes
let buffer = vec![0_u8; 1000];
Macros internas
O compilador do Rust fornece várias macros que são úteis quando
você está definindo suas próprias macros. Nenhuma delas poderia
ser implementada utilizando macro_rules! sozinha. Elas são codificadas
em rustc:
file!(), line!(), column!()
file!()se expande para um literal de string: o nome do arquivo atual.
line!() e column!() se expandem para literais u32 que fornecem a linha e
a coluna atuais (contando a partir de 1).
Se uma macro chama outra, que chama outra, tudo em arquivos
diferentes e a última macro chama file!(), line!() ou column!(), ela se
expandirá para indicar o local da primeira chamada de macro.
stringify!(...tokens...)
Expande em um literal de string contendo os tokens fornecidos. A
macro assert! utiliza isso para gerar uma mensagem de erro que
inclui o código da assertiva.
Chamadas de macro no argumento não são expandidas: stringify!(line!
()) se expande para a string "line!()".
O Rust constrói a string a partir dos tokens, então não há quebras
de linha ou comentários na string.
concat!(str0, str1, ...)
Expande em um único literal de string criado pela concatenação de
seus argumentos.
O Rust também define essas macros para consultar o ambiente de
construção:
cfg!(...)
Expande a uma constante booleana, true se a configuração de
compilação atual corresponder à condição entre parênteses. Por
exemplo, cfg!(debug_assertions) é verdadeiro se você estiver compilando
com as asserções de depuração habilitadas.
Essa macro suporta exatamente a mesma sintaxe que o atributo #
[cfg(...)] descrito em “Atributos”, na página 226, mas, em vez de
compilação condicional, você obtém uma resposta verdadeira ou
falsa.
env!("VAR_NAME")
Expande em uma string: o valor da variável de ambiente
especificada em tempo de compilação. Se a variável não existir, é
um erro de compilação.
Isso seria bastante inútil, exceto que o Cargo define muitas
variáveis de ambiente interessantes ao compilar um crate. Por
exemplo, para obter a string da versão atual do seu crate, você
pode escrever:
let version = env!("CARGO_PKG_VERSION");
Uma lista completa dessas variáveis de ambiente está incluída na
documentação do Cargo (https://oreil.ly/CQyuz).
option_env!("VAR_NAME")
Isso é o mesmo que env! exceto que retorna um Option<&'static str>
que será None se a variável especificada não estiver definida.
Outras três macros integradas permitem incorporar código ou dados
de outro arquivo:
include!("file.rs")
Expande para o conteúdo do arquivo especificado, que deve ser um
código Rust válido – uma expressão ou uma sequência de itens.
include_str!("file.txt")
Expande para um &'static str contendo o texto do arquivo
especificado. Você pode utilizar isso desta maneira:
const COMPOSITOR_SHADER: &str =
include_str!("../resources/compositor.glsl");
Se o arquivo não existir ou não for UTF-8 válido, você receberá um
erro de compilação.
include_bytes!("file.dat")
Isso é a mesma coisa, exceto que o arquivo é tratado como dados
binários, não como texto UTF-8. O resultado é um &'static [u8].
Como todas as macros, essas são processadas em tempo de
compilação. Se o arquivo não existir ou não puder ser lido, a
compilação falhará. Não podem falhar em tempo de execução. Em
todos os casos, se o nome do arquivo for um caminho relativo, ele
será resolvido em relação ao diretório que contém o arquivo atual.
O Rust também fornece várias macros convenientes que não
abordamos anteriormente:
todo!(), unimplemented!()
Elas são equivalentes a panic!(), mas transmitem uma intenção
diferente. unimplemented!() é inserida em cláusulas if, braços de match e
outros casos que ainda não foram tratados. Sempre gera um
pânico. todo!() é praticamente o mesmo, mas transmite a ideia de
que esse código simplesmente ainda não foi escrito; alguns IDEs
detectam essa macro e a sinalizam.
matches!(value, pattern)
Compara um valor com um padrão e retorna true se corresponder
ou, do contrário, false. É equivalente a escrever:
match value {
pattern => true,
_ => false
}
Se você está procurando um exercício básico sobre como escrever
macros, essa é uma boa macro para replicar – especialmente
porque a implementação real, que você pode ver na documentação
da biblioteca padrão, é bem simples.
Depurando macros
A depuração de uma macro excêntrica pode ser um desafio. O maior
problema é a falta de visibilidade do processo de expansão de
macros. O Rust geralmente expande todas as macros, encontra
algum tipo de erro e, em seguida, imprime uma mensagem de erro
que não mostra o código totalmente expandido que contém o erro!
Eis três ferramentas para ajudar a solucionar problemas de macros.
(Todos esses recursos são instáveis, mas como foram realmente
projetados para uso durante o desenvolvimento, não no código que
você verificaria, isso não é um grande problema na prática.)
Primeiro e mais simples, você pode solicitar a rustc que mostre a
aparência do seu código depois de expandir todas as macros. Utilize
cargo build --verbose para ver como o Cargo está invocando rustc. Copie a
linha de comando rustc e adicione -Z unstable-options –pretty expanded como
opções. O código totalmente expandido é colocado no seu terminal.
Infelizmente, isso só funciona se o código estiver livre de erros de
sintaxe.
Segundo, o Rust fornece uma macro log_syntax!() que simplesmente
imprime os argumentos no terminal em tempo de compilação. Você
pode utilizar isso para depuração no estilo de println!. Essa macro
requer o flag de recurso #![feature(log_syntax)].
Terceiro, você pode solicitar ao compilador do Rust que registre
todas as chamadas de macro no terminal. Insira trace_macros!(true); em
algum lugar do seu código. A partir desse ponto, sempre que o Rust
expande uma macro, ele imprimirá o nome e os argumentos da
macro. Por exemplo, considere este programa:
#![feature(trace_macros)]
fn main() {
trace_macros!(true);
let numbers = vec![1, 2, 3];
trace_macros!(false);
println!("total: {}", numbers.iter().sum::<u64>());
}
Ele produz esta saída:
$ rustup override set nightly
...
$ rustc trace_example.rs
note: trace_macro
--> trace_example.rs:5:19
|
5| let numbers = vec![1, 2, 3];
| ^^^^^^^^^^^^^
|
= note: expanding `vec! { 1 , 2 , 3 }`
= note: to `< [ _ ] > :: into_vec ( box [ 1 , 2 , 3 ] )`
O compilador mostra o código de cada chamada de macro, antes e
depois da expansão. A linha trace_macros!(false); desativa o
rastreamento novamente, então a chamada para println!() não é
rastreada.
Tipos de fragmento
A primeira tarefa ao escrever qualquer macro complexa é descobrir
como corresponder, ou analisar, a entrada desejada.
Já podemos ver que a macro terá várias regras, porque existem
vários tipos diferentes de coisas nos dados JSON: objetos, arrays,
números e assim por diante. Na verdade, podemos supor que
teremos uma regra para cada tipo JSON:
macro_rules! json {
(null) => { Json::Null };
([ ... ]) => { Json::Array(...) };
({ ... }) => { Json::Object(...) };
(???) => { Json::Boolean(...) };
(???) => { Json::Number(...) };
(???) => { Json::String(...) };
}
Isso não está totalmente correto, pois os padrões de macro não
oferecem nenhuma maneira de separar os últimos três casos, mas
veremos como lidar com isso mais tarde. Os primeiros três casos,
pelo menos, claramente iniciam com tokens diferentes, então vamos
começar com eles.
A primeira regra já funciona:
macro_rules! json {
(null) => {
Json::Null
}
}
#[test]
fn json_null() {
assert_eq!(json!(null), Json::Null); // passa!
}
Para adicionar suporte para arrays JSON, podemos tentar
corresponder os elementos como exprs:
macro_rules! json {
(null) => {
Json::Null
};
([ $( $element:expr ),* ]) => {
Json::Array(vec![ $( $element ),* ])
};
}
Infelizmente, isso não corresponde a todos os arrays JSON. Eis um
teste que ilustra o problema:
#[test]
fn json_array_with_json_element() {
let macro_generated_value = json!(
[
// jSON válido que não corresponde a `$element:expr`
{
"pitch": 440.0
}
]
);
let hand_coded_value =
Json::Array(vec![
Json::Object(Box::new(vec![
("pitch".to_string(), Json::Number(440.0))
].into_iter().collect()))
]);
assert_eq!(macro_generated_value, hand_coded_value);
}
O padrão $( $element:expr ),* significa “uma lista separada por vírgulas
de expressões Rust”. Mas muitos valores JSON, particularmente
objetos, não são expressões Rust válidas. Eles não corresponderão.
Como nem todo fragmento de código que você deseja corresponder
é uma expressão, o Rust oferece suporte a vários outros tipos de
fragmentos, listados na Tabela 21.2.
Tabela 21.2: Tipos de fragmentos suportados por macro_rules!
Tipo de
Pode ser
fragment Correspondências (com exemplos)
seguido por...
o
expr Uma expressão: => , ;
2 + 2, "udon", x.len()
stmt Uma expressão ou declaração, sem nenhum ponto e vírgula à => , ;
direita (difícil de utilizar; em vez disso, tente expr ou block)
ty Um tipo: => , ; = | { [ :
String, Vec<u8>, (&str, bool), dyn Read + Send > as where
path Um caminho (discutido na página 217): => , ; = | { [ :
ferns, ::std::sync::mpsc > as where
pat Um padrão (discutido na página 281): => , = | if in
_, Some(ref x)
item Um item (discutido na página 168): Qualquer coisa
struct Point { x: f64, y: f64 }, mod ferns;
block Um bloco (discutido na página 167): Qualquer coisa
{ s += "ok\n"; true }
meta O corpo de um atributo (discutido na página 226): Qualquer coisa
inline, derive(Copy, Clone), doc="3D models."
literal Um valor literal: Qualquer coisa
1024, "Hello, world!", 1_000_000f64
lifetime Um tempo de vida: Qualquer coisa
'a, 'item, 'static
vis Um especificador de visibilidade: Qualquer coisa
pub, pub(crate), pub(in module::submodule)
ident Um identificador: Qualquer coisa
std, Json, longish_variable_name
tt Uma árvore de tokens (ver texto): Qualquer coisa
Tipo de
Pode ser
fragment Correspondências (com exemplos)
seguido por...
o
;, >=, {}, [0 1 (+ 0 1)]
Recursão em macros
Já vimos um caso trivial de uma macro chamando a si mesma:
nossa implementação de vec! utiliza recursão para suportar vírgulas à
direita. Aqui podemos mostrar um exemplo mais significativo: json!
precisa chamar a si mesma recursivamente.
Podemos tentar oferecer suporte a arrays JSON sem utilizar
recursão, assim:
([ $( $element:tt ),* ]) => {
Json::Array(vec![ $( $element ),* ])
};
Mas isso não funcionaria. Estaríamos colando dados JSON (as
árvores de token $element) diretamente em uma expressão Rust. São
duas linguagens diferentes.
Precisamos converter cada elemento do array do formato JSON em
formato do Rust. Felizmente, existe uma macro que faz isso: a que
estamos escrevendo!
([ $( $element:tt ),* ]) => {
Json::Array(vec![ $( json!($element) ),* ])
};
Os objetos podem ser suportados da mesma forma:
({ $( $key:tt : $value:tt ),* }) => {
Json::Object(Box::new(vec![
$( ($key.to_string(), json!($value)) ),*
].into_iter().collect()))
};
O compilador impõe um limite de recursão nas macros:
64 chamadas, por padrão. Isso é mais do que suficiente para usos
normais de json!, mas macros recursivas complexas às vezes atingem
o limite. Você pode ajustá-lo adicionando esse atributo na parte
superior do crate onde a macro é utilizada:
#![recursion_limit = "256"]
Nossa macro json! está quase completa. Tudo o que resta é oferecer
suporte a valores booleanos, numéricos e de string.
Escopo e higiene
Um aspecto surpreendentemente complicado de escrever macros é
que elas envolvem colar código de diferentes escopos. Portanto, as
próximas páginas abordam as duas maneiras como o Rust lida com
a criação de escopo: uma maneira para variáveis e argumentos
locais e outra maneira para todo o resto.
Para mostrar por que isso é importante, vamos reescrever nossa
regra para analisar objetos JSON (a terceira regra na macro json!
mostrada anteriormente) para eliminar o vetor temporário. Podemos
escrever assim:
({ $($key:tt : $value:tt),* }) => {
{
let mut fields = Box::new(HashMap::new());
$( fields.insert($key.to_string(), json!($value)); )*
Json::Object(fields)
}
};
Agora estamos preenchendo o HashMap sem utilizar collect(), mas
chamando repetidamente o método .insert(). Isso significa que
precisamos armazenar o mapa em uma variável temporária, que
chamamos fields.
Mas então o que acontece se o código que chama json! começar a
utilizar uma variável própria, também chamada fields?
let fields = "Fields, W.C.";
let role = json!({
"name": "Larson E. Whipsnade",
"actor": fields
});
A expansão da macro colaria dois fragmentos de código, ambos
utilizando o nome fields para coisas diferentes!
let fields = "Fields, W.C.";
let role = {
let mut fields = Box::new(HashMap::new());
fields.insert("name".to_string(), Json::from("Larson E. Whipsnade"));
fields.insert("actor".to_string(), Json::from(fields));
Json::Object(fields)
};
Isso pode parecer uma armadilha inevitável sempre que as macros
utilizam variáveis temporárias, e você já pode estar pensando nas
possíveis correções. Talvez devêssemos renomear a variável que a
macro json! define para algo que os chamadores provavelmente não
passarão: em vez de fields, poderíamos chamá-la __json$fields.
A surpresa aqui é que a macro funciona como está. O Rust renomeia
a variável para você! Esse recurso, implementado pela primeira vez
em macros Scheme, chama-se higiene e, portanto, diz-se que o Rust
tem macros higiênicas.
A maneira mais fácil de entender a higiene de macros é imaginar
que, sempre que uma macro é expandida, as partes da expansão
que vêm da própria macro são pintadas com uma cor diferente.
Variáveis de cores diferentes, então, são tratadas como se tivessem
nomes diferentes:
let fields = "Fields, W.C.";
let role = {
let mut fields = Box::new(HashMap::new());
fields.insert("name".to_string(), Json::from("Larson E. Whipsnade"));
fields.insert("actor".to_string(), Json::from(fields));
Json::Object(fields)
};
Observe que os fragmentos de código que foram passados pelo
chamador da macro e colados na saída, como "name" e "actor",
mantêm a cor original (preto). Somente os tokens originados do
template de macro são pintados.
Agora há uma variável chamada fields (declarada no chamador) e
uma variável separada chamada fields (introduzida pela macro). Como
os nomes têm cores diferentes, as duas variáveis não são
confundidas.
Se uma macro realmente precisar referenciar uma variável no
escopo do chamador, o chamador deverá passar o nome da variável
para a macro.
(A metáfora da pintura não pretende ser uma descrição exata de
como a higiene funciona. O mecanismo real é ainda um pouco mais
inteligente do que isso, reconhecendo dois identificadores como
iguais, independentemente da “pintura”, se eles referenciarem uma
variável comum que esteja no escopo da macro e de seu chamador.
Mas casos como esse são raros no Rust. Se entender o exemplo
anterior, você saberá o suficiente para utilizar macros higiênicas.)
Você deve ter notado que muitos outros identificadores foram
pintados com uma ou mais cores à medida que as macros foram
expandidas: Box, HashMap e Json, por exemplo. Apesar da pintura, o
Rust não teve problemas para reconhecer esses nomes de tipo. Isso
ocorre porque a higiene no Rust é limitada a variáveis e argumentos
locais. Quando se trata de constantes, tipos, métodos, módulos,
estática e nomes de macro, o Rust não vê cores.
Isso significa que, se nossa macro json! for utilizada em um módulo
em que Box, HashMap ou Json não estão no escopo, a macro não
funcionará. Mostraremos como evitar esse problema na próxima
seção.
Primeiro, vamos considerar um caso em que a higiene rigorosa do
Rust atrapalha, e precisamos contornar isso. Suponha que temos
muitas funções que contêm esta linha de código:
let req = ServerRequest::new(server_socket.session());
Copiar e colar essa linha é trabalhoso. Podemos utilizar uma macro
em vez disso?
macro_rules! setup_req {
() => {
let req = ServerRequest::new(server_socket.session());
}
}
fn handle_http_request(server_socket: &ServerSocket) {
setup_req!(); // declara `req`, utiliza `server_socket`
... // código que utiliza `req`
}
Quando este livro foi escrito, isso não funcionava. Seria necessário
que o nome server_socket na macro referenciasse ao server_socket local
declarado na função, e vice-versa para a variável req. Mas a higiene
evita que nomes em macros “colidam” com nomes em outros
escopos – mesmo em casos como esse, em que é isso que você
deseja.
A solução é passar para a macro todos os identificadores que você
planeja utilizar dentro e fora do código da macro:
macro_rules! setup_req {
($req:ident, $server_socket:ident) => {
let $req = ServerRequest::new($server_socket.session());
}
}
fn handle_http_request(server_socket: &ServerSocket) {
setup_req!(req, server_socket);
... // código que utiliza `req`
}
Como req e server_socket agora são fornecidos pela função, elas têm a
“cor” certa para esse escopo.
A higiene torna o uso dessa macro um pouco mais complicado, mas
isso é um recurso, não um bug: é mais fácil raciocinar sobre macros
higiênicas sabendo que elas não podem bagunçar variáveis locais de
uma maneira dissimulada. Se você procurar um identificador como
server_socket em uma função, encontrará todos os lugares onde ela é
utilizada, incluindo chamadas de macro.
mod m {
crate::lazy_static!{ }
}
Obviamente, fazer qualquer uma dessas coisas significa que sua
macro pode ser chamada em outros módulos. Uma macro
exportada, portanto, não deve depender de nada que esteja no
escopo – não há como dizer o que estará no escopo em que será
utilizada. Mesmo os recursos do prelúdio padrão podem ser
escondidos.
Em vez disso, a macro deve utilizar caminhos absolutos para
quaisquer nomes que utilizar. macro_rules! fornece o fragmento
especial $crate para ajudar nisso. Não é o mesmo que crate, que é
uma palavra-chave que pode ser utilizada em caminhos em qualquer
lugar, não apenas em macros. $crate age como um caminho absoluto
para o módulo raiz do crate em que a macro foi definida. Em vez de
dizer Json, podemos escrever $crate::Json, que funciona mesmo se Json
não tiver sido importada. HashMap pode ser alterado para
::std::collections::HashMap ou $crate::macros::HashMap. Nesse último caso,
teremos de reexportar HashMap, porque $crate não pode ser utilizado
para acessar recursos privados de um crate. Na verdade, apenas se
expande para algo como ::jsonlib, um caminho comum. As regras de
visibilidade não são afetadas.
Depois de mover a macro para um módulo próprio macros e modificá-
lo para utilizar $crate, ela se parece com isto. Esta é a versão final:
// macros.rs
pub use std::collections::HashMap;
pub use std::boxed::Box;
pub use std::string::ToString;
#[macro_export]
macro_rules! json {
(null) => {
$crate::Json::Null
};
([ $( $element:tt ),* ]) => {
$crate::Json::Array(vec![ $( json!($element) ),* ])
};
({ $( $key:tt : $value:tt ),* }) => {
{
let mut fields = $crate::macros::Box::new(
$crate::macros::HashMap::new());
$(
fields.insert($crate::macros::ToString::to_string($key),
json!($value));
)*
$crate::Json::Object(fields)
}
};
($other:tt) => {
$crate::Json::from($other)
};
}
Como o método .to_string() é parte do trait ToString padrão, utilizamos
$crate para referenciar isso também, utilizando a sintaxe que
introduzimos em “Chamadas de método totalmente qualificadas”, na
página 318: $crate::macros::ToString::to_string($key). No nosso caso, isso não
é estritamente necessário para fazer a macro funcionar, porque
ToString está no prelúdio padrão. Mas, se você estiver chamando
métodos de um trait que pode não estar no escopo no ponto em que
a macro é chamada, uma chamada de método totalmente
qualificada é a melhor maneira de fazer isso.
Inseguro de quê?
No início deste livro, mostramos um programa C que falha de
maneira surpreendente porque não segue uma das regras prescritas
pelo padrão C. Você pode fazer o mesmo no Rust:
$ cat crash.rs
fn main() {
let mut a: usize = 0;
let ptr = &mut a as *mut usize;
unsafe {
*ptr.offset(3) = 0x7ffff72f484c;
}
}
$ cargo build
Compiling unsafe-samples v0.1.0
Finished debug [unoptimized + debuginfo] target(s) in 0.44s
$ ../../target/debug/crash
crash: Error: .netrc file is readable by others.
crash: Remove password or make file unreadable by others.
Segmentation fault (core dumped)
$
Esse programa pega emprestada uma referência mutável à variável
local a, converte-a em um ponteiro bruto do tipo *mut usize e então
utiliza o método offset para produzir um ponteiro cujo endereço é três
palavras mais adiante na memória. Isso acontece onde o endereço
de retorno de main é armazenado. O programa sobrescreve o
endereço de retorno com uma constante, de modo que retornar de
main apresenta um comportamento surpreendente. O que torna essa
falha possível é o uso incorreto de recursos inseguros do programa –
nesse caso, a capacidade de desreferenciar ponteiros brutos.
Um recurso inseguro é aquele que impõe um contrato: regras que o
Rust não pode aplicar automaticamente, mas que você deve seguir
para evitar comportamento indefinido.
Um contrato vai além das verificações usuais de tipo e verificações
de tempo de vida, impondo outras regras específicas a esse recurso
inseguro. Normalmente, o próprio Rust não sabe nada sobre o
contrato; é explicado apenas na documentação do recurso. Por
exemplo, o tipo de ponteiro bruto tem um contrato que proíbe
desreferenciar um ponteiro que avançou para além do final de seu
referente original. A expressão *ptr.offset(3) = ... neste exemplo quebra
esse contrato. Mas, como mostra a transcrição, o Rust compila o
programa sem reclamar: as verificações de segurança não detectam
essa violação. Ao utilizar recursos inseguros, você, como
programador, assume a responsabilidade de verificar se seu código
está de acordo com os contratos.
Muitos recursos têm regras que você deve seguir para usá-los
corretamente, mas essas regras não são contratos no sentido que
queremos dizer aqui, a menos que as possíveis consequências
incluam comportamento indefinido. Comportamento indefinido é
comportamento que o Rust supõe firmemente que seu código nunca
pode exibir. Por exemplo, o Rust supõe que você não substituirá o
endereço de retorno de uma chamada de função por outra coisa. O
código que passa nas verificações de segurança usuais do Rust e
está em conformidade com os contratos dos recursos inseguros que
ele utiliza não pode fazer isso. Como o programa viola o contrato de
ponteiro bruto, seu comportamento é indefinido e sai dos trilhos.
Se seu código exibir comportamento indefinido, você violou sua
metade do acordo com o Rust, e o Rust se recusa a prever as
consequências. Extrair mensagens de erro irrelevantes das
profundezas das bibliotecas do sistema e travar é uma consequência
possível; entregar o controle do seu computador para um invasor é
outra. Os efeitos podem variar de uma versão do Rust para outra,
sem aviso prévio. Às vezes, porém, o comportamento indefinido não
tem consequências visíveis. Por exemplo, se a função main nunca
retorna (talvez chame std::process::exit para encerrar o programa
antecipadamente), então o endereço de retorno corrompido
provavelmente não será importante.
Você só pode utilizar recursos inseguros dentro de um bloco unsafe ou
uma função unsafe; explicaremos ambos nas seções a seguir. Isso
torna mais difícil utilizar recursos inseguros sem saber: forçando-o a
escrever um bloco ou função unsafe, o Rust garante que você
reconheceu que seu código pode ter regras adicionais a serem
seguidas.
Blocos inseguros
Um bloco unsafe se parece com um bloco Rust comum precedido pela
palavra-chave unsafe, com a diferença de que você pode utilizar
recursos inseguros no bloco:
unsafe {
String::from_utf8_unchecked(ascii)
}
Sem a palavra-chave unsafe na frente do bloco, o Rust se oporia ao
uso de from_utf8_unchecked, que é uma função unsafe (insegura). Com o
bloco unsafe em torno dela, você pode utilizar esse código em
qualquer lugar.
Como um bloco Rust comum, o valor de um bloco unsafe é o da
expressão final, ou () se não houver um. A chamada para
String::from_utf8_unchecked mostrada anteriormente fornece o valor do
bloco.
Um bloco unsafe libera cinco opções adicionais para você:
• Você pode chamar funções unsafe. Cada função unsafe deve
especificar seu próprio contrato, dependendo do propósito.
• Você pode desreferenciar ponteiros brutos. O código seguro pode
passar ponteiros brutos, compará-los e criá-los por conversão de
referências (ou mesmo de números inteiros), mas apenas código
inseguro pode realmente usá-los para acessar a memória.
Abordaremos ponteiros brutos em detalhes e explicaremos como
usá-los com segurança em “Ponteiros brutos”, na página 719.
• Você pode acessar os campos de unions, que o compilador não
pode ter certeza se contêm padrões de bits válidos para seus
respectivos tipos.
• Você pode acessar variáveis static mutáveis. Como explicado em
“Variáveis globais”, na página 606, o Rust não pode ter certeza
quando threads estão utilizando variáveis static mutáveis, portanto
o contrato exige que você garanta que todo o acesso esteja
sincronizado corretamente.
• Você pode acessar funções e variáveis declaradas por meio da
interface de função externa (Foreign Function Interface ou FFI) do
Rust. Essas são consideradas unsafe mesmo quando imutáveis, pois
são visíveis para códigos escritos em outras linguagens que
podem não respeitar as regras de segurança do Rust.
Na verdade, restringir recursos inseguros a blocos unsafe não o
impede de fazer o que você quer. É perfeitamente possível inserir
um bloco unsafe em seu código e seguir em frente. O benefício da
regra reside principalmente em chamar a atenção humana para o
código cuja segurança o Rust não pode garantir:
• Você não utilizará acidentalmente recursos inseguros e descobrirá
que foi responsável por contratos que nem sabia que existiam.
• Um bloco unsafe atrai mais atenção dos revisores. Alguns projetos
até tem automação para garantir isso, sinalizando mudanças no
código que afetam blocos unsafe que requerem atenção especial.
• Ao pensar em escrever um bloco unsafe, você pode reservar um
momento para se perguntar se sua tarefa realmente requer tais
medidas. Se é para desempenho, você tem medições para mostrar
que isso é um gargalo? Talvez haja uma boa maneira de alcançar
a mesma coisa no Rust seguro.
impl Ascii {
/// Cria um `Ascii` a partir do texto ASCII em `bytes`.
/// Retorna um erro `NotAsciiError` se `bytes` contém
/// algum caractere não-ASCII.
pub fn from_bytes(bytes: Vec<u8>) -> Result<Ascii, NotAsciiError> {
if bytes.iter().any(|&byte| !byte.is_ascii()) {
return Err(NotAsciiError(bytes));
}
Ok(Ascii(bytes))
}
}
// Essa chamada não envolve alocação ou cópias de texto, apenas uma varredura
let ascii: Ascii = Ascii::from_bytes(bytes)
.unwrap(); // Sabemos que esses bytes escolhidos estão ok
Funções inseguras
Uma definição de função unsafe se parece com uma definição de
função comum precedida pela palavra-chave unsafe. O corpo de uma
função unsafe é automaticamente considerado um bloco unsafe.
Você só pode chamar funções unsafe dentro de blocos unsafe. Isso
significa que marcar uma função unsafe avisa os chamadores que a
função tem um contrato que eles devem atender para evitar
comportamento indefinido.
Por exemplo, eis um novo construtor para o tipo Ascii que
introduzimos antes que constrói um Ascii de um vetor de bytes sem
verificar se o conteúdo é ASCII válido:
// Isso deve ser colocado dentro do módulo `my_ascii`
impl Ascii {
/// Constrói um valor `Ascii` de `bytes`, sem verificar
/// se `bytes` realmente contém ASCII bem formado.
///
/// Esse construtor é infalível, e retorna um `Ascii` diretamente, em vez de
/// um `Resultado<Ascii, NotAsciiError> ` como o construtor `from_bytes`retorna
///
/// # Segurança
///
/// O chamador deve garantir que `bytes` contenha apenas caracteres ASCII:
/// bytes não maiores que 0x7f. Do contrário, o efeito é indefinido
pub unsafe fn from_bytes_unchecked(bytes: Vec<u8>) -> Ascii {
Ascii(bytes)
}
}
Presumivelmente, de alguma forma a chamada de código
Ascii::from_bytes_unchecked já sabe que o vetor em mãos contém apenas
caracteres ASCII, então a verificação que Ascii::from_bytes insiste em
realizar seria perda de tempo, e o chamador teria de escrever um
código para lidar com os resultados Err que ele sabe que nunca
ocorrerão. Ascii::from_bytes_unchecked permite que esse chamador evite
as verificações e o tratamento de erros.
Mas antes enfatizamos a importância dos construtores e métodos
públicos assegurando que os valores Ascii estejam bem formados. O
código from_bytes_unchecked não cumpre essa responsabilidade?
Não exatamente: from_bytes_unchecked cumpre suas obrigações
transmitindo-as ao chamador por meio de seu contrato. A presença
desse contrato é o que torna correto marcar essa função unsafe:
apesar do fato de que a função em si não realiza operações
inseguras, os chamadores devem seguir regras que o Rust não pode
impor automaticamente para evitar comportamento indefinido.
Você pode provocar comportamento indefinido quebrando o contrato
de Ascii::from_bytes_unchecked? Sim. Você pode construir um String
contendo UTF-8 malformado da seguinte forma:
// Imagine que esse vetor seja o resultado de algum processo complicado
// que esperávamos que produzisse ASCII. Algo está errado!
let bytes = vec![0xf7, 0xbf, 0xbf, 0xbf];
// `bogus` agora contém UTF-8 malformado. A análise de seu primeiro caractere produz
// um `char` que não é um ponto de código Unicode válido. Isso é comportamento
// indefinido, assim a linguagem não informa como essa asserção deve se comportar
assert_eq!(bogus.chars().next().unwrap() as u32, 0x1fffff);
Em certas versões do Rust, em algumas plataformas, observou-se
que essa asserção falhou com a seguinte mensagem de erro
divertida:
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `2097151`,
right: `2097151`', src/main.rs:42:5
Esses dois números parecem iguais para nós, mas isso não é culpa
do Rust; é culpa do bloco unsafe anterior. Quando dizemos que
comportamento indefinido leva a resultados imprevisíveis, é a isso
que nos referimos.
Isso ilustra dois fatos cruciais sobre bugs e código inseguro:
• Bugs que ocorrem antes que o bloco unsafe possa quebrar
contratos. Se um bloco unsafe causa comportamento indefinido,
pode depender não apenas do código no próprio bloco, mas
também do código que fornece os valores nos quais ele opera.
Tudo de que seu código unsafe depende para atender os contratos
é fundamental para a segurança. A conversão de Ascii em String
baseada em String::from_utf8_unchecked está bem definida apenas se o
restante do módulo mantiver adequadamente invariantes de Ascii.
• As consequências de quebrar um contrato podem aparecer depois
que você sai do bloco unsafe. O comportamento indefinido
cortejado pelo descumprimento do contrato de um recurso
inseguro muitas vezes não ocorre dentro do próprio bloco unsafe.
Construir um String falso como mostrado antes só pode causar
problemas muito mais tarde na execução do programa.
Essencialmente, o verificador de tipos, o verificador de empréstimos
e outras verificações estáticas do Rust inspecionam seu programa e
tentam gerar evidência de que ele não pode exibir comportamento
indefinido. Quando o Rust compila seu programa com sucesso, isso
significa que ele conseguiu provar que seu código é sólido. Um bloco
unsafe é uma lacuna nesse processo em que você diz ao Rust: “Esse
código é bom, confie em mim”. Se sua afirmação é verdadeira pode
depender de qualquer parte do programa que influencia o que
acontece no bloco unsafe e as consequências de estar errado podem
aparecer em qualquer lugar influenciado pelo bloco unsafe. Escrever a
palavra-chave unsafe equivale a um lembrete de que você não está
obtendo todos os benefícios das verificações de segurança da
linguagem.
Dada a escolha, você deve naturalmente preferir criar interfaces
seguras, sem contratos. É muito mais fácil trabalhar com elas, pois
os usuários podem contar com as verificações de segurança do Rust
para garantir que o código esteja livre de comportamento indefinido.
Mesmo que sua implementação utilize recursos inseguros, é melhor
utilizar os tipos, tempos de vida e sistema de módulos do Rust para
cumprir os contratos usando apenas o que você pode garantir, em
vez de passar responsabilidades para seus chamadores.
Infelizmente, não é incomum encontrar funções inseguras em
ambientes cuja documentação não se preocupa em explicar os
contratos. Espera-se que você mesmo deduza as regras, com base
em sua experiência e conhecimento de como o código se comporta.
Se você já se perguntou de modo constrangedor se o que está
fazendo com uma API C ou C++ está OK, então você sabe como é.
Comportamento indefinido
Na introdução, dissemos que o termo comportamento indefinido
significa “comportamento que o Rust supõe firmemente que seu
código nunca poderia exibir”. Essa é uma frase estranha,
especialmente porque sabemos, por nossa experiência com outras
linguagens, que esses comportamentos ocorrem acidentalmente
com alguma frequência. Por que esse conceito é útil para definir as
obrigações do código inseguro?
Um compilador é um tradutor de uma linguagem de programação
em outra. O compilador do Rust pega um programa Rust e o traduz
em um programa equivalente em linguagem de máquina. Mas o que
significa dizer que dois programas em linguagens tão diferentes são
equivalentes?
Felizmente, essa questão é mais fácil para programadores do que
para linguistas. Costumamos dizer que dois programas são
equivalentes se eles sempre terão o mesmo comportamento visível
quando executados: eles fazem as mesmas chamadas de sistema,
interagem com bibliotecas estrangeiras de maneiras equivalentes e
assim por diante. É um pouco como um teste de Turing de
programas: se você não consegue dizer se está interagindo com o
original ou com a tradução, então eles são equivalentes.
Agora considere o seguinte código:
let i = 10;
very_trustworthy(&i);
println!("{}", i * 100);
Mesmo não sabendo nada sobre a definição de very_trustworthy,
podemos ver que recebe apenas uma referência compartilhada a i,
assim a chamada não pode mudar o valor i. Como o valor passado
para println! sempre será 1000, o Rust pode traduzir esse código em
linguagem de máquina como se tivéssemos escrito:
very_trustworthy(&10);
println!("{}", 1000);
Essa versão transformada tem o mesmo comportamento visível da
original, e provavelmente é um pouco mais rápida. Mas faz sentido
considerar o desempenho dessa versão apenas se concordarmos que
ela tem o mesmo significado da original. E se very_trustworthy fosse
definido da seguinte forma?
fn very_trustworthy(shared: &i32) {
unsafe {
// Transforma a referência compartilhada em um ponteiro mutável.
// Isso é um comportamento indefinido.
let mutable = shared as *const i32 as *mut i32;
*mutable = 20;
}
}
Esse código quebra as regras para referências compartilhadas: ele
muda o valor de i para 20, embora deva ser imutável porque i é
emprestado para compartilhamento. Como resultado, a
transformação que fizemos no chamador agora tem um efeito bem
visível: se o Rust transformar o código, o programa imprimirá 1000;
se não alterar o código e utilizar o novo valor de i, ele imprimirá 2000.
Quebrar as regras para referências compartilhadas em very_trustworthy
significa que as referências compartilhadas não se comportarão
como esperado nos chamadores.
Esse tipo de problema surge em quase todo tipo de transformação
que o Rust pode tentar. Até mesmo colocar inline uma função no
local de chamada supõe, entre outras coisas, que, quando a
chamada termina, o fluxo de controle retornará ao local de
chamada. Mas abrimos o capítulo com um exemplo de código
malcomportado que viola até mesmo essa suposição.
É basicamente impossível para o Rust (ou qualquer outra linguagem)
avaliar se uma transformação em um programa preserva o
significado, a menos que possa confiar nos recursos fundamentais
da linguagem para se comportar conforme projetado. E se
apresentam ou não esse comportamento pode depender não apenas
do código em mãos, mas de outras partes potencialmente distantes
do programa. Para fazer qualquer coisa no seu código, o Rust deve
supor que o restante de seu programa é bem-comportado.
Aqui, então, estão as regras do Rust para programas bem-
comportados:
• O programa não deve ler a memória não inicializada.
• O programa não deve criar valores primitivos inválidos:
– Referências, boxes ou ponteiros fn que são null.
– valores bool que não são 0 ou 1.
– valores enum com valores discriminantes inválidos.
– valores char que não são válidos, pontos de código Unicode não
substitutos.
– valores str que não são UTF-8 bem formados.
– Ponteiros (fat) gordos com comprimentos inválidos de
vtables/fatia.
– Qualquer valor do tipo “never” (nunca), escrito !, para funções
que não retornam.
• As regras para referências explicadas no Capítulo 5 devem ser
seguidas. Nenhuma referência pode sobreviver ao referente;
acesso compartilhado é acesso somente leitura; e acesso mutável
é acesso exclusivo.
• O programa não deve desreferenciar ponteiros nulos, alinhados
incorretamente ou pendentes.
• O programa não deve utilizar um ponteiro para acessar a
memória fora da alocação à qual o ponteiro está associado.
Explicaremos essa regra em detalhes em “Desreferenciando
ponteiros brutos com segurança”, na página 722.
• O programa deve estar livre de corridas de dados (data races).
Uma corrida de dados ocorre quando dois threads acessam a
mesma posição na memória sem sincronização, e pelo menos um
dos acessos é uma gravação.
• O programa não deve terminar em uma chamada feita de outra
linguagem, via interface de função externa, como explicado em
“Desempilhamento”, na página 189.
• O programa deve cumprir os contratos das funções da biblioteca
padrão.
Como ainda não temos um modelo completo da semântica do Rust
para código unsafe, essa lista provavelmente vai evoluir ao longo do
tempo, mas esses provavelmente permanecerão proibidos.
Qualquer violação dessas regras constitui um comportamento
indefinido e torna os esforços do Rust para otimizar seu programa e
traduzi-lo em linguagem de máquina não confiável. Se você quebrar
a última regra e passar UTF-8 malformado para
String::from_utf8_unchecked, talvez 2097151 não seja tão igual a 2097151
afinal de contas.
O código Rust que não utiliza recursos inseguros tem a garantia de
seguir todas as regras anteriores, uma vez compilado (supondo que
o compilador não tenha bugs; estamos chegando lá, mas a curva
nunca cruzará a assíntota). Somente quando você utiliza recursos
não seguros, essas regras se tornam sua responsabilidade.
Em C e C++, o fato de seu programa compilar sem erros ou avisos
significa muito menos; como mencionamos na introdução deste
livro, mesmo os melhores programas C e C++ escritos por projetos
respeitados que mantêm o código em padrões elevados exibem um
comportamento indefinido na prática.
Traits inseguros
Um trait inseguro é um trait que tem um contrato que o Rust não
pode verificar ou impor que os implementadores satisfaçam para
evitar comportamento indefinido. Para implementar um trait
inseguro, você deve marcar a implementação como insegura. Cabe a
você entender o contrato do trait e certificar-se de que seu tipo o
satisfaça.
Uma função que limita as variáveis de tipo com um trait inseguro é
tipicamente aquela que utiliza traits inseguros e só atende os
contratos dependendo do contrato do trait inseguro. Uma
implementação incorreta do trait pode fazer com que tal função
exiba um comportamento indefinido.
std::marker::Send e std::marker::Sync são os exemplos clássicos de traits
inseguros. Esses traits não definem nenhum método, portanto são
triviais de implementar para qualquer tipo que você desejar. Mas
eles têm contratos: Send exige que os implementadores estejam
seguros para mover para outro thread e Sync exige que sejam
seguros para compartilhar entre threads por meio de referências
compartilhadas. Implementar Send para um tipo inadequado, por
exemplo, faria com que std::sync::Mutex não mais estivesse protegido
de corridas de dados (data races).
Como um exemplo simples, a biblioteca padrão do Rust costumava
incluir um trait inseguro, core::nonzero::Zeroable, para tipos que podem
ser inicializados com segurança definindo todos os seus bytes como
zero. Claramente, sem problemas zerar um usize, mas zerar um &T
fornece uma referência nula, que causará uma falha se
desreferenciada. Para os tipos que fossem Zeroable, algumas
otimizações eram possíveis: você poderia inicializar uma array deles
rapidamente com std::ptr::write_bytes (o equivalente do Rust a memset)
ou utilizar chamadas do sistema operacional que alocam páginas
zeradas. (Zeroable era instável e foi movido para uso interno apenas
no crate num no Rust 1.26, mas é um bom exemplo simples e do
mundo real.)
Zeroable era um trait marcador típico, sem métodos ou tipos
associados:
pub unsafe trait Zeroable {}
As implementações para os tipos apropriados eram igualmente
simples e diretas:
unsafe impl Zeroable for u8 {}
unsafe impl Zeroable for i32 {}
unsafe impl Zeroable for usize {}
// e assim por diante para todos os tipos de inteiros
Com essas definições, poderíamos escrever uma função que aloca
rapidamente um vetor de um determinado comprimento contendo
um tipo Zeroable:
use core::nonzero::Zeroable;
Ponteiros brutos
Um ponteiro bruto no Rust é um ponteiro irrestrito. Você pode
utilizar ponteiros brutos para formar todos os tipos de estruturas que
os tipos de ponteiro verificados do Rust não podem, como listas
duplamente vinculadas ou gráfos arbitrários dos objetos. Mas, como
os ponteiros brutos são muito flexíveis, o Rust não pode dizer se
você os está utilizando com segurança ou não, então você pode
desreferenciá-los apenas em um bloco unsafe.
Os ponteiros brutos são essencialmente equivalentes aos ponteiros C
ou C++, portanto também são úteis para interagir com o código
escrito nessas linguagens.
Existem dois tipos de ponteiros brutos:
• Um *mut T é um ponteiro bruto para um T que permite modificar o
referente.
• Um *const T é um ponteiro bruto para um T que permite apenas a
leitura de seu referente.
(Não há um tipo *T simples; você deve sempre especificar const ou
mut.)
Você pode criar um ponteiro bruto por conversão de uma referência,
e desreferenciá-lo com o operador *:
let mut x = 10;
let ptr_x = &mut x as *mut i32;
let y = Box::new(20);
let ptr_y = &*y as *const i32;
unsafe {
*ptr_x += *ptr_y;
}
assert_eq!(x, 30);
Ao contrário de boxes e referências, ponteiros brutos podem ser
nulos, como NULL em C++ ou nullptr em C:
fn option_to_raw<T>(opt: Option<&T>) -> *const T {
match opt {
None => std::ptr::null(),
Some(r) => r as *const T
}
}
assert!(!option_to_raw(Some(&("pea", "pod"))).is_null());
assert_eq!(option_to_raw::<i32>(None), std::ptr::null());
Esse exemplo não tem blocos unsafe: criar ponteiros brutos, passá-los
e compará-los é seguro. Somente desreferenciar um ponteiro bruto
não é seguro.
Um ponteiro bruto para um tipo não dimensionado é um ponteiro
gordo (fat), assim como a referência correspondente ou tipo Box
seriam. Um ponteiro *const [u8] inclui um comprimento com o
endereço e um objeto trait como um ponteiro *mut dyn std::io::Write
carrega uma vtable.
Embora o Rust desreferencie implicitamente tipos de ponteiro
seguros em várias situações, as desreferências de ponteiros brutos
devem ser explícitas:
• O operador . não desreferenciará implicitamente um ponteiro
bruto; você deve escrever (*raw).field ou (*raw).method(...).
• Ponteiros brutos não implementam Deref, assim coerções deref
não se aplicam a eles.
• Operadores como == e < comparam ponteiros brutos como
endereços: dois ponteiros brutos são iguais se apontarem para a
mesma posição na memória. Da mesma forma, usar hash em um
ponteiro bruto propaga o endereço para o qual ele aponta, não o
valor de seu referente.
• Traits de formatação como std::fmt::Display seguem as referências
automaticamente, mas não manipulam ponteiros brutos. As
exceções são std::fmt::Debug e std::fmt::Pointer, que mostram ponteiros
brutos como endereços hexadecimais, sem desreferenciá-los.
Ao contrário do operador + em C e C++, o + do Rust não lida com
ponteiros brutos, mas você pode executar aritmética de ponteiro por
meio dos métodos offset e wrapping_offset, ou os métodos add, sub,
wrapping_add e wrapping_sub mais convenientes. Inversamente, o método
offset_from fornece a distância entre dois ponteiros em bytes, embora
sejamos responsáveis por garantir que o início e o fim estejam na
mesma região na memória (o mesmo Vec, por exemplo):
let trucks = vec!["garbage truck", "dump truck", "moonstruck"];
let first: *const &str = &trucks[0];
let last: *const &str = &trucks[2];
assert_eq!(unsafe { last.offset_from(first) }, 2);
assert_eq!(unsafe { first.offset_from(last) }, -2);
Nenhuma conversão explícita é necessária para first e last; apenas
especificar o tipo é suficiente. O Rust implicitamente converte
referências em ponteiros brutos (mas não o contrário, é claro).
O operador as permite quase todas as conversões plausíveis de
referências a ponteiros brutos ou entre dois tipos de ponteiro bruto.
Contudo, pode ser necessário dividir uma conversão complexa em
uma série de etapas mais simples. Por exemplo:
&vec![42_u8] as *const String; // erro: conversão inválida
&vec![42_u8] as *const Vec<u8> as *const String; // permitido
Observe que as não converterá ponteiros brutos em referências.
Essas conversões seriam inseguras, e as deve permanecer uma
operação segura. Em vez disso, você deve desreferenciar o ponteiro
bruto (em um bloco unsafe) e, em seguida, pegar emprestado o valor
resultante.
Tenha muito cuidado ao fazer isso: uma referência produzida dessa
maneira tem um tempo de vida irrestrito: não há limite de quanto
tempo ela pode viver, já que o ponteiro bruto não dá ao Rust nada
para basear essa decisão. Em “Uma interface segura para libgit2”, na
página 764, mais adiante neste capítulo, mostramos vários exemplos
de como restringir adequadamente os tempos de vida.
Muitos tipos têm métodos as_ptr e as_mut_ptr que retornam um
ponteiro bruto para o conteúdo. Por exemplo, fatias de array e
strings retornam ponteiros para seus primeiros elementos, e alguns
iteradores retornam um ponteiro para o próximo elemento que eles
produzirão. Tipos de ponteiro proprietários como Box, Rc e Arc têm
funções into_raw e from_raw que convertem de e para ponteiros brutos.
Alguns dos contratos desses métodos impõem requisitos
surpreendentes, portanto verifique sua documentação antes de usá-
los.
Você também pode construir ponteiros brutos por conversão de
inteiros, embora os únicos inteiros nos quais você pode confiar para
isso geralmente sejam aqueles obtidos de um ponteiro em primeiro
lugar. “Exemplo: RefWithFlag”, na página 723, utiliza ponteiros
brutos dessa maneira.
Ao contrário de referências, ponteiros brutos não são nem Send nem
Sync. Como resultado, qualquer tipo que inclua ponteiros brutos não
implementa esses traits por padrão. Não há nada inerentemente
inseguro sobre enviar ou compartilhar ponteiros brutos entre
threads; afinal, aonde quer que eles vão, você ainda precisa de um
bloco unsafe para desreferenciá-los. Mas, considerando as funções
que os ponteiros brutos normalmente desempenham, os designers
de linguagem consideraram esse comportamento o padrão mais útil.
Já discutimos como implementar Send e Sync em “Traits inseguros”, na
página 717.
Exemplo: RefWithFlag
Eis um exemplo de como pegar um clássico1 hack no nível de bits
possibilitado por ponteiros brutos e encapsulado como um tipo
completamente seguro do Rust. Esse módulo define um tipo,
RefWithFlag<'a, T>, que contém um &'a T e um bool, como a tupla (&'a T,
bool), e ainda assim consegue ocupar apenas uma palavra de
máquina em vez de duas. Esse tipo de técnica é utilizado
regularmente em coletores de lixo e máquinas virtuais, em que
certos tipos – digamos, o tipo que representa um objeto – são tão
numerosos que adicionar até mesmo uma única palavra a cada valor
aumentaria drasticamente o uso da memória:
mod ref_with_flag {
use std::marker::PhantomData;
use std::mem::align_of;
Aritmética de ponteiro
O Rust apresenta os elementos de um array, fatia ou vetor como um
único bloco contíguo de memória, como mostrado na Figura 22.1.
Os elementos são regularmente espaçados, de modo que, se cada
elemento ocupa size bytes, então o i-ésimo elemento começa com o
i * size-ésimo byte.
Figura 22.2: Movendo uma string de uma variável local para outra.
Após a atribuição, pot não é inicializado e plate é o proprietário da
string.
No nível de máquina, não está especificado o que um movimento faz
com a fonte, mas na prática geralmente não faz nada. A atribuição
provavelmente ainda permite que pot contenha um ponteiro,
capacidade e comprimento para a string. Naturalmente, seria
desastroso tratar isso como um valor ativo, e o Rust garante que
você não faça isso.
As mesmas considerações se aplicam a estruturas de dados que
gerenciam sua própria memória. Suponha que você execute este
código:
let mut noodles = vec!["udon".to_string()];
let soba = "soba".to_string();
let last;
Na memória, o estado se parece com a Figura 22.3.
ptr.copy_to(dst, count)
Uma versão mais conveniente de copy, que move count valores do
array na memória, começando em ptr a dst, em vez de utilizar o
ponto inicial como um argumento.
std::ptr::copy_nonoverlapping(src, dst, count)
Como a chamada correspondente para copy, exceto que o contrato
ainda exige que os blocos de memória de origem e destino não se
sobreponham. Isso pode ser um pouco mais rápido do que chamar
copy.
ptr.copy_to_nonoverlapping(dst, count)
Uma versão mais conveniente de copy_nonoverlapping, como copy_to.
Existem outras duas famílias de funções read e write, também no
módulo std::ptr:
read_unaligned, write_unaligned
Essas funções são como read e write, exceto que o ponteiro não
precisa estar alinhado como normalmente exigido para o tipo de
referência. Essas funções podem ser mais lentas do que as funções
read e write simples.
read_volatile, write_volatile
Essas funções são equivalentes a leituras e gravações voláteis em C
ou C++.
Exemplo: GapBufferGenericName
Eis um exemplo que coloca em uso as funções de ponteiro bruto que
acabamos de descrever.
Suponha que você esteja escrevendo um editor de texto e esteja
procurando um tipo para representar o texto. Poderia escolher String
e utilizar os métodos insert e remove para inserir e excluir caracteres à
medida que o usuário digita. Mas, se eles estiverem editando texto
no início de um arquivo grande, esses métodos podem ser caros:
inserir um novo caractere envolve deslocar todo o restante da string
para a direita na memória e a exclusão desloca tudo de volta para a
esquerda. Você quer que essas operações comuns sejam mais
baratas.
O editor de texto Emacs utiliza uma estrutura de dados simples
chamada gap buffer que pode inserir e excluir caracteres em tempo
constante. Considerando que um String mantém toda a sua
capacidade sobressalente no final do texto, o que torna push e pop
baratos, um gap buffer mantém sua capacidade sobressalente no
meio do texto, no ponto em que a edição está ocorrendo. Essa
capacidade sobressalente chama-se lacuna. Inserir ou excluir
elementos em uma lacuna é fácil: você simplesmente reduz ou
aumenta a lacuna conforme necessário. Pode mover a lacuna para
qualquer local que desejar, deslocando o texto de um lado da lacuna
para o outro. Quando a lacuna está vazia, você migra para um buffer
maior.
Embora a inserção e exclusão em um gap buffer sejam rápidas,
alterar a posição em que elas ocorrem envolve mover a lacuna para
a nova posição. Deslocar os elementos requer tempo proporcional à
distância que está sendo movida. Felizmente, a atividade típica de
edição envolve fazer várias alterações em uma vizinhança do buffer
antes de sair e mexer no texto em outro lugar.
Nesta seção, implementaremos um gap buffer no Rust. Para evitar
distração pelo UTF-8, criaremos valores char para nosso
armazenamento em buffer diretamente, mas os princípios da
operação seriam os mesmos se armazenássemos o texto de alguma
outra forma.
Primeiro, mostraremos um gap buffer em ação. Esse código cria um
GapBuffer, insere algum texto nele e, em seguida, move o ponto de
inserção para que permaneça um pouco antes da última palavra:
let mut buf = GapBuffer::new();
buf.insert_iter("Lord of the Rings".chars());
buf.set_position(12);
Depois de executar esse código, o buffer se parece com a
Figura 22.6.
...
}
Ele limpa muitas das funções a seguir para que haja um método
utilitário que retorne um ponteiro bruto para o elemento do buffer
em um determinado índice. Isso sendo Rust, acabamos precisando
de um método para ponteiros mut e um para const. Ao contrário dos
métodos anteriores, esses não são públicos. Continuando esse
bloco impl:
/// Retorna um ponteiro para o `index`-esímo elemento do armazenamento
/// subjacente, independentemente da lacuna
///
/// Segurança: `index` deve ser um índice válido dentro de `self.storage`
unsafe fn space(&self, index: usize) -> *const T {
self.storage.as_ptr().offset(index as isize)
}
unsafe {
let gap = self.gap.clone();
if pos > gap.start {
// `pos` entra após a lacuna. Move a lacuna para a direita
// deslocando os elementos após a lacuna para antes dele
let distance = pos - gap.start;
std::ptr::copy(self.space(gap.end),
self.space_mut(gap.start),
distance);
} else if pos < gap.start {
// `pos` entra antes da lacuna. Move a lacuna para a esquerda
// deslocando elementos antes da lacuna para depois dela
let distance = gap.start - pos;
std::ptr::copy(self.space(pos),
self.space_mut(gap.end - distance),
distance);
}
self.gap = pos .. pos + gap.len();
}
}
Essa função utiliza o método std::ptr::copy para deslocar os elementos;
copy requer que o destino seja não inicializado e deixa a origem não
inicializada. Os intervalos de origem e destino podem se sobrepor,
mas copy lida com esse caso corretamente. Como a lacuna é
memória não inicializada antes da chamada e a função ajusta a
posição da lacuna para abranger o espaço vago pela cópia, o
contrato das funções copy é atendido.
A inserção e remoção de elementos são relativamente simples. A
inserção ocupa um espaço da lacuna para o novo elemento,
enquanto a remoção move um valor para fora e aumenta a lacuna
para abranger o espaço que costumava ocupar:
/// Insere `elt` na posição de inserção atual,
/// e deixa a posição de inserção depois dele
pub fn insert(&mut self, elt: T) {
if self.gap.len() == 0 {
self.enlarge_gap();
}
unsafe {
let index = self.gap.start;
std::ptr::write(self.space_mut(index), elt);
}
self.gap.start += 1;
}
// Não fazemos ideia do que redimensionar um Vec faz com sua capacidade
// "não utilizada". Assim, basta criar um novo vetor e mover os elementos
let mut new = Vec::with_capacity(new_capacity);
let after_gap = self.capacity() - self.gap.end;
let new_gap = self.gap.start .. new.capacity() - after_gap;
unsafe {
// Move os elementos que entram antes da lacuna
std::ptr::copy_nonoverlapping(self.space(0),
new.as_mut_ptr(),
self.gap.start);
assert_eq!(sign(-1), true);
assert_eq!(sign(1), false);
assert_eq!(sign(i64::MAX), false);
assert_eq!(sign(i64::MIN), true);
Aqui, o bit de sinal é o bit mais significativo do byte mais
significativo. Como os processadores x86 são little-endian, a ordem
desses bytes é invertida; o byte mais significativo não é bytes[0], mas
bytes[7]. Normalmente, isso não é algo com o qual o código Rust
tenha de lidar, mas como esse código está trabalhando diretamente
com a representação na memória do i64, esses detalhes de baixo
nível tornam-se importantes.
Como uniões não sabem como dropar o conteúdo, todos os seus
campos devem ser Copy. Entretanto, se você simplesmente precisa
armazenar um String em uma união, há uma solução alternativa;
consulte na documentação da biblioteca padrão std::mem::ManuallyDrop.
Correspondendo uniões
A correspondência em uma união Rust é como a correspondência
em um struct, exceto que cada padrão precisa especificar
exatamente um campo:
unsafe {
match u {
SmallOrLarge { s: true } => { println!("boolean true"); }
SmallOrLarge { l: 2 } => { println!("integer 2"); }
_ => { println!("something else"); }
}
}
Um braço match que corresponde a uma variante de união sem
especificar um valor sempre será bem-sucedido. O código a seguir
causará um comportamento indefinido se o último campo escrito
de u era u.i:
// Comportamento indefinido!
unsafe {
match u {
FloatOrInt { f } => { println!("float {}", f) },
// aviso: padrão inacessível
FloatOrInt { i } => { println!("int {}", i) }
}
}
#[repr(C)]
pub struct git_error {
pub message: *const c_char,
pub klass: c_int
}
O atributo #[repr(C)] só afeta o layout do próprio struct, não as
representações dos campos individuais; portanto, para corresponder
ao struct C, cada campo também deve utilizar o tipo semelhante
a C: *const c_char para char *, c_int para int e assim por diante.
Nesse caso específico, o atributo #[repr(C)] provavelmente não altera
o layout de git_error. Na verdade, não há muitas maneiras
interessantes de dispor um ponteiro e um inteiro. Mas, embora o C e
C++ garantam que os membros de uma estrutura apareçam na
memória na ordem em que são declarados, cada um em um
endereço distinto, o Rust reordena os campos para minimizar o
tamanho geral do struct e os tipos de tamanho zero não ocupam
espaço. O atributo #[repr(C)] diz ao Rust que siga as regras do C para
o tipo fornecido.
Você também pode utilizar #[repr(C)] para controlar a representação
de enums no estilo C:
#[repr(C)]
#[allow(non_camel_case_types)]
enum git_error_code {
GIT_OK = 0,
GIT_ERROR = -1,
GIT_ENOTFOUND = -3,
GIT_EEXISTS = -4,
...
}
Normalmente, o Rust joga todos os tipos de jogos ao escolher como
representar enums. Por exemplo, mencionamos o truque que o Rust
utiliza para armazenar Option<&T> em uma única palavra (se T estiver
dimensionado). Sem #[repr(C)], o Rust utilizaria um único byte para
representar o enum git_error_code; com #[repr(C)], o Rust utiliza um
valor do tamanho de um int do C, assim como o C usaria.
Você também pode solicitar ao Rust que dê a um enum a mesma
representação de algum tipo inteiro. Iniciar a definição anterior com
#[repr(i16)] forneceria um tipo de 16 bits com a mesma representação
que o enum C++ a seguir:
#include <stdint.h>
#[repr(C)]
struct Value {
tag: Tag,
union: FloatOrInt
}
fn is_zero(v: Value) -> bool {
use self::Tag::*;
unsafe {
match v {
Value { tag: Int, union: FloatOrInt { i: 0 } } => true,
Value { tag: Float, union: FloatOrInt { f: num } } => (num == 0.0),
_ => false
}
}}
Mesmo estruturas complexas podem ser facilmente utilizadas dentro
dos limites do FFI usando essa técnica.
Passar strings entre o Rust e o C é um pouco mais difícil. O C
representa uma string como um ponteiro para um array de
caracteres, terminado com um caractere nulo. O Rust, por outro
lado, armazena o comprimento de uma string explicitamente, seja
como um campo de um String ou como a segunda palavra de uma
referência gorda &str. Strings Rust não são terminadas em nulo; na
verdade, elas podem incluir caracteres nulos no conteúdo, como
qualquer outro caractere.
Isso significa que você não pode pegar emprestada uma string Rust
como uma string C: se você passar um ponteiro de código C para
uma string Rust, ele poderá confundir um caractere nulo incorporado
com o final da string ou sair do final procurando por uma terminação
nula que não está lá. Por outro lado, você pode conseguir pegar
emprestada uma string C como um &str Rust, desde que o conteúdo
seja UTF-8 bem formado.
Essa situação efetivamente força Rust a tratar strings C como tipos
totalmente distintos de String e &str. No módulo std::ffi, os tipos CString e
CStr representam arrays proprietários e emprestados terminados com
um byte nulo. Comparado com String e str, os métodos em CString e CStr
são bastante limitados, restritos à construção e conversão em outros
tipos. Mostraremos esses tipos em ação na próxima seção.
É
É uma convenção do Cargo que um crate que fornece acesso a uma
biblioteca C deve ser nomeado LIB-sys, em que LIB é o nome da
biblioteca C. Um crate -sys não deve conter nada além da biblioteca
vinculada estaticamente e os módulos Rust contendo blocos extern e
definições de tipo. As interfaces de nível superior pertencem então
aos crates que dependem do crate -sys. Isso permite que vários
crates upstream dependam do mesmo crate -sys, supondo que haja
uma única versão do crate -sys que atende às necessidades de todos.
Para obter todos os detalhes sobre o suporte do Cargo para criar
scripts e vincular com bibliotecas do sistema, consulte a
documentação on-line do Cargo (https://oreil.ly/Rxa1D). Ela mostra
como evitar caminhos absolutos em scripts de construção, controlar
flags de compilação, utilizar ferramentas como pkg-config e assim
por diante. O crate git2-rs também fornece bons exemplos para
emular; o script de construção (build) lida com algumas situações
complexas.
impl Repository {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Repository> {
ensure_initialized();
let path = path_to_cstring(path.as_ref())?;
let mut repo = ptr::null_mut();
unsafe {
check(raw::git_repository_open(&mut repo, path.as_ptr()))?;
}
Ok(Repository { raw: repo })
}
}
Como a única maneira de fazer qualquer coisa com a interface
segura é começar com um valor Repository e Repository::open inicia com
uma chamada para ensure_initialized, podemos ter certeza de que
ensure_initialized será chamada antes de quaisquer funções da libgit2.
Sua definição é como a seguir:
fn ensure_initialized() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
unsafe {
check(raw::git_libgit2_init())
.expect("initializing libgit2 failed");
assert_eq!(libc::atexit(shutdown), 0);
}
});
}
extern fn shutdown() {
unsafe {
if let Err(e) = check(raw::git_libgit2_shutdown()) {
eprintln!("shutting down libgit2 failed: {}", e);
std::process::abort();
}
}
}
O tipo std::sync::Once ajuda a executar o código de inicialização de
maneira thread-safe. Somente o primeiro thread a chamar
ONCE.call_once executa a closure específica. Quaisquer chamadas
subsequentes, por esse thread ou qualquer outro, são bloqueadas
até que a primeira seja concluída e, em seguida, retornam
imediatamente, sem executar a closure novamente. Depois que a
closure concluiu, chamar ONCE.call_once é barato, exigindo nada mais
do que uma leitura atômica de uma flag armazenada em ONCE.
No código anterior, a closure de inicialização chama git_libgit2_init e
verifica o resultado. Ele aposta um pouco e utiliza expect para garantir
que a inicialização seja bem-sucedida, em vez de tentar propagar os
erros de volta ao chamador.
Para garantir que o programa chama git_libgit2_shutdown, a closure de
inicialização utiliza a função atexit da biblioteca C, que leva um
ponteiro para uma função a ser invocada antes que o processo seja
encerrado. Closures Rust não podem servir como ponteiros de
função C: uma closure é um valor de algum tipo anônimo que
transporta os valores de quaisquer variáveis que ela capture ou
referencie; um ponteiro de função C é simplesmente um ponteiro.
Contudo, os tipos fn do Rust funcionam bem, desde que você os
declare extern para que o Rust saiba utilizar as convenções de
chamada do C. A função local shutdown é adequada e garante que
libgit2 seja desligada adequadamente.
Em “Desempilhamento”, na página 189, mencionamos que é um
comportamento indefinido que gera um pânico cruzar os limites da
linguagem. A chamada de atexit para shutdown é esse limite, por isso é
essencial que shutdown não entre em pânico. Essa é a razão pela qual
shutdown simplesmente não pode utilizar .expect para lidar com erros
relatados de raw::git_libgit2_shutdown. Em vez disso, ele deve relatar o
erro e encerrar o processo. O POSIX proíbe chamar exit dentro de um
manipulador atexit, então shutdown chama std::process::abort para encerrar
o programa abruptamente.
Pode ser possível chamar git_libgit2_shutdown mais cedo – digamos,
quando o último valor Repository é dropado. Mas, independentemente
de como organizamos as coisas, chamar git_libgit2_shutdown deve ser
responsabilidade da API segura. No momento em que é chamada, o
uso de quaisquer objetos libgit2 existentes se torna inseguro, assim a
API segura não deve expor essa função diretamente.
Um ponteiro bruto de Repository sempre deve apontar para um objeto
git_repository ativo. Isso implica que a única maneira de fechar um
repositório é dropar o valor Repository que o possui:
impl Drop for Repository {
fn drop(&mut self) {
unsafe {
raw::git_repository_free(self.raw);
}
}
}
Ao chamar git_repository_free somente quando o único ponteiro para o
está prestes a ser encerrado, o tipo Repository também
raw::git_repository
garante que o ponteiro nunca será utilizado após ser liberado.
O método Repository::open utiliza uma função privada chamada
path_to_cstring, que tem duas definições – uma para sistemas do tipo
Unix e outra para Windows:
use std::ffi::CString;
#[cfg(unix)]
fn path_to_cstring(path: &Path) -> Result<CString> {
// O método `as_bytes` só existe em sistemas do tipo Unix
use std::os::unix::ffi::OsStrExt;
Ok(CString::new(path.as_os_str().as_bytes())?)
}
#[cfg(windows)]
fn path_to_cstring(path: &Path) -> Result<CString> {
// Tenta converter em UTF-8. Se isso falhar, libgit2 não pode lidar
// com o caminho de nenhuma maneira
match path.to_str() {
Some(s) => Ok(CString::new(s)?),
None => {
let message = format!("Couldn't convert path '{}' to UTF-8",
path.display());
Err(message.into())
}
}
}
A interface libgit2 torna esse código um pouco complicado. Em todas
as plataformas, libgit2 aceita caminhos como strings C terminadas em
nulo. No Windows, libgit2 supõe que essas strings C contêm UTF-8
bem formado, e as converte internamente nos caminhos de 16 bits
que o Windows requer. Isso geralmente funciona, mas não é o ideal.
O Windows permite nomes de arquivo que não são Unicode bem
formados e, portanto, não podem ser representados em UTF-8. Se
você tiver tal arquivo, é impossível passar seu nome para libgit2.
No Rust, a representação adequada de um caminho de sistema de
arquivos é um std::path::Path, cuidadosamente projetado para lidar com
qualquer caminho que possa aparecer no Windows ou POSIX. Isso
significa que existem valores Path no Windows que não podem ser
passados para libgit2, porque eles não são UTF-8 bem formados.
Assim, embora o comportamento de path_to_cstring esteja abaixo do
ideal, na verdade é o melhor que podemos fazer dada a interface de
libgit2.
As duas definições path_to_cstring que acabamos de mostrar dependem
de conversões no nosso tipo Error: o operador ? tenta essas
conversões, e a versão do Windows chama explicitamente .into().
Essas conversões não são dignas de nota:
impl From<String> for Error {
fn from(message: String) -> Error {
Error { code: -1, message, class: 0 }
}
}
// NulError é o que `CString::new` retorna se uma string tem zero bytes embutidos
impl From<std::ffi::NulError> for Error {
fn from(e: std::ffi::NulError) -> Error {
Error { code: -1, message: e.to_string(), class: 0 }
}
}
Em seguida, vamos descobrir como resolver uma referência do Git a
um identificador de objeto. Como um identificador de objeto é
apenas um valor hash de 20 bytes, é perfeitamente possível expô-lo
na API segura:
/// O identificador de algum tipo de objeto armazenado no banco de dados
/// de objetos Git: um commit, árvore, blob, tag etc. Isso é um amplo hash
/// do conteúdo do objeto
pub struct Oid {
pub raw: raw::git_oid
}
Vamos adicionar um método a Repository para realizar a consulta:
use std::mem;
use std::os::raw::c_char;
impl Repository {
pub fn reference_name_to_id(&self, name: &str) -> Result<Oid> {
let name = CString::new(name)?;
unsafe {
let oid = {
let mut oid = mem::MaybeUninit::uninit();
check(raw::git_reference_name_to_id(
oid.as_mut_ptr(), self.raw,
name.as_ptr() as *const c_char))?;
oid.assume_init()
};
Ok(Oid { raw: oid })
}
}
}
Embora oid seja mantido não inicializado quando a pesquisa falha,
essa função garante que o chamador nunca possa ver o valor não
inicializado simplesmente seguindo a linguagem Result do Rust: ou o
chamador recebe um Ok que transporta um valor Oid adequadamente
inicializado, ou recebe um Err.
Em seguida, o módulo precisa de uma maneira de recuperar os
commits do repositório. Vamos definir um tipo Commit da seguinte
forma:
use std::marker::PhantomData;
pub struct Commit<'repo> {
// Isso sempre deve ser um ponteiro para uma estrutura `git_commit` utilizável
raw: *mut raw::git_commit,
_marker: PhantomData<&'repo Repository>
}
Como mencionado anteriormente, um objeto git_commit nunca deve
sobreviver ao objeto git_repository do qual foi recuperado. Os tempos
de vida do Rust permitem que o código capture essa regra com
precisão.
O exemplo RefWithFlag anterior neste capítulo usou um campo
PhantomData para dizer ao Rust que trate um tipo como se contivesse
uma referência com um determinado tempo de vida, embora o tipo
aparentemente não contivesse tal referência. O tipo Commit precisa
fazer algo semelhante. Nesse caso, o tipo _marker do campo é
PhantomData<&'repo Repository>, indicando que o Rust deve tratar
Commit<'repo> como se contivesse uma referência com tempo de vida
'repo a algum Repository.
O método para consultar um commit é o seguinte:
impl Repository {
pub fn find_commit(&self, oid: &Oid) -> Result<Commit> {
let mut commit = ptr::null_mut();
unsafe {
check(raw::git_commit_lookup(&mut commit, self.raw, &oid.raw))?;
}
Ok(Commit { raw: commit, _marker: PhantomData })
}
}
Como isso relaciona o tempo de vida de Commit ao tempo de vida de
Repository? A assinatura de find_commit omite os tempos de vida das
referências envolvidas de acordo com as regras descritas em
“Omitindo parâmetros de tempo de vida”, na página 151. Se
fôssemos especificar os tempos de vida, a assinatura completa seria:
fn find_commit<'repo, 'id>(&'repo self, oid: &'id Oid)
-> Result<Commit<'repo>>
É exatamente isso que queremos: O Rust trata o Commit retornado
como se pegasse algo emprestado de self, que é o Repository.
Quando um Commit é dropado, ele deve liberar raw::git_commit:
impl<'repo> Drop for Commit<'repo> {
fn drop(&mut self) {
unsafe {
raw::git_commit_free(self.raw);
}
}
}
A partir de um Commit, você pode pegar emprestado Signature (um
nome e endereço de e-mail) e o texto da mensagem de
confirmação:
impl<'repo> Commit<'repo> {
pub fn author(&self) -> Signature {
unsafe {
Signature {
raw: raw::git_commit_author(self.raw),
_marker: PhantomData
}
}
}
pub fn message(&self) -> Option<&str> {
unsafe {
let message = raw::git_commit_message(self.raw);
char_ptr_to_str(self, message)
}
}
}
Eis o tipo Signature:
pub struct Signature<'text> {
raw: *const raw::git_signature,
_marker: PhantomData<&'text str>
}
Um objeto git_signature sempre pega emprestado o texto de outro
lugar; em particular, assinaturas retornadas por git_commit_author
pegam emprestado o texto de git_commit. Assim, nosso tipo Signature
seguro inclui um PhantomData<&'text str> para dizer ao Rust que se
comporte como se contivesse um &str com um tempo de vida de 'text.
Assim como antes, Commit::author conecta adequadamente esse tempo
de vida 'text do Signature que ele retorna ao do Commit sem que seja
necessário escrever nada. O método Commit::message faz o mesmo com
o Option<&str> que contém a mensagem de confirmação.
Um Signature inclui métodos para recuperar o nome e o endereço de
e-mail do autor:
impl<'text> Signature<'text> {
/// Retorna o nome do autor como `&str`,
/// ou `None` se não é UTF-8 bem formado.
pub fn name(&self) -> Option<&str> {
unsafe {
char_ptr_to_str(self, (*self.raw).name)
}
}
Conclusão
O Rust não é uma linguagem simples. O objetivo dele é abranger
dois mundos muito diferentes. Ele é uma linguagem de programação
moderna, segura por design, com conveniências como closures e
iteradores, mas visa colocá-lo no controle das capacidades brutas da
máquina em que é executado, com sobrecarga mínima em tempo de
execução.
Os contornos da linguagem são determinados por esses objetivos. O
Rust consegue preencher a maior parte da lacuna com código
seguro. Seu verificador de empréstimos (borrow checker) e
abstrações de custo zero o colocam o mais próximo possível do
metal (nível da máquina), sem arriscar um comportamento
indefinido. Quando isso não é suficiente ou quando você deseja
aproveitar o código C existente, o código inseguro e a interface de
função externa estão disponíveis. Mas, novamente, a linguagem não
oferece apenas esses recursos inseguros e deseja boa sorte. O
objetivo é sempre utilizar recursos não seguros para criar APIs
seguras. Foi o que fizemos com libgit2. É também o que a equipe do
Rust fez com Box, Vec, as outras coleções, canais e muito mais: a
biblioteca padrão está cheia de abstrações seguras, implementadas
com algum código inseguro nos bastidores.
Uma linguagem com as ambições do Rust talvez não fosse destinada
a ser a mais simples das ferramentas. Mas o Rust é seguro, rápido,
concorrente – e eficaz. Use-o para construir sistemas grandes,
rápidos, seguros e robustos que aproveitam todo o poder do
hardware em que são executados. Use-o para melhorar o software.
Sobre os autores