RUST - A Linguagem de Programação
RUST - A Linguagem de Programação
RUST - A Linguagem de Programação
Esta versão do texto assume que você está utilizando Rust 1.37.0 ou superior com
edition="2018" no arquivo Cargo.toml de todos os projetos que utilizarem recursos de
edição 2018 de Rust. Veja a [seção "Instalação" do capítulo 1][install] para instalar ou
atualizar Rust, e veja a o novo [apêndice E][editions] para informações sobre as edições.
A edição 2018 da linguagem Rust inclui várias melhorias que fazem Rust mais ergonômica e
fácil de aprender. Esta iteração do livro contém várias mudanças que refletem essas
melhorias:
Note que qualquer código em iterações anteriores de A Linguagem de Programação Rust que
compilavam irão continuar a compilar sem edition="2018" no Cargo.toml do projeto,
mesmo que você atualize o compilador Rust que você utiliza. Estas são as garantias de
compatibilidade retroativa de Rust trabalhando!
Este texto está disponível nos [formatos brochura e ebook pela No Starch Press][nsprust].
Prefácio
Não foi sempre tão claro, mas a linguagem de programação Rust é
fundamentalmente
sobre empoderamento: não importa que tipo de código você
está escrevendo agora, Rust te
empodera a ir além, a programar com
confiança em uma variedade maior de domínios do
que você fazia antes.
Considere, por exemplo, um trabalho a nível de sistema que lide com detalhes de
baixo
nível de gerenciamento de memória, representação de dados, e
concorrência.
Tradicionalmente, esse domínio da programação é visto como
arcano, acessível somente a
uns poucos escolhidos que devotaram a os anos
necessários para aprender a evitar seus
armadilhas infâmes. E mesmo aqueles
que o praticam o fazem com cautela, em caso seu
código esteja aberto a
exploits, quebras, ou corrupção de memória.
Programadores que já estejam trabalhando com código de baixo nível podem usar
Rust
para aumentar suas ambições. Por exemplo, introduzir paralelismo em Rust
é uma
operação relativamente de baixo risco: o compilador irá pegar os erros
clássicos para você.
E você poderá atacar otimizações mais agressivas no seu
código com a confiança de que
você não irá introduzir acidentalmente quebras
ou exploits de segurança.
Mas Rust não é apenas limitada a programação de baixo nível de sistemas. Ela
é expressiva
e ergonômica o suficiente para fazer aplicações de linha de
comando (CLIs), servidores web,
e muitos outros tipos de código bastante
prazerosos de escrever — você irá encontrar
exemplos simples de ambos mais
tarde no livro. Trabalhar com Rust te permite adquirir
habilidades que são
transferíveis de um domínio a outro. Você pode aprender Rust ao
escrever um
aplicativo web, e então aplicar as mesmas habilidades para endereçar seu
Raspberry Pi.
Introdução
Bem-vindo ao “A Linguagem de Programação Rust”, um livro introdutório sobre Rust.
Rust é uma linguagem de programação que ajuda a escrever software mais rápido e
confiável. A ergonomia de alto nível e o controle de baixo nível estão frequentemente em
desacordo no design da linguagem de programação; Rust desafia isso. Ao equilibrar uma
poderosa capacidade técnica e uma ótima experiência de desenvolvedor, Rust oferece a
opção de controlar detalhes de baixo nível (como o uso de memória) sem todo o incômodo
tradicionalmente associado a esse controle.
Times de Desenvolvedores
Rust está provando ser uma ferramenta produtiva para colaborar entre grandes equipes de
desenvolvedores com níveis variados de conhecimento de programação de sistemas. O
código de baixo nível é propenso a uma variedade de erros sutis, que na maioria das outras
linguagens só podem ser detectados por meio de testes extensivos e revisão cuidadosa do
código por desenvolvedores experientes. Em Rust, o compilador desempenha um papel de
guardião, recusando-se a compilar código com esses tipos de erros - incluindo erros de
concorrência. Ao trabalhar junto com o compilador, a equipe pode dedicar mais tempo à
lógica do programa, em vez de procurar bugs.
Estudantes
Empresas
Rust é usado em produção por centenas de empresas, grandes e pequenas, para uma
variedade de tarefas, como ferramentas de linha de comando, serviços na Web,
ferramentas DevOps, dispositivos embarcados, análise e transcodificação de áudio e vídeo,
criptomoedas, bioinformática, motores de busca, internet das coisas, aprendizado de
máquina e até partes importantes do navegador Firefox.
Rust é para pessoas que desejam criar a linguagem de programação Rust, a comunidade, as
ferramentas de desenvolvedor e as bibliotecas Rust. Gostaríamos que você contribuísse
para a linguagem Rust.
Por velocidade, entendemos a velocidade dos programas que Rust permite criar e a
velocidade com que Rust permite que você os escreva. As verificações do compilador Rust
garantem estabilidade por meio de adições e refatoração de recursos, em oposição ao
código legado frágil (quebrável) em linguagens sem essas verificações, que os
desenvolvedores têm medo de modificar. Ao buscar abstrações de custo zero, recursos de
nível superior que se compilam para código de baixo nível, tão rápido quanto o código
escrito manualmente, Rust se esforça para tornar o código seguro bem como um código
rápido.
Esta não é uma lista completa de tudo que a linguagem Rust espera apoiar, mas esses são
alguns dos maiores interessados. No geral, a maior ambição de Rust é aceitar trocas aceitas
pelos programadores há décadas e eliminar a dicotomia. Segurança e produtividade.
Velocidade e ergonomia. Experimente Rust e veja se as opções funcionam para você.
Existem dois tipos de capítulos neste livro: capítulos conceituais e capítulos de projetos. Nos
capítulos conceituais, você aprenderá sobre um aspecto de Rust. Nos capítulos de projeto,
criaremos pequenos programas juntos, aplicando o que aprendemos até agora. Os
capítulos 2, 12 e 20 são capítulos de projetos; o resto são capítulos conceituais.
Além disso, o Capítulo 2 é uma introdução prática ao Rust como linguagem. Abordaremos
conceitos de alto nível e os capítulos posteriores serão detalhados. Se você é o tipo de
pessoa que gosta de sujar as mãos imediatamente, o Capítulo 2 é ótimo para isso. Se você é
realmente esse tipo de pessoa, pode até pular o Capítulo 3, que abrange recursos muito
semelhantes a outras linguagens de programação, e vá direto ao Capítulo 4 para aprender
sobre o sistema de ownership (propriedade) Rust. Por outro lado, se você é particularmente
aluno meticuloso que prefere aprender todos os detalhes antes de passar para o próximo,
pule o Capítulo 2 e vá direto para o Capítulo 3.
Finalmente, existem alguns apêndices. Eles contêm informações úteis sobre a linguagem
em um formato mais parecido como uma referência.
No final, não há uma maneira errada de ler o livro: se você quiser pular, vá em frente! Você
pode ter que voltar atrás se achar as coisas confusas. Faça o que funciona para você.
Ferris Significado
Começando
Vamos começar sua jornada Rust! Neste capítulo, discutiremos:
Instalação
O primeiro passo é instalar Rust. Vamos fazer o download de Rust através do rustup , uma
ferramenta de linha de comando para gerenciar versões Rust e ferramentas associadas.
Você precisará de uma conexão com a Internet para o download.
Nota: Se você preferir não usar o rustup por algum motivo, consulte a página de
instalação de Rust para outras opções.
As etapas a seguir instalam a versão estável mais recente do compilador Rust. As garantias
de estabilidade de Rust garantem que todos os exemplos do livro que compilam continuem
sendo compilados com as versões mais recentes de Rust. A saída pode diferir ligeiramente
entre as versões, porque Rust geralmente melhora as mensagens de erro e os avisos. Em
outras palavras, qualquer versão mais recente e estável de Rust instalada usando essas
etapas deve funcionar conforme o esperado com o conteúdo deste livro.
Se você estiver usando Linux ou macOS, abra um terminal e digite o seguinte comando:
O comando baixa um script e inicia a instalação da ferramenta rustup , que instala a versão
estável mais recente de Rust. Você pode ser solicitado a fornecer sua senha. Se a instalação
for bem-sucedida, a seguinte linha aparecerá:
$ source $HOME/.cargo/env
$ export PATH="$HOME/.cargo/bin:$PATH"
Além disso, você precisará de um linker de algum tipo. Provavelmente já estáinstalado, mas
quando você tenta compilar um programa Rust e obtem erros, indicando que um linker não
pôde executar, isso significa que um linker não está instalado no seu sistema e você
precisará instalá-lo manualmente. Os compiladores C geralmente vêm com o linker correto.
Verifique a documentação da sua plataforma para saber como instalar um compilador C.
Além disso, alguns pacotes Rust comuns dependem do código C e precisarão de um
compilador C. Portanto, pode valer a pena instalar um agora.
O restante deste livro usa comandos que funcionam no cmd.exe e no PowerShell. Se houver
diferenças específicas, explicaremos qual usar.
Atualização e Desinstalação
Depois de instalar o Rust via rustup , é fácil atualizar para a versão mais recente. No seu
shell, execute o seguinte script de atualização:
$ rustup update
Para desinstalar o Rust e o rustup , execute o seguinte script de desinstalação do seu shell:
Solução de Problemas
Para verificar se você possui Rust instalado corretamente, abra um shell e digite esta linha:
$ rustc --version
Você deverá ver o número da versão, commit hash, e commit da data da versão estável mais
recente lançada no seguinte formato:
Se você visualizar essas informações, instalou Rust com sucesso! Se você não vir essas
informações e estiver no Windows, verifique se Rust está na sua variável de sistema
%PATH% . Se tudo estiver correto e Rust ainda não estiver funcionando, há vários lugares
onde você pode obter ajuda. O mais fácil é o canal #beginners em the official Rust Discord.
Lá, você pode conversar com outros Rustáceos (um apelido bobo que chamamos a nós
mesmos) que podem ajudá-lo. Outros ótimos recursos incluem o canal no Telegram Rust
Brasil, além do the Users forum e Stack Overflow.
Documentação Local
O instalador também inclui uma cópia da documentação localmente, para que você possa
lê-la offline. Execute rustup doc para abrir a documentação local no seu navegador.
Sempre que um tipo ou função for fornecida pela biblioteca padrão e você não tiver certeza
do que esta faz ou como usá-la, use a documentação da interface de programação de
aplicativos (API) para descobrir!
Olá, mundo!
Agora que você instalou Rust, vamos escrever seu primeiro programa Rust. Quando se
aprende uma nova linguagem, é tradicional escrever um pequeno programa que imprime o
texto Hello, world! na tela, para que façamos o mesmo aqui!
Nota: Este livro pressupõe familiaridade básica com a linha de comando. Rust não
requer exigências específicas sobre a sua edição, ferramentas ou a localização do seu
código; portanto, se você preferir usar um ambiente de desenvolvimento integrado
(IDE) em vez da linha de comando, fique à vontade para usar o seu IDE favorito. Muitos
IDEs agora têm algum grau de apoio ao Rust; consulte a documentação do IDE para
obter detalhes. Recentemente, a equipe do Rust tem se concentrado em permitir um
ótimo suporte a IDE, e houve progresso rápido nessa frente!
Você começará criando um diretório para armazenar seu código Rust. Não importa para
Rust onde seu código mora, mas para os exercícios e projetos deste livro, sugerimos criar
um diretório projects no diretório inicial e manter todos os seus projetos lá.
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
> cd /d "%USERPROFILE%\projects"
> cd hello_world
Em seguida, crie um novo arquivo source e chame-o de main.rs. Arquivos Rust sempre
terminam com a extensão .rs . Se você estiver usando mais de uma palavra no seu nome de
arquivo, use um sublinhado para separá-las. Por exemplo, use hello_world.rs em vez de
helloworld.rs.
Agora abra o arquivo main.rs que você acabou de criar e insira o código na Listagem 1-1.
fn main() {
println!("Hello, world!");
Salve o arquivo e volte para a janela do seu terminal. No Linux ou macOS, digite os
seguintes comandos para compilar e executar o arquivo:
$ rustc main.rs
$ ./main
Hello, world!
> .\main.exe
Hello, world!
Independentemente do seu sistema operacional, a string Hello, world! deve ser impressa
no terminal. Se você não vir essa saída, consulte a parte “Solução de Problemas” da seção
instalação para obter maneiras de obter ajuda.
Se Hello, world! foi impresso, parabéns! Você escreveu oficialmente um programa Rust.
Isso faz de você um programador Rust — bem-vindo!
Vamos analisar em detalhes o que aconteceu no seu programa Hello, world! Aqui está a
primeira peça do quebra-cabeça:
fn main() {
Essas linhas definem uma função em Rust. A função main é especial: é sempre o primeiro
código executado em todos os programas Rust executáveis. A primeira linha declara uma
função chamada main que não possui parâmetros e não retorna nada. Se houvesse
parâmetros, eles entrariam entre parênteses, () .
Observe também que o corpo da função está entre colchetes, {} . Rust exige isso em todos
os corpos funcionais. É um bom estilo colocar o colchete de abertura na mesma linha da
declaração de função, adicionando um espaço no meio.
println!("Hello, world!");
Esta linha faz todo o trabalho neste pequeno programa: imprime texto na tela. Há quatro
detalhes importantes a serem observados aqui. Primeiro, o estilo Rust é recuar com quatro
espaços, não uma tabulação.
Segundo, println! chama uma macro Rust. Se fosse chamada uma função, ela seria
inserida como println (sem o ! ). Discutiremos Rust macros com mais detalhes no
Capítulo 19. Por enquanto, você só precisa saber que usar um ! significa que você está
chamando uma macro em vez de uma função normal.
Terceiro, você vê a string "Hello, world!" . Passamos essa string como argumento para
println! , e a string é impressa na tela.
Quarto, terminamos a linha com um ponto-e-vírgula ( ; ), que indica que essa expressão
acabou e a próxima está pronta para começar. A maioria das linhas do código Rust termina
com um ponto e vírgula.
Você acabou de executar um programa recém-criado, portanto, vamos examinar cada etapa
do processo.
Antes de executar um programa Rust, você deve compilá-lo usando o compilador Rust
digitando o comando rustc e passando o nome do seu arquivo source, assim:
$ rustc main.rs
Se você tem experiência em C ou C ++, notará que isso é semelhante a gcc ou clang . Após
compilar com sucesso, Rust gera um executável binário.
$ ls
main main.rs
> dir /B %= the /B option says to only show the file names =%
main.exe
main.pdb
main.rs
Isso mostra o arquivo de código-fonte com a extensão .rs, o arquivo executável (main.exe no
Windows, mas main em todas as outras plataformas) e, ao usar o Windows, um arquivo
contendo informações de depuração com o extensão .pdb. A partir daqui, você executa o
arquivo main ou main.exe, assim:
Se main.rs era seu programa Hello, world!, esta linha imprimirá Hello, world! no seu
terminal.
Se você está mais familiarizado com uma linguagem dinâmica, como Ruby, Python ou
JavaScript, pode não estar acostumado a compilar e executar um programa como etapas
separadas. Rust é uma linguagem compilada antecipadamente, o que significa que você pode
compilar um programa e fornecer o executável para outra pessoa, e eles podem executá-lo
mesmo sem Rust instalado. Se você fornecer a alguém um arquivo .rb, .py ou .js, eles
deverão ter uma implementação Ruby, Python ou JavaScript instalada (respectivamente).
Mas essas linguagens, você só precisa de um comando para compilar e executar seu
programa. Tudo é uma troca no design da linguagem.
Apenas compilar com rustc é bom para programas simples, mas à medida que o seu
projeto cresce, você deseja gerenciar todas as opções e facilitar o compartilhamento do seu
código. Em seguida, apresentaremos a ferramenta Cargo, que ajudará você a criar
programas Rust no mundo real.
Olá, Cargo!
Cargo é o gestor de sistemas e pacotes da linguagem Rust. A maioria dos Rustáceos usa essa
ferramenta para gerenciar seus projetos Rust porque o Cargo cuida de muitas tarefas para
você, como criar seu código, fazer o download das bibliotecas das quais seu código
depende e criar essas bibliotecas. (Chamamos de bibliotecas que seu código precisa de
dependências.)
Os programas Rust mais simples, como o que escrevemos até agora, não tem
dependências; portanto, se tivéssemos construído o projeto Hello World com o Cargo, ele
usaria apenas a parte do Cargo que cuida da criação do seu código. Ao escrever programas
Rust mais complexos, você deseja adicionar dependências e, se você iniciar o projeto
usando Cargo, isso será muito mais fácil.
Como a grande maioria dos projetos Rust usa Cargo, o restante deste livro pressupõe que
você também esteja usando Cargo. Cargo vem instalado com o próprio Rust, se você usou
os instaladores oficiais, conforme descrito na seção “Instalação”. Se você instalou Rust por
outros meios, poderá verificar se possui o Cargo instalado inserindo o seguinte em seu
terminal:
$ cargo --version
Se você vir um número de versão, ótimo! Se você vir um erro como command not found ,
consulte a documentação do seu método de instalação para determinar como instalar o
Cargo separadamente.
Vamos criar um novo projeto usando Cargo e ver como ele difere do nosso projeto original
Hello World. Navegue de volta para o diretório projects (ou onde quer que você tenha
decidido colocar seu código) e, em seguida, em qualquer sistema operacional:
$ cd hello_cargo
Vá para o diretório hello_cargo e liste os arquivos, e você verá que Cargo gerou dois arquivos
e um diretório para nós: um diretório Cargo.toml e src com um arquivo main.rs dentro.
Também inicializou um novo repositório git, junto com um arquivo .gitignore.
Nota: Git é um sistema de controle de versão comum. Você pode alterar cargo new
para usar um sistema de controle de versão diferente, ou nenhum sistema de controle
de versão, usando o sinalizador --vcs . Execute cargo new --help para ver as opções
disponíveis.
Abra Cargo.toml no seu editor de texto de sua escolha. Deve ser semelhante ao código na
Listagem 1-2:
[package]
name = "hello_cargo"
version = "0.1.0"
[dependencies]
Este arquivo está no formato TOML (Tom Óbvia, Linguagem Mínima), que é o que o Cargo
usa como formato de configuração.
As próximas três linhas definem as informações de configuração que Cargo precisa para
saber que ele deve compilar seu programa: o nome, a versão e quem o escreveu. Cargo
obtém seu nome e informações de e-mail do seu ambiente; portanto, se isso não estiver
correto, prossiga, corrija-o e salve o arquivo.
A última linha, [dependencies] , é o início de uma seção para você listar qualquer uma das
dependências do seu projeto. Em Rust, pacotes de código são referidos como crates. Não
precisaremos de outras crates para este projeto, mas precisaremos no primeiro projeto do
capítulo 2, portanto, usaremos essa seção de dependências.
fn main() {
println!("Hello, world!");
Cargo gerou um “Hello World!” para você, exatamente como o que escrevemos na Lista 1-1!
Até agora, as diferenças entre o projeto anterior e o projeto gerado pelo Cargo são que,
com Cargo, nosso código entra no diretório src e temos um arquivo de configuração
Cargo.toml no diretório superior.
Cargo espera que seus arquivos source morem dentro do diretório src, para que o diretório
de projeto de nível superior seja apenas para READMEs, informações de licença, arquivos de
configuração e qualquer outra coisa não relacionada ao seu código. Dessa forma, o uso do
Cargo ajuda a manter seus projetos organizados. Há um lugar para tudo, e tudo está em
seu lugar.
Se você iniciou um projeto que não usa Cargo, como fizemos com nosso projeto no
diretório hello_world, você pode convertê-lo em um projeto que usa Cargo movendo o
código do projeto para o diretório src e criando um apropriado Cargo.toml.
Agora, vamos ver o que há de diferente na criação e execução do seu programa Hello World
através do Cargo! No diretório do projeto, construa seu projeto digitando os seguintes
comandos:
$ cargo build
Hello, world!
Bam! Se tudo correr bem, Hello, world! deve ser impresso no terminal mais uma vez. A
execução do cargo build pela primeira vez também faz com que o Cargo crie um novo
arquivo no nível superior chamado Cargo.lock, que é usado para acompanhar as versões
exatas das dependências do seu projeto. Este projeto não tem dependências, portanto o
arquivo é um pouco esparso. Você nunca precisará tocar nesse arquivo; Cargo gerenciará
seu conteúdo para você.
$ cargo run
Running `target/debug/hello_cargo`
Hello, world!
Observe que, desta vez, não vimos a saída nos dizendo que Cargo estava compilando
hello_cargo . Cargo descobriu que os arquivos não foram alterados; portanto, apenas
executou o binário. Se você tivesse modificado seu código-fonte, Cargo reconstruiria o
projeto antes de executá-lo e você teria visto resultados como este:
$ cargo run
Running `target/debug/hello_cargo`
Hello, world!
Finalmente, há cargo check . Este comando verificará rapidamente seu código para
garantir que ele seja compilado, mas não se incomode em produzir um executável:
$ cargo check
Por que você não gostaria de um executável? O cargo check geralmente é muito mais
rápido que o cargo build , porque pula toda a etapa de produção do executável. Se você
estiver verificando seu trabalho durante todo o processo de escrever o código, o uso de
cargo check acelerará as coisas! Como tal, muitos Rustaceans executam cargo check
periodicamente enquanto escrevem seu programa para garantir que ele seja compilado e,
em seguida, executam cargo build quando estiverem prontos para rodar.
Quando seu projeto estiver finalmente pronto para o lançamento, você poderá usar o
cargo build --release para compilar seu projeto com otimizações. Isso criará um
executável em target/release em vez de target/debug. Essas otimizações tornam seu código
Rust mais rápido, mas ativá-los leva mais tempo para compilar o programa. É por isso que
existem dois perfis diferentes: um para desenvolvimento, quando você deseja reconstruir
de forma rápida e frequente, e outro para a criação do programa final, que você fornecerá a
um usuário que não será reconstruído repetidamente e que será executado como o mais
rápido possível. Se você estiver comparando o tempo de execução do seu código, lembre-se
de executar cargo build --release e faça a comparação com o executável em
target/release.
Em projetos simples, Cargo não fornece muito valor ao usar apenas rustc , mas provará
seu valor à medida que você continua. Com projetos complexos compostos por várias
crates, é muito mais fácil deixar Cargo coordenar a construção.
Embora o projeto hello_cargo seja simples, agora ele usa grande parte das ferramentas
reais que você usará para o resto de sua carreira em Rust. De fato, para trabalhar em
qualquer projeto existente, você pode usar os seguintes comandos para verificar o código
usando o Git, mudar para o diretório do projeto e criar:
$ cd someproject
$ cargo build
Resumo
Você já começou bem a sua jornada Rust! Neste capítulo, você:
Este é um ótimo momento para criar um programa mais substancial, para se acostumar a
ler e escrever código em Rust. No capítulo 2, criaremos um programa de jogos de
adivinhação. Se você preferir começar a aprender sobre como os conceitos comuns de
programação funcionam em Rust, consulte o Capítulo 3 e, sem seguida retorne ao capítulo
2.
Jogo de Adivinhação
Vamos entrar de cabeça no Rust e colocar a mão na massa! Este capítulo vai lhe
apresentar
alguns conceitos bem comuns no Rust, mostrando como usá-los em um
programa de
verdade. Você vai aprender sobre let , match , métodos, funções
associadas, crates
externos, e mais! Os capítulos seguintes vão explorar essas
ideias em mais detalhes. Neste
capítulo, você vai praticar o básico.
$ cd jogo_de_advinhacao
Arquivo: Cargo.toml
[package]
name = "jogo_de_advinhacao"
version = "0.1.0"
[dependencies]
Assim como no Capítulo 1, cargo new gera um programa "Hello, world!" para nós.
Confira
em src/main.rs:
Arquivo: src/main.rs
fn main() {
println!("Hello, world!");
Agora vamos compilar esse programa "Hello, world!" e executá-lo de uma vez só
usando o
comando cargo run :
$ cargo run
Running `target/debug/jogo_de_advinhacao`
Hello, world!
Arquivo: src/main.rs
use std::io;
fn main() {
println!("Advinhe o número!");
io::stdin().read_line(&mut palpite)
Esse código tem muita informação, vamos ver uma parte de cada vez. Para obter a
entrada
do usuário, e então imprimir o resultado como saída, precisaremos trazer
ao escopo a
biblioteca io (de entrada/saída). A biblioteca io provém da
biblioteca padrão (chamada de
std ):
use std::io;
Por padrão, o Rust traz apenas alguns tipos para o escopo de todos os programas
no
prelúdio. Se um tipo que você quiser usar não
estiver no prelúdio, você terá que importá-lo
explicitamente através do use .
A biblioteca std::io oferece várias ferramentas de
entrada/saída, incluindo a
funcionalidade de ler dados de entrada do usuário.
fn main() {
Como você também já aprendeu no Capítulo 1, println! é uma macro que imprime
uma
string na tela:
println!("Advinhe o número!");
Este código está exibindo uma mensagem que diz de que se trata o jogo e solicita
uma
entrada do usuário.
Agora o programa está ficando interessante! Tem muita coisa acontecendo nesta
pequena
linha. Repare que esta é uma declaração let , que é usada para criar
variáveis. Segue outro
exemplo:
Essa linha cria uma nova variável chamada foo , e a vincula ao valor bar . Em
Rust, variáveis
são imutáveis por padrão. O exemplo a seguir mostra como usar
mut antes do nome da
variável para torná-la mutável:
Agora você sabe que let mut palpite vai introduzir uma variável mutável de
nome
palpite . No outro lado do símbolo = está o valor ao qual palpite está
vinculado, que é o
resultado da chamada String::new , uma função que retorna
uma nova instância de
String . String é um tipo
fornecido pela biblioteca padrão que representa uma cadeia
expansível de
caracteres codificados em UTF-8.
Esta função new() cria uma nova String vazia. Você encontrará uma função
new() em
muitos tipos, já que é um nome comum para uma função que produz um
novo valor de
algum tipo.
Para resumir, a linha let mut palpite = String::new(); criou uma variável
mutável que
está atualmente vinculada a uma nova instância vazia de uma
String . Ufa!
io::stdin().read_line(&mut palpite)
O símbolo & indica que o argumento é uma referência, o que permite múltiplas
partes do
seu código acessar um certo dado sem precisar criar várias cópias dele
na memória.
Referências são uma característica complexa, e uma das maiores
vantagens do Rust é o
quão fácil e seguro é usar referências. Você não precisa
conhecer muitos desses detalhes
para finalizar esse programa. O Capítulo 4 vai
explicar sobre referências de forma mais
aprofundada. Por enquanto, tudo que
você precisa saber é que, assim como as variáveis,
referências são imutáveis por
padrão. Por isso, precisamos escrever &mut palpite , em vez
de apenas
&palpite , para fazer com que o palpite seja mutável.
Ainda não finalizamos completamente esta linha de código. Embora esta seja uma
única
linha de texto, é apenas a primeira parte de uma linha lógica de código. A
segunda parte é a
chamada para este método:
Quando você chama um método com a sintaxe .foo() , geralmente é bom introduzir
uma
nova linha e outro espaço para ajudar a dividir linhas muito compridas.
Poderíamos ter feito
assim:
Porém, uma linha muito comprida fica difícil de ler. Então é melhor dividirmos a
linha em
duas, uma para cada método chamado. Agora vamos falar sobre o que essa
linha faz.
Se não chamarmos expect , nosso programa vai compilar, mas vamos ter um aviso:
$ cargo build
--> src/main.rs:10:5
10 | io::stdin().read_line(&mut palpite);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Rust avisa que não usamos o valor Result , retornado por read_line , indicando
que o
programa deixou de tratar um possível erro. A maneira correta de suprimir
o aviso é
realmente escrevendo um tratador de erro, mas como queremos que o
programa seja
encerrado caso ocorra um problema, podemos usar expect . Você
aprenderá sobre
recuperação de erros no Capítulo 9.
Exibindo Valores com Curingas do println!
Tirando a chave que delimita a função main , há apenas uma linha mais a ser
discutida no
código que fizemos até agora, que é a seguinte:
Esta linha imprime a string na qual salvamos os dados inseridos pelo usuário. O
{} é um
curinga que reserva o lugar de um valor. Você pode imprimir mais de um
valor usando {} :
o primeiro conjunto de {} guarda o primeiro valor listado
após a string de formatação, o
segundo conjunto guarda o segundo valor, e
assim por diante. Imprimir múltiplos valores
em uma só chamada a println!
seria assim:
let x = 5;
let y = 10;
Vamos testar a primeira parte do jogo de advinhação. Você pode executá-lo usando
cargo
run :
$ cargo run
Running `target/debug/jogo_de_advinhacao`
Advinhe o número!
Você disse: 6
Nesse ponto, a primeira parte do jogo está feita: podemos coletar entrada do
teclado e
mostrá-la na tela.
É no uso de crates externos que Cargo realmente brilha. Antes que possamos
escrever o
código usando rand , precisamos modificar o arquivo Cargo.toml
para incluir o crate rand
como uma dependência. Abra o arquivo e adicione
esta linha no final, abaixo do cabeçalho
da seção [dependencies] que o Cargo
criou para você:
Arquivo: Cargo.toml
[dependencies]
rand = "0.3.14"
Agora, sem mudar código algum, vamos compilar nosso projeto, conforme mostrado
na
Listagem 2-2:
$ cargo build
Talvez pra você apareçam versões diferentes (mas elas são todas compatíveis com
o código,
graças ao Versionamento Semântico!), e as linhas talvez apareçam em
ordem diferente.
Agora que temos uma dependência externa, Cargo busca as versões mais recentes de
tudo
no registro, que é uma cópia dos dados do Crates.io.
Crates.io é onde as pessoas do
ecossistema Rust postam seus projetos
open source para que os outros possam usar.
Se, logo em seguida, você executar cargo build novamente sem fazer mudanças,
não vai
aparecer nenhuma mensagem de saída. O Cargo sabe que já baixou e
compilou as
dependências, e você não alterou mais nada sobre elas no seu arquivo
Cargo.toml. Cargo
também sabe que você não mudou mais nada no seu código, e
por isso não o recompila.
Sem nada a fazer, ele simplesmente sai. Se você abrir
src/main.rs, fizer uma modificação
trivial, salvar e compilar de novo, vai
aparecer uma mensagem de apenas duas linhas:
$ cargo build
Essas linhas mostram que o Cargo só atualiza o build com a sua pequena mudança
no
arquivo src/main.rs. Suas dependências não mudaram, então o Cargo sabe que
pode
reutilizar o que já tiver sido baixado e compilado para elas. Ele apenas
recompila a sua
parte do código.
O Cargo tem um mecanismo que assegura que você pode reconstruir o mesmo artefato
toda vez que você ou outra pessoa compilar o seu código. O Cargo vai usar apenas
as
versões das dependências que você especificou, até que você indique o
contrário. Por
exemplo, o que acontece se, na semana que vem, sair a versão
v0.3.15 contendo uma
correção de bug, mas também uma regressão que não
funciona com o seu código?
A resposta para isso está no arquivo Cargo.lock, que foi criado na primeira
vez que você
executou cargo build , e agora está no seu diretório
jogo_de_advinhacao. Quando você
compila o seu projeto pela primeira vez, o
Cargo descobre as versões de todas as
dependências que preenchem os critérios
e então as escreve no arquivo Cargo.lock. Quando
você compilar o seu projeto
futuramente, o Cargo verá que o arquivo Cargo.lock existe e
usará as versões
especificadas lá, em vez de refazer todo o trabalho descobrir as versões
novamente. Isto lhe permite ter um build reproduzível automaticamente. Em
outras
palavras, seu projeto vai continuar com a versão 0.3.14 até que você
faça uma atualização
explícita, graças ao arquivo Cargo.lock.
Quando você quiser atualizar um crate, o Cargo tem outro comando, update ,
que faz o
seguinte:
$ cargo update
Nesse ponto, você vai notar também uma mudança no seu arquivo Cargo.lock
dizendo que a
versão do crate rand que você está usando agora é a 0.3.15 .
[dependencies]
rand = "0.4.0"
Na próxima vez que você executar cargo build , o Cargo vai atualizar o registro
de crates
disponíveis e reavaliar os seus requisitos sobre o rand de acordo
com a nova versão que
você especificou.
Arquivo: src/main.rs
extern crate rand;
use std::io;
use rand::Rng;
fn main() {
println!("Advinhe o número!");
io::stdin().read_line(&mut palpite)
Estamos adicionando a linha extern crate rand ao topo do arquivo para indicar
ao Rust
que estamos usando uma dependência externa. Isto também é equivalente a
um use
rand; , assim podemos chamar qualquer coisa que esteja no crate rand
prefixando-a com
rand:: .
Tem outras duas linhas que adicionamos no meio. A função rand::thread_rng nos
dá o
gerador de números aleatórios que vamos usar, um que é local à thread
corrente e que é
inicializado pelo sistema operacional. Depois, vamos chamar o
método gen_range no
gerador de números aleatórios. Esse método está definido
pelo trait Rng que trouxemos ao
escopo por meio do use rand::Rng . Este
método recebe dois argumentos e gera um
número aleatório entre eles. Ele inclui
o limite inferior mas exclui o superior, então
precisamos passar 1 e 101
para obter um número de 1 a 100.
Saber quais traits devem ser usadas e quais funções e métodos de um crate
devem ser
chamados não é nada trivial. As instruções de como usar um crate
estão na documentação
de cada um. Outra coisa boa do Cargo é que você pode rodar
o comando cargo doc --
open que vai construir localmente a documentação
fornecida por todas as suas
dependências e abrí-las no seu navegador. Se você
estiver interessado em outras
funcionalidades do crate rand , por exemplo,
execute cargo doc --open e clique em rand ,
no menu ao lado esquerdo.
A segunda linha que adicionamos imprime o número secreto. Isto é útil enquanto
estamos
desenvolvendo o programa para podermos testá-lo, mas vamos retirá-la da
versão final.
Um jogo não é muito interessante se ele mostra a resposta logo no
início!
$ cargo run
Running `target/debug/jogo_de_advinhacao`
Advinhe o número!
O número secreto é: 7
Você disse: 4
$ cargo run
Running `target/debug/jogo_de_advinhacao`
Advinhe o número!
O número secreto é: 83
Você disse: 5
Você já deve obter números aleatórios diferentes, e eles devem ser todos entre 1
e 100.
Bom trabalho!
Arquivo: src/main.rs
extern crate rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Advinhe o número!");
io::stdin().read_line(&mut palpite)
match palpite.cmp(&numero_secreto) {
A primeira novidade aqui é outro use , que traz ao escopo um tipo da biblioteca
padrão
chamado std::cmp::Ordering . Ordering é outra enum, igual a Result ,
mas as suas
variantes são Less , Greater e Equal (elas significam menor,
maior e igual,
respectivamente). Estes são os três possíveis resultados quando
você compara dois valores.
Depois, adicionamos cinco novas linhas no final que usam o tipo Ordering :
match palpite.cmp(&numero_secreto) {
O método cmp compara dois valores, e pode ser chamado a partir de qualquer
coisa que
possa ser comparada. Ele recebe uma referência de qualquer coisa que
você queira
comparar. Neste caso, está comparando o palpite com o
numero_secreto . cmp retorna
uma variante do tipo Ordering , que trouxemos
ao escopo com use . Nós usamos uma
expressão match
para decidir o que fazer em seguida, com base em qual variante de
Ordering foi
retornada pelo método cmp , que foi chamado com os valores palpite e
numero_secreto .
Uma expressão match é composta de braços. Um braço consiste em um padrão
mais o
código que deve ser executado se o valor colocado no início do match se
encaixar no
padrão deste braço. O Rust pega o valor passado ao match e o
compara com o padrão de
cada braço na sequência. A expressão match e os
padrões são ferramentas poderosas do
Rust que lhe permitem expressar uma
variedade de situações que seu código pode
encontrar, e ajuda a assegurar que
você tenha tratado todas elas. Essas ferramentas serão
abordadas em detalhes nos
capítulos 6 e 18, respectivamente.
Porém, o código da Listagem 2-4 ainda não vai compilar. Vamos tentar:
$ cargo build
--> src/main.rs:23:21
23 | match palpite.cmp(&numero_secreto) {
O que este erro está dizendo é que temos tipos incompatíveis. Rust tem um
sistema de tipos
forte e estático. Porém, Rust também tem inferência de tipos.
Quando escrevemos let
palpite = String::new() , Rust foi capaz de inferir que
palpite deveria ser uma String ,
então ele não nos faz escrever o tipo. O
numero_secreto , por outro lado, é de um tipo
numérico. Existem alguns tipos
numéricos capazes de guardar um valor entre 1 e 100: i32 ,
que é um número de
32 bits; u32 , um número de 32 bits sem sinal; i64 , um número de 64
bits; e
mais alguns outros. O tipo numérico padrão do Rust é i32 , que é o tipo do
Arquivo: src/main.rs
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Advinhe o número!");
io::stdin().read_line(&mut palpite)
match palpite.cmp(&numero_secreto) {
Nós criamos uma variável chamada palpite . Mas espera, o programa já não tinha
uma
variável chamada palpite ? Sim, mas o Rust nos permite sombrear o
palpite anterior com
um novo. Isto é geralmente usado em situações em que você
quer converter um valor de
um tipo em outro. O sombreamento nos permite
reutilizar o nome palpite , em vez de nos
forçar a criar dois nomes únicos como
palpite_str e palpite , por exemplo. (O Capítulo 3
vai cobrir sombreamento em
mais detalhes).
$ cargo run
Running `target/jogo_de_advinhacao`
Advinhe o número!
O número secreto é: 58
76
Você disse: 76
Muito alto!
Boa! Até mesmo colocando alguns espaços antes de digitar o palpite, o programa
ainda
descobriu que o palpite do usuário é 76. Execute o programa mais algumas
vezes para
verificar os diferentes comportamentos com diferentes tipos de
entrada: advinhe o número
corretamente, digite um número muito alto, e digite um
número muito baixo.
Agora já temos a maior parte do jogo funcionando, mas o usuário só consegue dar
um
palpite uma vez. Vamos mudar isso adicionando laços!
Arquivo: src/main.rs
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Advinhe o número!");
loop {
io::stdin().read_line(&mut palpite)
match palpite.cmp(&numero_secreto) {
Como você pode ver, movemos tudo para dentro do laço a partir da mensagem
pedindo o
palpite do usuário. Certifique-se de indentar essas linhas mais quatro
espaços cada uma, e
execute o programa novamente. Repare que há um novo
problema, porque o programa
está fazendo exatamente o que dissemos para ele
fazer: pedir sempre outro palpite! Parece
que o usuário não consegue sair!
O usuário pode sempre interromper o programa usando as teclas
ctrl-c. Mas há uma outra
forma de escapar deste
monstro insaciável que mencionamos na discussão do método
parse , na seção
"Comparando o Palpite com o Número Secreto": se o usuário fornece uma
resposta
não-numérica, o programa vai sofrer um crash. O usuário pode levar vantagem
disso para conseguir sair, como mostrado abaixo:
$ cargo run
Running `target/jogo_de_advinhacao`
Advinhe o número!
O número secreto é: 59
45
Você disse: 45
Muito baixo!
60
Você disse: 60
Muito alto!
59
Você disse: 59
Você acertou!
sair
Digitar sair , na verdade, sai do jogo, mas isso também acontece com qualquer
outra
entrada não numérica. Porém, isto não é o ideal. Queremos que o jogo
termine
automaticamente quando o número é advinhado corretamente.
Vamos programar o jogo para sair quando o usuário vencer, colocando um break :
Arquivo: src/main.rs
extern crate rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Advinhe o número!");
loop {
io::stdin().read_line(&mut palpite)
match palpite.cmp(&numero_secreto) {
Ordering::Equal => {
println!("Você acertou!");
break;
Adicionando a linha break após o Você acertou! , o programa vai sair do laço
quando o
usuário advinhar corretamente o número secreto. Sair do laço também
significa sair do
programa, pois o laço é a última parte da main .
};
Trocando uma chamada a expect por uma expressão match é a forma como você
geralmente deixa de causar um crash em um erro e passa a tratá-lo, de fato.
Lembre-se que
o método parse retorna um valor do tipo Result , uma enum que
contém a variante Ok
ou Err . Estamos usando um match aqui, assim como
fizemos com o Ordering resultante
do método cmp .
Agora, tudo no programa deve funcionar como esperado. Vamos tentar executá-lo
usando
o comando cargo run :
$ cargo run
Running `target/jogo_de_advinhacao`
Advinhe o número!
O número secreto é: 61
10
Você disse: 10
Muito baixo!
99
Você disse: 99
Muito alto!
foo
61
Você disse: 61
Você acertou!
Arquivo: src/main.rs
extern crate rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Advinhe o número!");
loop {
io::stdin().read_line(&mut palpite)
};
match palpite.cmp(&numero_secreto) {
Ordering::Equal => {
println!("Você acertou!");
break;
Resumo
Neste ponto, você construiu com sucesso o jogo de adivinhação! Parabéns!
Este projeto foi uma forma prática de apresentar vários conceitos novos de Rust:
let ,
match , métodos, funções associadas, uso de crates externos, e outros.
Nos próximos
capítulos, você vai aprender sobre esses conceitos em mais
detalhes. O Capítulo 3 aborda
conceitos que a maioria das linguagens de
programação tem, como variáveis, tipos de
dados e funções, e mostra como usá-los
em Rust. O Capítulo 4 explora posse (ownership),
que é a característica do
Rust mais diferente das outras linguagens. O Capítulo 5 discute
structs e a
sintaxe de métodos, e o Capítulo 6 se dedica a explicar enums.
Conceitos Comuns de Programação
Este capítulo aborda conceitos que aparecem em quase todas as linguagens de
programação e como eles funcionam no Rust. Muitas linguagens de programação têm
muito em comum em seu cerne. Nenhum dos conceitos apresentados neste capítulo é
exclusivo de Rust, mas vamos discuti-los no contexto do Rust e explicar as convenções em
torno do uso desses conceitos.
Palavras chaves
A linguagem Rust tem uma série de palavras-chaves que são reservadas para uso
exclusivo
da linguagem, como ocorre em outras linguagens. Tenha em mente que você
não
pode usar essas palavras como nome de variáveis ou funções. A maioria das
palavras-chaves tem
um significado específico, e você estará usando-as para várias
tarefas em programas em Rust;
algumas ainda não possuem funcionalidades
associadas a elas, mas
foram reservadas para funcionalidades que podem ser
adicionadas ao Rust futuramente. Você
encontrará uma lista de palavras-chaves no
Apêndice A.
Variáveis e Mutabilidade
Como mencionado no Capítulo 2, por padrão, as variáveis são imutáveis. Essa é uma das
maneiras que o Rust lhe dá para escrever o seu código de
modo seguro e a fácil
concorrência que Rust oferece. No entanto, você ainda tem
a opção de tornar a sua variável
mutável. Vamos explorar como e por que Rust
incentiva você a usar variáveis imutáveis e
por que às vezes pode
não optar por utilizá-las.
Quando uma variável é imutável, logo que um valor é associado a uma variável, você não
pode mudar este valor.
Para ilustrar isso, vamos criar um projeto chamado variaveis
no seu
diretório projetos usando cargo new --bin variables .
let x = 5;
x = 6;
Salve e execute o programa usando cargo run . Você deve receber uma mensagem de erro,
conforme mostrado nesta saída:
--> src/main.rs:4:5
2 | let x = 5;
4 | x = 6;
Esse exemplo mostra como o compilador ajuda você a encontrar erros no seus programas.
Mesmo que erros de compilação sejam frustrantes, eles apenas significam que seu
programa
não está fazendo de modo seguro o que você espera fazer; eles não siginificam
que você
não é um bom programador! Programadores experientes também recebem erros
de compilação.
Em Rust, o compilador garante que quando você afirma que um valor não pode mudar,
ele
não mude. Isso significa que quando você está lendo e ecrevendo código,
você não tenha de
acompanhar como e onde um valor pode mudar. E assim seu código
fica mais fácil de
entender.
Mas mutabilidade pode ser muito útil. Variáveis são imutáveis por padrão; como
você fez no
Capítulo 2, você pode torná-las mutáveis adicionando mut na frente do
nome da variável.
Além de permitir que este valor mude, mut transmite
a intenção aos futuros leitores do
código, indicando que naquela
parte do código estarão mudando o valor da variável.
let mut x = 5;
x = 6;
$ cargo run
Running `target/debug/variaveis`
O valor de x é: 5
O valor de x é: 6
Ser incapaz de mudar o valor de uma variável, pode ter feito você lembrar de
outro conceito
de programação, que a maioria das outras linguagens possui, chamado: constantes. Como
variáveis imutáveis, constantes são valores que estão vinculados ao nome e não
podem
serem alterados, mas há algumas diferenças entre constantes e
variáveis.
Primeiro, você não pode usar mut com constantes. Constante não são apenas
imutáveis
por padrão, constante são sempre imutáveis.
Constantes podem ser declaradas em qualquer escopo, incluindo o escopo global, o que os
tornam
úteis para valores que várias partes do código precisa conhecer.
A última diferença é que as constantes podem ser definidas apenas para uma expressão
constante,
ou seja, não pode ser o resultado de uma chamada de função ou qualquer outro
valor que só poderia ser
calculado em tempo de execução.
Shadowing
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
Running `target/debug/variaveis`
O valor de x é: 12
Shadowing é diferente do que dizer que uma variável é mut , porque teremos um
erro em
tempo de compilação se, acidentalmente, tentarmos reatribuir essa variável sem
utilizar
let . Usando let , nós podemos realizar algumas transformações,
mas sem ter uma
variável imutável após estas transformações terem
sido concluídas.
espacos = espacos.len();
--> src/main.rs:3:14
3 | espacos = espacos.len();
Agora que exploramos como as variáveis funcionam, vamos ver mais tipos de dados.
Tipos de dados
Todo valor em Rust é um tipo de dado, que informa ao Rust que tipos de
dados estão sendo
especificados para que saiba como trabalhar com esses dados. Vamos olhar para
dois
subconjuntos de tipos de dados: escalar e composto.
Tenha em mente que Rust é uma linguagem de tipagem estática, o que significa
que deve
conhecer os tipos de todas as variáveis em tempo de compilação. O compilador
geralmente
pode inferir que tipo queremos com base no valor e como o usamos. Nos casos
em que são
é possível vários tipos de dados, como quando convertemos uma String em um tipo
numérico
usando parse na seção "Comparando o Adivinha ao Número Secreto" no
Capítulo 2, devemos adicionar uma anotação de tipo, como a seguinte:
Se não adicionarmos uma anotação de tipo, Rust irá mostrar o seguinte erro,
que significa
que o compilador precisa de mais informaçoes para saber qual tipo de dados
queremos
usar:
--> src/main.rs:2:9
| ^^^^^
| |
Tipos escalares
Um tipo escalar representa um valor único. Rust tem quatro tipos escalares primários:
inteiros, números de ponto flutuante, booleanos e caracteres. Você pode reconhecer
esses
tipos de outras linguagens de programação. Vamos pular para como eles funcionam no
Rust.
Tipos inteiros
Cada variante pode ser com ou sem sinal e ter tamanho explícito.
Signed e unsigned refere-
se à possibilidade do número ser
negativo ou positivo - em outras palavras, se o número
precisa de um sinal
com ele (signed) ou se sempre for
positivo pode ser representado sem
um sinal (unsigned). É como escrevemos números no papel: Quando
o sinal importa, o
número é mostrado com um sinal de mais ou menos; contudo,
quando é seguro assumir
que o número é positivo, é mostrado sem sinal.
Números com sinais são armazenados
usando a representação complemento de dois (se você não tiver
certeza do que é isso, você
pode procurar sobre isso na internet; uma explicação está fora do escopo
deste livro).
Além disso, os tipos isize e usize dependem do computador em que seu programa
está
rodando: 64 bits se estiver em uma arquitetura de 64-bit e 32 bits
se sua arquitetura for 32-
bit.
Você pode criar inteiros literais em qualquer uma das formas mostrada na Tabela 3-2.
Observe
que todos os literais de números, exceto o byte literal, permitem um sufixo de tipo,
como por exemplo, 57u8 e _ são separadores visuais, tal como 1_000 .
Hexadecimal 0xff
Octal 0o77
Binário 0b1111_0000
Então como você pode saber qual tipo de inteiro usar? Se sentir-se inseguro, as
escolhas
padrões do Rust geralmente são boas, e por padrão os inteiros são do tipo i32 : Esse
tipo
geralmente é o mais rápido, até em sistemas de 64-bit. A
principal situação em que você
usuaria isize ou usize é indexar algum tipo de coleção.
Tipos de ponto flutuante
Rust também tem dois tipos primitivos para números de ponto flutuante, que são
números
com casas decimais. Os pontos flutuantes do Rust são
f32 e f64 , que têm
respectivamente os tamanhos de 32 e 64 bits. O tipo padrão é f64
porque nos
processadores modernos, a velocidade é quase a mesma que em um f32 , mas possui
maior precisão.
fn main() {
Números em ponto flutuante são representados de acordo com o padrão IEEE-754. O tipo
Operações numéricas
fn main() {
// adição
// subtração
// multiplicação
// divisão
// resto
let resto = 43 % 5;
fn main() {
let t = true;
O tipo de caractere
Até agora trabalhamos apenas com números, mas Rust também suporta letras. O char
é o
tipo mais primitivo da linguaguem e o seguinte código
mostra uma forma de utilizá-lo.
(Observe que o char é
específicado com aspas simples, é o oposto de strings, que usa
aspas duplas.)
fn main() {
let c = 'z';
😻';
let z = 'ℤ';
O tipo char representa um valor unicode, o que quer dizer que você pode
armazenar
muito mais que apenas ASCII. Letras com acentuação; ideogramas chinês, japonês e
coreano; emoji; e caracteres não visíveis são válidos.
Valores Unicode vão de U+0000 até
U+D7FF e U+E000 até
U+10FFFF incluso. Contudo, um "caractere" não é realmente um
conceito em Unicode,
então a sua intuição de o que é um "caractere" pode não combinar
com o que é um
char em Rust. Discutiremos esse tópico em detalhes em "Strings" no
Capítulo 8.
Tipos compostos
Tipos compostos podem agrupar vários valores em um único tipo. Rust tem dois
tipos
primitivos compostos: tuplas e vetores.
Criamos uma tupla escrevendo uma lista de valores separados por vírgula
dentro de
parênteses. Cada posição da tupla tem um tipo e os tipos dos elementos
da tupla não
necessitam serem iguais.
Adicionamos anotações de tipo neste exemplo:
fn main() {
fn main() {
Esse primeito programa cria uma tupla e vincula ela à variável tup . Em seguida,
ele usa um
padrão com let para tirar tup e tranformá-lo em três variáveis
separadas, x , y e z . Isso
é chamado de desestruturação, porque quebra uma única tupla
em três partes. Finalmente,
o programa exibe o valor de y ,
que é 6.4 .
fn main() {
let um = x.2;
Esse programa cria uma tupla, x , e então cria uma variável para cada
elemento usando
seus índices. Como ocorre nas maiorias das linguagens, o primeiro
índice em uma tupla é o
0.
O tipo matriz
Uma outra maneira de ter uma coleção de vários valores é uma matriz. Diferentemente
de
uma tupla, todos os elementos de uma matriz devem ser do mesmo tipo.
Matrizes em Rust
são diferentes de matrizes de outras linguagens, porque matrizes em Rust são de
tamanhos
fixos: uma vez declarado, eles não podem aumentar ou diminuir de tamanho.
Em Rust, os valores que entram numa matriz são escritos em uma lista separados
por
vírgulas dentro de colchetes:
fn main() {
Matrizes são úteis quando você deseja que seus dados sejam alocados em pilha do que
no
heap (discutiremos mais sobre pilha e heap no Capítulo 4), ou quando
você quer garantir
que sempre terá um número fixo de elementos. Uma matriz não
é tão flexível como um
vetor. Um vetor é de tipo semelhante,
fornecido pela biblioteca padrão que é permitido
diminuir ou aumentar o tamanho.
Se você não tem certeza se deve usar uma matriz ou
vetor, você provavlemente usará um
vetor. O Capítulo 8 discute sobre vetores com mais
detalhes.
Um exemplo de quando você poderia necessitar usar uma matriz no lugar de um vetor é
um programa em que você precisa saber o nome dos meses do ano. É improvável
que tal
programa deseje adicionar ou remover meses, então você pode usar uma matriz
porque
você sabe que sempre conterá 12 itens:
fn main() {
O que acontece se você tentar acessar um elemento da matriz que está além do fim
da
matriz? Digamos que você mude o exemplo para o código a seguir, que será compilado,
mas existe um erro quando for executar:
fn main() {
$ cargo run
Running `target/debug/arrays`
thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is
10', src/main.rs:6
Funções
Funções são difundidas em códigos em Rust. Você já viu uma das mais
importantes funções
da linguagem: a função main , que é o
ponto de entrada de diversos programas. Você
também já viu a notação fn , que permite você
declarar uma nova função.
Códigos em Rust usam, por convenção, o estilo snake case para nomes de função e
variável.
No snake case, todas as letras são minúsculas e sublinhado (underline) separa as palavras.
Aqui está um programa que contém uma definição de função de exemplo:
fn main() {
println!("Olá, mundo!");
outra_funcao();
fn outra_funcao() {
println!("Outra função.");
Podemos chamar qualqer função que tenhamos definido, inserindo seu nome, seguido de
um
conjunto de parenteses. Pelo fato da outra_funcao ter sido definida no programa, ela
pode
ser chamada dentro da função main . Note que definimos outra_funcao
depois da
função main ; poderíamos ter definido antes
também. Rust não se importa onde você
definiu suas funções, apenas que elas foram
definidas em algum lugar.
Vamos começar um novo projeto binário, chamado funcoes para explorar mais
funções.
Coloque o exemplo outra_funcao em src/main.rs e execute-o. Você
verá a seguinte saída:
$ cargo run
Running `target/debug/funcoes`
Olá, mundo!
Outra função.
Parâmetros de função
Funções também podem ser definidas tendo parâmetros, que são variáveis especiais
que
fazem parte da assinatura da função. Quando uma função tem parâmetros, você
pode
fornecer tipos específicos para esses parâmetros. Tecnicamente, os
valores definidos são
chamados de argumentos, mas informalmente, as pessoas tendem
a usar as palavras
parâmetro e argumento para falar tanto de
variáveis da definição da função como os valores
passados quando você
chama uma função.
A seguinte versão (reescrita) da outra_funcao mostra como os parâmetros
aparecem no
Rust:
fn main() {
outra_funcao(5);
fn outra_funcao(x: i32) {
$ cargo run
Running `target/debug/funcoes`
O valor de x é: 5
Nas assinaturas de função, você deve declarar o tipo de cada parâmetro. Essa é
decisão
deliberada no design do Rust: exigir anotações de tipo na definição da função,
significa que
o compilador quase nunca precisará que as use em outro lugar do código
para especificar o
que você quer.
Quando você precisa que uma função tenha vários parâmetros, separe as declarações de
parâmetros
com vírgula, como a seguir:
fn main() {
outra_funcao(5, 6);
Este exemplo cria uma função com dois parâmetros, ambos com o tipo i32 . Então a função
exibe os valores de ambos os parâmetros. Note que os
parâmetros de função não precisam
ser do mesmo tipo, isto apenas
aconteceu neste exemplo.
Vamos tentar executar este código. Substitua o programa src/main.rs, atualmente em seu
projeto funcoes
com o exemplo anterior e execute-o usando cargo run :
$ cargo run
Running `target/debug/funcoes`
O valor de x é: 5
O valor de y é: 6
Corpos de função
Corpos de função são constituídos por uma série de declarações que terminam,
opcionalmente, em uma expressão. Até agora, foram apresentadas apenas funções sem
uma expressão final,
mas você viu uma expressão como parte de instruções. Porque Rust é
uma
linguagem baseada em expressão, essa é uma importante distinção a ser entendida.
Outras linguagens não têm as mesmas distinções, então, vamos ver o que são
declarações e
expressões e como elas afetam o corpo
das funções.
Declarações e Expressões
Criar uma variável e atribuir um valor a ela com a palavra-chave let é uma declaração.
Na
Listagem 3-1, let y = 6; é uma declaração:
fn main() {
let y = 6;
Definições não retornam valores. Assim sendo, você não pode atribuir uma declaração let
para
outra variável, como o código a seguir tenta fazer; você receberá um erro:
fn main() {
$ cargo run
--> src/main.rs:2:14
| ^^^
A declaração let y = 6 não retorna um valor, então não existe nada para o
x se ligar. Isso
é diferente do que acontece em
outras linguagens, como
C e Ruby, onde a atribuição
retorna o valor atribuído. Nestas linguagens, você pode escrever x = y = 6 e ter ambos, x
e y contendo o valor
6 ; esse não é o caso em Rust.
fn main() {
let x = 5;
let y = {
let x = 3;
x + 1
};
A expressão:
let x = 3;
x + 1
Funções podem retornar valores para o código que os chama. Não nomeamos valores de
retorno, mas declaramos o tipo deles depois de uma seta ( -> ). Em Rust, o valor de retorno
da função é sinônimo do valor da expressão
final no bloco do corpo de uma função. Você
pode retornar cedo de uma função usando
a palavra-chave return e especificando um
valor, mas a maioria das funções retorna
a última expressão implicitamente. Veja um
exemplo de uma função que retorna um
valor:
fn main() {
let x = cinco();
Não há chamadas de função, macros ou até mesmo declarações let na função cinco
$ cargo run
Running `target/debug/funcoes`
O valor de x é: 5
let x = 5;
let x = soma_um(5);
x + 1
fn main() {
let x = soma_um(5);
x + 1;
--> src/main.rs:7:28
| ____________________________^
8 | | x + 1;
9 | | }
A principal mensagem de erro, "tipos incompatíveis", revela o problema central com este
código. A definição da função soma_um diz que retornará uma
i32 , mas as declarações não
avaliam um valor expresso por () ,
a tupla vazia. Portanto, nada é retornado, o que
contradiz a função
definição e resulta em erro. Nesta saída, Rust fornece uma mensagem
para
possivelmente ajudar a corrigir este problema: sugere a remoção do ponto e vírgula,
que
iria corrigir o erro.
Comentários
Todos os programadores se esforçam para tornar seu código fácil de entender, mas às
vezes
explicação extra é garantida. Nestes casos, os programadores deixam notas ou
comentários, em seus códigos fonte que o compilador irá ignorar, mas as pessoas que
lerem
o código-fonte podem achar útil.
// Olá, mundo.
Em Rust, os comentários devem começar com duas barras e continuar até o final da
linha.
Para comentários que se estendem além de uma única linha, você precisará incluir
// em
cada linha, assim:
// Então, estamos fazendo algo complicado aqui, tempo suficiente para que
precisemos
// várias linhas de comentários para fazer isso! Ufa! Espero que este comentário
Comentários também podem ser colocados no final das linhas contendo código:
fn main() {
Mas você verá com mais frequência essas palavras nesse formato, com o comentário em
uma
linha separada acima do código que está anotando:
fn main() {
let numero_da_sorte = 7;
Controle de fluxo
Decidir se deve ou não executar algum código, dependendo se uma condição é verdadeira
e
decidir executar algum código repetidamente enquanto uma condição é verdadeira,
são
blocos de construção básicos na maioria das linguagens de programação. As construções
mais comuns que permitem controlar o fluxo de execução do código Rust são as
expressões if e
laços de repetição.
Expressão if
Uma expressão if permite ramificar seu código dependendo das condições. Você
fornecer
uma condição e, em seguida, estado, "Se esta condição for cumprida, execute este bloco
de
código. Se a condição não for atendida, não execute este bloco de código. ”
Crie um novo projeto chamado branches no seu diretório projects para explorar
a expressão
if . No arquivo * src / main.rs *, digite o seguinte:
fn main() {
let numero = 3;
if numero < 5 {
} else {
$ cargo run
Running `target/debug/branches`
Vamos tentar alterar o valor de numero para um valor que torne a condição
false para ver
o que acontece:
let numero = 7;
$ cargo run
Running `target/debug/branches`
Também é importante notar que a condição neste código deve ser um bool . E se
a
condição não é um bool , nós vamos receber um erro. Por exemplo:
fn main() {
let numero = 3;
if numero {
--> src/main.rs:4:8
4 | if numero {
O erro indica que Rust esperava um bool , mas obteve um inteiro. Ao contrário de
linguagens como Ruby e JavaScript, o Rust não tentará automaticamente
converter tipos
não-booleanos em um booleano. Você deve explicitar e sempre fornecer
if com um
booleano como sua condição. Se quisermos que o bloco de código if seja executado
somente quando um número não é igual a 0 , por exemplo, podemos mudar o if
para o
seguinte:
fn main() {
let numero = 3;
if numero != 0 {
A execução deste código irá imprimir número era algo diferente de zero .
fn main() {
let numero = 6;
if numero % 4 == 0 {
} else if numero % 3 == 0 {
} else if numero % 2 == 0 {
} else {
Este programa tem quatro caminhos possíveis. Depois de executá-lo, você deve
ver a
seguinte saída:
$ cargo run
Running `target/debug/branches`
Quando este programa é executado, ele verifica cada expressão if por sua vez e executa
o
primeiro corpo para o qual a condição é verdadeira. Note que mesmo que 6 seja
divisível
por 2, nós não vemos a saída o número é divisível por 2 , nem vemos o
texto número
não é divisível por 4, 3 ou 2 do bloco else .
Isso ocorre porque o Rust só executa o
bloco para a primeira condição verdadeira e,
depois de encontrar um, não verifica o
restante.
Usar muitas expressões else if pode confundir seu código, portanto, se você tiver
mais
de uma, convém refatorar seu código. O Capítulo 6 descreve uma poderosa
construção de
ramificação em Rust chamada match para esses casos.
Pelo fato de if ser uma expressão, podemos usá-la do lado direito de uma declaração
let ,
como na Listagem 3-2:
} else {
};
$ cargo run
Running `target/debug/branches`
O valor do número é: 5
fn main() {
} else {
"seis"
};
--> src/main.rs:4:18
| __________________^
5 | | 5
6 | | } else {
7 | | "seis"
8 | | };
Geralmente, é útil executar um bloco de código mais de uma vez. Para essa tarefa,
o Rust
fornece vários loops. Um loop percorre o código dentro do corpo do loop
até o final e, em
seguida, inicia imediatamente no início. Para
experimentar loops, vamos criar um novo
projeto chamado loops.
O Rust possui três tipos de loops: loop , while e for . Vamos tentar cada um.
A palavra-chave loop diz ao Rust para executar um bloco de código várias vezes
para
sempre ou até que você diga explicitamente para parar.
fn main() {
loop {
println!("novamente!");
$ cargo run
Running `target/debug/loops`
novamente!
novamente!
novamente!
novamente!
^Cnovamente!
Geralmente, é útil para um programa avaliar uma condição dentro de um loop. Enquanto
a
condição é verdadeira, o loop é executado. Quando a condição deixa de ser verdadeira,
o
programa chama o break , parando o loop. Esse tipo de loop pode ser implementado
usando uma combinação de loop , if , else e break ; você poderia tentar isso
agora em
um programa, se você quiser.
No entanto, esse padrão é tão comum que o Rust possui uma construção de linguagem
integrada
para isso, chamado de loop while . A Listagem 3-3 usa while : o programa faz o
loop
três vezes, a contagem decrescente de cada vez e, depois do ciclo, imprime
outra
mensagem e sai.
while numero != 0 {
println!("{}!", numero);
numero = numero - 1;
println!("LIFTOFF!!!");
Essa construção elimina muito o aninhamento que seria necessário se você usasse
loop ,
if , else e break , e é mais claro. Enquanto a condição for
verdadeira, o código é
executado; caso contrário, sai do loop.
Você poderia usar a construção while para fazer um loop sobre os elementos de uma
coleção,
como uma matriz. Por exemplo, vamos ver a Listagem 3-4:
fn main() {
indice = indice + 1;
Running `target/debug/loops`
O valor é: 10
O valor é: 20
O valor é: 30
O valor é: 40
O valor é: 50
Mas essa abordagem é propensa a erros; poderíamos fazer o programa entrar em pânico
se o
o comprimento do índice estivesse incorreto. Também é lento, porque o compilador
adiciona código de tempo de execução
para executar a verificação condicional em cada
elemento em cada iteração
através do loop.
Como uma alternativa mais concisa, você pode usar um laço for e executar algum código
para cada item de uma coleção. Um laço for parece com este código na Listagem 3-5:
fn main() {
Quando executamos esse código, veremos a mesma saída da listagem 3-4. Mais
importante,
agora aumentamos a segurança do código e eliminamos a
chance de erros que podem
resultar de ir além do final da matriz ou não
indo longe o suficiente e faltando alguns itens.
A segurança e a concisão dos loops for fazem deles o loop mais comumente usado
em
Rust. Mesmo em situações em que você deseja executar algum código
certo número de
vezes, como no exemplo da contagem regressiva que usou um loop while
da Listagem 3-3,
a maioria dos Rustaceans usaria um loop for . A maneira de fazer isso
seria usar um
Range , que é um tipo fornecido pela biblioteca padrão
que gera todos os números em
sequência a partir de um número e terminando
antes de outro número.
Veja como seria a contagem regressiva usando um loop for e outro método,
que nós
ainda não falamos, rev , para reverter o intervalo:
fn main() {
println!("{}!", numero);
println!("LIFTOFF!!!");
Resumo
Você conseguiu! Esse foi um capítulo considerável: você aprendeu sobre variáveis, tipos
de
dados escalares e compostos, funções, comentários, expressões if e loops! E se
você quer
praticar com os conceitos discutidos neste capítulo, tente construir
programas para fazer o
seguinte:
Quando você estiver pronto para seguir em frente, falaremos sobre um conceito em Rust
que não
comumente existente em outras linguagens de programação: propriedade.
Entendendo Ownership
Ownership (posse) é a característica mais única do Rust, que o permite ter
garantias de
segurança de memória sem precisar de um garbage collector. Logo,
é importante entender
como funciona ownership no Rust. Neste capítulo, falaremos
sobre ownership e também
sobre várias características relacionadas: borrowing,
slices e como o Rust dispõe seus dados
na memória.
O Que É Ownership?
A característica central do Rust é ownership. Embora seja bem direta de
explicar, ela tem
implicações profundas em todo o resto da linguagem.
Todos os programas têm que decidir de que forma vão usar a memória do computador
durante a execução. Algumas linguagens possuem garbage collection (coleta de
lixo), que
constantemente busca segmentos de memória que já não são mais
utilizados enquanto o
programa executa; em outras linguagens, o programador deve
alocar e liberar memória de
forma explícita. Rust usa uma terceira abordagem: a
memória é gerenciada através de um
sistema de posse, que tem um conjunto de
regras verificadas em tempo de compilação.
Nenhuma característica relacionada ao
ownership implica qualquer custo em tempo de
execução.
Quando você entender ownership, você terá uma fundação sólida para entender as
características que fazem o Rust ser único. Neste capítulo, você vai aprender
ownership
trabalhando em alguns exemplos com foco em uma estrutura de dados
muito comum:
strings.
A Pilha e a Heap
Em muitas linguagens de programação, não temos que pensar muito sobre a pilha
e
sobre a heap. Mas em uma linguagem de programação de sistemas, como Rust,
o fato
de um valor estar na pilha ou na heap tem impacto na forma como a
linguagem se
comporta e no porquê de termos que tomar certas decisões. Vamos
descrever partes
do ownership em relação à pilha e à heap mais para a frente
neste capítulo, então
aqui vai uma explicação preparatória.
Tanto a pilha como a heap são partes da memória que estão disponíveis ao seu
código
para uso em tempo de execução, mas elas são estruturadas de formas
diferentes. A
pilha armazena valores na ordem em que eles chegam, e os remove
na ordem inversa.
Isto é chamado de last in, first out (último a chegar,
primeiro a sair). Imagine uma pilha
de pratos: quando você coloca mais pratos,
você os põe em cima da pilha, e quando
você precisa de um prato, você pega o
que está no topo. Adicionar ou remover pratos
do meio ou do fundo não funciona
tão bem! Dizemos fazer um push na pilha quando
nos refererimos a inserir
dados, e fazer um pop da pilha quando nos referimos a
remover dados.
A pilha é rápida por conta da forma como ela acessa os dados: ela nunca tem
que
procurar um lugar para colocar novos dados, ou um lugar de onde obter
dados, este
lugar é sempre o topo da pilha. Outra propriedade que faz a pilha
ser rápida é que
todos os dados contidos nela devem ocupar um tamanho fixo e
conhecido.
Para dados com um tamanho desconhecido em tempo de compilação, ou com um
tamanho que pode mudar, podemos usar a heap em vez da pilha. A heap é menos
organizada: quando colocamos dados na heap, nós pedimos um certo espaço de
memória. O sistema operacional encontra um espaço vazio em algum lugar na heap
que seja grande o suficiente, marca este espaço como em uso, e nos retorna um
ponteiro, que é o endereço deste local. Este processo é chamado de
alocar na heap, e
às vezes se abrevia esta frase como apenas "alocação".
Colocar valores na pilha não é
considerado uma alocação. Como o ponteiro tem
um tamanho fixo e conhecido,
podemos armazená-lo na pilha, mas quando queremos
os dados, de fato, temos que
seguir o ponteiro.
Imagine que você está sentado em um restaurante. Quando você entra, você diz
o
número de pessoas que estão com você, o atendente encontra uma mesa vazia
que
acomode todos e os leva para lá. Se alguém do seu grupo chegar mais tarde,
poderá
perguntar onde vocês estão para encontrá-los.
Acessar dados na heap é mais lento do que acessar dados na pilha, porque você
precisa seguir um ponteiro para chegar lá. Processadores de hoje em dia são
mais
rápidos se não precisarem pular tanto de um lugar para outro na memória.
Continuando com a analogia, considere um garçom no restaurante anotando os
pedidos de várias mesas. É mais eficiente anotar todos os pedidos de uma única
mesa
antes de passar para a mesa seguinte. Anotar um pedido da mesa A, depois
um da
mesa B, depois outro da mesa A, e outro da mesa B novamente seria um
processo
bem mais lento. Da mesma forma, um processador pode cumprir melhor
sua tarefa se
trabalhar em dados que estão próximos uns dos outros (assim como
estão na pilha)
em vez de dados afastados entre si (como podem estar na heap).
Alocar um espaço
grande na heap também pode levar tempo.
Quando nosso código chama uma função, os valores passados para ela (incluindo
possíveis ponteiros para dados na heap) e as variáveis locais da função são
colocados
na pilha. Quando a função termina, esses valores são removidos dela.
Rastrear quais partes do código estão usando quais dados na heap, minimizar a
quantidade de dados duplicados na heap e limpar segmentos inutilizados da heap
para que não fiquemos sem espaço são todos problemas tratados pelo ownership.
Uma vez que você entende ownership, você não vai mais precisar pensar tanto
sobre a
pilha e a heap, mas saber que ownership existe para gerenciar os dados
na heap pode
ajudar a explicar como e por que ele funciona.
Regras de Ownership
Primeiro, vamos dar uma olhada nas regras de ownership. Mantenha em mente essas
regras quando trabalharmos com os exemplos em seguida:
1. Cada valor em Rust possui uma variável que é dita seu owner (sua dona).
2. Pode apenas haver um owner por vez.
3. Quando o owner sai fora de escopo, o valor será destruído.
Escopo de Variáveis
let s = "olá";
Em outras palavras, existem dois pontos no tempo que são importantes aqui:
Neste ponto, a relação entre escopos e quando variáveis são válidas é similar a
outras
linguagens de programação. Agora vamos construir sobre este entendimento,
apresentando o tipo String .
O Tipo String
Para ilustrar as regras de ownership, precisamos de um tipo de dados que seja
mais
complexo do que aqueles abordados no Capítulo 3. Os tipos abordados na
seção "Tipos de
Dados" são todos armazenados na pilha, e retirados dela quando
seu escopo termina, mas
queremos ver dados que são armazenados na heap e
explorar como o Rust faz para saber
quando limpar esses dados.
Vamos usar String como exemplo aqui, e concentrar nas partes de String que
estão
relacionadas ao ownership. Esses aspectos também se aplicam aos outros
tipos complexos
de dados fornecidos pela biblioteca padrão e os que você mesmo
cria. Vamos discutir
String mais a fundo no Capítulo 8.
let s = String::from("texto");
Mas então, qual é a diferença aqui? Por que String pode ser alterada enquanto
literais não
podem? A diferença está em como esses dois tipos lidam com memória.
Memória e Alocação
// mais válida
Nota: Em C++, esta forma de desalocar recursos no fim do tempo de vida útil de
um
item às vezes é chamado de Resource Acquisition Is Initialization (RAII,
do inglês,
Aquisição de Recurso É Inicialização). A função drop em Rust vai
lhe ser bastante
familar se você já tiver usado padrões RAII.
Este padrão tem um profundo impacto na forma de escrever código em Rust. Pode
parecer
simples agora, mas o comportamento do código pode ser inesperado em
situações mais
complicadas, quando queremos que múltiplas variáveis usem os
dados que alocamos na
heap. Vamos explorar algumas dessas situações agora.
let x = 5;
let y = x;
Provavelmente podemos advinhar o que isto faz com base nas nossas experiências
com
outras linguagens: "Associe o valor 5 a x ; depois faça uma cópia do
valor em x e a associe
a y ." Agora temos duas variáveis, x e y , e ambas
são iguais a 5 . É isto mesmo que
acontece, porque números inteiros são valores
simples que possuem um tamanho fixo e
conhecido, e esses dois valores 5 são
colocados na pilha.
let s1 = String::from("texto");
let s2 = s1;
Isso parece bem similar ao código anterior, então poderíamos assumir que
funcionaria da
mesma forma, isto é, a segunda linha faria uma cópia do valor em
s1 e a associaria a s2 .
Mas não é exatamente isso que acontece.
Para explicar isso mais detalhadamente, vamos ver como a String funciona por
baixo dos
panos na Figura 4-1. Uma String é feita de três partes, mostradas
à esquerda: um
ponteiro para a memória que guarda o conteúdo da string, um
tamanho, e uma capacidade.
Este grupo de dados é armazenado na pilha. No lado
direito está a memória na heap que
guarda o conteúdo.
s1
nome valor índicevalo
ptr 0 t
tamanho 5 1 e
capacidade 5 2 x
3 t
4 o
s1
nome valor
ptr
tamanho 5
capacidade 5 índicevalo
0 t
s2 1 e
nome valor 2 x
ptr 3 t
tamanho 5 4 o
capacidade 5
s1
nome valor índicevalo
ptr 0 t
tamanho 5 1 e
capacidade 5 2 x
3 t
4 o
s2
nome valor índicevalo
ptr 0 t
tamanho 5 1 e
capacidade 5 2 x
3 t
4 o
let s2 = s1;
println!("{}", s1);
Você vai ter um erro como este, porque o Rust lhe impede de usar a referência
que foi
invalidada:
--> src/main.rs:5:20
3 | let s2 = s1;
4 |
5 | println!("{}", s1);
= note: move occurs because `s1` has type `std::string::String`, which does
s1
nome valor
ptr
tamanho 5
capacidade 5 índicevalo
0 t
s2 1 e
nome valor 2 x
ptr 3 t
tamanho 5 4 o
capacidade 5
Isso resolve o nosso problema! Tendo apenas s2 válida, quando ela sair de
escopo,
somente ela vai liberar a memória, e pronto.
Ademais, isto implica uma decisão de projeto: Rust nunca vai criar deep copies
dos seus
dados. Logo, para qualquer cópia automática que aconteça, pode-se
assumir que ela não
será custosa em termos de desempenho em tempo de execução.
Se nós queremos fazer uma cópia profunda dos dados da String que estão na
heap, e não
apenas os dados que estão na pilha, podemos usar um método comum
chamado clone .
Vamos discutir sintaxe de métodos no Capítulo 5, mas como os
métodos constituem uma
característica comum em várias linguagens de programação,
você provavelmente já os viu
antes.
let s1 = String::from("texto");
let s2 = s1.clone();
Quando você ver uma chamada para clone , você sabe que algum código arbitrário
está
sendo executado, e que este código talvez seja custoso. É um indicador
visual de que algo
diferente está acontecendo.
Há um outro detalhezinho de que ainda não falamos. Este código usando números
inteiros,
parte do qual foi mostrado anteriormente na Listagem 4-2, funciona e é
válido:
let x = 5;
let y = x;
Mas este código parece contradizer o que acabamos de aprender: não temos uma
chamada
ao método clone , mas x ainda é válido e não foi movido para y .
O motivo é que tipos como números inteiros têm um tamanho conhecido em tempo de
compilação e são armazenados inteiramente na pilha, e por isso, cópias desses
valores são
rápidas de se fazer. Isso significa que não há razão para impedir
x de ser válido após
criarmos a variável y . Em outras palavras, não há
diferença entre cópia rasa e profunda
aqui, então chamar o método clone não
faria nada diferente de uma cópia rasa, por isso
podemos deixá-lo de lado.
O Rust tem uma anotação especial chamada de trait Copy , que podemos colocar
em tipos
como números inteiros, que são armazenados na pilha (falaremos mais
sobre traits no
Capítulo 10). Se um tipo possui o trait Copy , uma variável
anterior vai continuar sendo
utilizável depois de uma atribuição. O Rust não vai
nos deixar anotar um tipo com o trait
Copy se este tipo, ou qualquer uma de
suas partes, tiver implementado o trait Drop . Se o
tipo precisa que algo
especial aconteça quando o valor sair de escopo e há uma anotação
Copy neste
tipo, vamos ter um erro de compilação. Para aprender sobre como inserir a
anotação Copy ao seu tipo, veja o Apêndice C em Traits Deriváveis.
Ownership e Funções
Arquivo: src/main.rs
fn main() {
} // Aqui, x sai de escopo, e depois s. Mas como o valor de s foi movido, nada
// de especial acontece.
println!("{}", uma_string);
println!("{}", um_inteiro);
Arquivo: src/main.rs
fn main() {
// para s1.
// que a chamou.
// entrega_valor.
// escopo.
// pega_e_entrega_valor.
A posse de uma variável segue o mesmo padrão toda vez: atribuir um valor a outra
variável
irá movê-lo. Quando uma variável que inclui dados na heap sai de
escopo, o valor será
destruído pelo método drop , a não ser que os dados tenham
sido movidos para outra
variável.
Arquivo: src/main.rs
fn main() {
let s1 = String::from("texto");
(s, tamanho)
Mas isto é muita cerimônia e trabalho para um conceito que deveria ser comum.
Para nossa
sorte, Rust tem uma ferramenta para este conceito, e ela é chamada de
referências.
Referências e Borrowing
O problema de usar tuplas, que vimos no fim da seção anterior, é que precisamos
retornar
a String , de forma que ainda possamos usá-la após a chamada à função
Aqui está uma forma de como você poderia definir e usar uma função
calcula_tamanho
que recebe uma referência para um objeto como parâmetro, em
vez de pegar este valor
para si:
Arquivo: src/main.rs
fn main() {
let s1 = String::from("texto");
s.len()
Primeiro, repare que todo aquele código usando uma tupla na declaração da
variável e no
retorno da função já se foi. Segundo, note que passamos &s1 para
calcula_tamanho , e na
sua definição, temos &String em vez de apenas
String .
Esses & são referências, e eles permitem que você se refira a algum valor
sem tomar posse
dele. A Figura 4-5 mostra um diagrama.
s s1
nomevalor nome valor índicevalor
ptr ptr 0 t
tamanho 5 1 e
capacidade 5 2 x
3 t
4 o
let s1 = String::from("texto");
A sintaxe &s1 nos permite criar uma referência que se refere ao valor s1 ,
mas não o
possui. Como ela não o possui, o valor a que ela aponta não será
destruído quando a
referência sair de escopo.
Da mesma forma, a assinatura da função usa & para indicar que o tipo do
parâmetro s é
uma referência. Vamos adicionar algumas anotações para explicar:
s.len()
} // Aqui, s sai de escopo. Mas como ela não possui o valor a que se refere,
// nada acontece.
Arquivo: src/main.rs
fn main() {
let s = String::from("texto");
modifica(&s);
fn modifica(uma_string: &String) {
uma_string.push_str(" longo");
--> main.rs:8:5
7 | fn modifica(uma_string: &String) {
8 | uma_string.push_str(" longo");
Assim como as variáveis são imutáveis por padrão, referências também são. Não
temos
permissão para modificar algo para o qual temos uma referência.
Referências Mutáveis
Arquivo: src/main.rs
fn main() {
modifica(&mut s);
uma_string.push_str(" longo");
Primeiro, temos que fazer com que s seja mut . Depois, temos que criar uma
referência
mutável com &mut s e aceitar uma referência mutável com
uma_string: &mut String .
Mas referências mutáveis possuem uma grande restrição: você só pode ter uma
referência
mutável para um determinado dado em um determinado escopo. Este
código vai falhar:
Arquivo: src/main.rs
let r1 = &mut s;
let r2 = &mut s;
--> main.rs:5:19
4 | let r1 = &mut s;
5 | let r2 = &mut s;
6 | }
Esta restrição permite a mutação, mas de uma forma bem controlada. Isto é algo
com que
novos Rustáceos passam trabalho, porque a maioria das linguagens de
programação
permitem modificar um valor quando você quiser. O benefício de ter
esta restrição é que o
Rust previne data races em tempo de compilação.
Um data race é parecido com uma condição de corrida, e acontece quando esses
três
fatores ocorrem:
Como sempre, podemos usar chaves ( {} ) para criar um novo escopo, permitindo
múltiplas
referências mutáveis, mas não simultâneas:
let mut s = String::from("texto");
let r1 = &mut s;
} // aqui r1 sai de escopo, então já podemos criar uma nova referência sem
// problema nenhum.
let r2 = &mut s;
Existe uma regra parecida para combinar referências mutáveis e imutáveis. Este
código
resulta em erro:
immutable
--> main.rs:6:19
7 | }
Eita! Nós também não podemos ter uma referência mutável enquanto temos uma
imutável.
Usuários de uma referência imutável não esperam que os valores mudem
de repente!
Porém, múltiplas referências imutáveis são permitidas, pois ninguém
que esteja apenas
lendo os dados será capaz de afetar a leitura que está sendo
feita em outra parte do código.
Mesmo que esses erros sejam frustrantes às vezes, lembre-se que é o compilador
do Rust
apontando um bug potencial antecipadamente (em tempo de compilação,
em vez de
execução), e mostrando exatamente onde está o problema, em vez de você
ter que
investigar por que algumas vezes os seus dados não são aquilo que você
esperava que
fosse.
Referências Soltas
Vamos tentar criar uma referência solta, que o Rust vai impedir com um erro em
tempo de
compilação:
Arquivo: src/main.rs
fn main() {
let s = String::from("texto");
&s
--> main.rs:5:16
= help: this function's return type contains a borrowed value, but there is
Esta mensagem de erro se refere a uma característica que não abordamos ainda:
lifetimes.
Vamos discutir lifetimes em detalhe no Capítulo 10. Mas, se você
desconsiderar a parte
sobre lifetimes, a mensagem mostra a razão deste código
ser um problema:
this function's return type contains a borrowed value, but there is no value
Tradução: o tipo de retorno desta função contém um valor emprestado, mas não
há
nenhum valor que se possa pegar emprestado.
Vamos dar uma olhada mais de perto no que está acontecendo, exatamente, em cada
estágio da nossa função soltar :
fn soltar() -> &String { // soltar retorna uma referência a uma String
// Perigo!
let s = String::from("texto");
Isto funciona sem nenhum problema. A String é movida para fora, e nada é
desalocado.
As Regras de Referências
Slices
Outro tipo de dados em que não há ownership é a slice (do inglês, fatia).
Slices lhe permitem
referenciar uma sequência contígua de elementos em uma
coleção em vez de referenciar a
coleção inteira.
Aqui está um pequeno problema de programação: escrever uma função que pega uma
string e retorna a primeira palavra que encontrar dentro dela. Se a função não
encontrar
um espaço na string, significa que a string inteira é uma palavra só,
então a string toda deve
ser retornada.
Esta função, primeira_palavra , tem uma &String como parâmetro. Nós não
queremos
tomar posse dela, então tudo bem. Mas o que nós deveríamos retornar?
Não temos uma
forma de falar sobre parte de uma string. No entanto, poderíamos
retornar o índice do final
de uma palavra. Vamos tentar fazer isso, conforme
mostrado na Listagem 4-5:
Arquivo: src/main.rs
return i;
s.len()
Vamos dividir este código em algumas partes. Como precisamos varrer a String
elemento
por elemento, e verificar se algum valor é um espaço, vamos converter
nossa String em
um array de bytes usando o método as_bytes :
Vamos discutir sobre iteradores em mais detalhes no Capítulo 13. Por enquanto,
saiba que
iter é um método que retorna cada elemento em uma coleção, e
enumerate encapsula o
resultado do iter e retorna cada elemento como parte
de uma tupla. O primeiro elemento
da tupla é o índice, e o segundo elemento é
uma referência ao valor. Isto é um pouco mais
conveniente do que calcular o
índice nós mesmos.
Como o método enumerate retorna uma tupla, podemos usar padrões para
desestruturar
esta tupla, assim como qualquer outra coisa em Rust. Então, no
for , especificamos um
padrão que tem i para o índice na tupla e &item para
o byte. Como pegamos uma
referência ao elemento através do
.iter().enumerate() , usamos um & neste padrão.
Nós procuramos o byte que representa um espaço usando a sintaxe de byte literal.
Se
encontrarmos um espaço, retornamos a posição dele. Caso contrário, retornamos
o
tamanho da string usando s.len() :
return i;
s.len()
Arquivo: src/main.rs
fn main() {
// palavra ainda tem o valor 5 aqui, mas já não há mais uma string para a
Ter que se preocupar sobre o índice da palavra ficar fora de sincronia com os
dados em s
é tedioso e propenso a erros! Gerenciar esses índices é ainda mais
delicado se escrevermos
uma função segunda_palavra . Sua assinatura teria que
ser algo do tipo:
Felizmente, Rust possui uma solução para este problema: slices de string.
Slices de String
Uma slice de string é uma referência para uma parte de uma String , e tem a
seguinte
forma:
Isto é similar a pegar uma referência à String inteira, mas com um [0..5] a
mais. Em vez
de uma referência à String inteira, trata-se de uma referência a
uma porção da String . A
sintaxe início..fim representa um range
(uma faixa) que começa em início e continua
até, mas não incluindo, fim .
let s = String::from("texto");
Da mesma forma, se a sua slice inclui o último byte da String , você pode
omitir o último
número. Isso significa que as seguintes formas são equivalentes:
let s = String::from("texto");
Você também pode omitir ambos os valores para pegar uma slice da string inteira.
Logo,
essas duas formas são equivalentes:
let s = String::from("texto");
Arquivo: src/main.rs
return &s[0..i];
&s[..]
Pegamos o índice para o fim da palavra da mesma forma como fizemos na Listagem
4-5,
buscando a primeira ocorrência de um espaço. Quando o encontramos,
retornamos uma
slice de string usando o início da string e o índice do espaço
como índices inicial e final,
respectivamente.
Agora, temos uma API bem direta que é bem mais difícil de bagunçar, uma vez que
o
compilador vai se certificar que as referências dentro da String
permanecerão válidas.
Lembra do bug do programa na Listagem 4-6, quando
obtivemos o índice para o fim da
primeira palavra mas depois limpamos a string,
invalidando o índice obtido? Aquele código
era logicamente incorreto, mas não
mostrava nenhum erro imediato. Os problemas
apareceriam mais tarde quando
tentássemos usar o índice da primeira palavra com uma
string que foi esvaziada.
Slices tornam esse bug impossível de acontecer e nos permitem
saber que temos um
problema no código muito mais cedo. Na versão usando slice, a
função
primeira_palavra vai lançar um erro em tempo de compilação:
Arquivo: src/main.rs
fn main() {
s.clear(); // Erro!
--> src/main.rs:6:5
5 |
6 | s.clear(); // Erro!
7 | }
O tipo de s aqui é &str : é uma slice apontando para aquele ponto específico
do binário.
Também é por isso que strings literais são imutáveis; &str é uma
referência imutável.
Slices de Strings como Parâmetros
Saber que você pode obter slices de literais e String s nos levam a mais um
aprimoramento da função primeira_palavra , e aqui está sua assinatura:
Arquivo: src/main.rs
fn main() {
Outras Slices
Slices de string, como você pode imaginar, são específicas de strings. Mas há
também um
tipo de slice mais genérico. Considere esta array:
Assim como às vezes queremos nos referir a uma parte de uma string, podemos
também
querer nos referir a uma parte de uma array, e faríamos isso da seguinte
forma:
let a = [1, 2, 3, 4, 5];
Essa slice tem o tipo &[i32] . Ela funciona da mesma forma que as slices de
string,
armazenando uma referência para o primeiro elemento e um tamanho. Você
vai usar esse
tipo de slice para todos os tipos de coleções. Vamos discutir
essas coleções em mais detalhe
quando falarmos sobre vetores no Capítulo 8.
Resumo
Os conceitos de ownership, borrowing, e slices são o que garante a segurança de
memória
dos programas em Rust em tempo de compilação. A linguagem Rust lhe dá
controle sobre o
uso da memória, assim como outras linguagens de programação de
sistemas, mas como o
dono dos dados limpa automaticamente a memória quando ele
sai de escopo, você não
tem que escrever e debugar código extra para ter esse
controle.
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
Para usar uma struct depois de a definirmos, criamos uma instância dessa struct,
especificando valores para cada um dos campos. Estamos a criar uma instância, indicando o
nome da struct e depois entre chavetas, adicionamos pares campo:valor onde as chaves
são os nomes dos campos e os valores são os
dados que deseja armazenar nesses campos.
Nós não temos que atribuir os elementos na mesma ordem em que os temos declarado na
struct.
Em outras palavras, a definição da struct é como um modelo geral para o tipo,
e as
instâncias preenchem esse modelo com os dados específicos, para criar valores desse tipo.
Por exemplo, podemos declarar um usuário específico como mostrado na Lista 5-2:
email: String::from("alguem@exemplo.com"),
username: String::from("algumnome123"),
active: true,
sign_in_count: 1,
};
Para obter um valor específico de uma struct, podemos utilizar a notação de ponto. Se
quiséssemos apenas esse endereço de e-mail do usuário, podemos usar
user1.email
sempre que queremos usar este valor. Para alterar um valor em uma
struct, se a instância é
mutável, podemos usar a notação de ponto e atribuir a um campo específico. Lista 5-3
mostra como alterar o valor do campo e-mail
de uma instância de User mutável:
let mut user1 = User {
email: String::from("alguem@exemplo.com"),
username: String::from("algumnome123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("outroemail@exemplo.com");
Se você tiver as variáveis com os mesmos nomes dos campos da struct, você pode usar o
field init shorthand ((inicialização abreviada do campo). Isto pode fazer com que as funções
que criam novas instâncias de structs mais concisos.
Em primeiro lugar, vejamos o modo
mais detalhado para inicializar uma instância
de uma struct. A função chamada build_user
mostrada aqui na Lista 5-4 tem parâmetros chamados e-mail e username (nome de
usuário). A função cria e retorna uma instância do User :
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
Porque os nomes dos parâmetros e-mail e username são os mesmos que os nomes
de
campo do e-mail e nome de usuário da struct User , podemos escrever build_user sem
a repetição de e-mail e username como mostrado na Lista5-5.
Esta versão de build_user
comporta-se da mesma maneira como na Lista 5-4.
A sintaxe abreviada pode fazer casos
como esse mais curtos para escrever, especialmente quando structs têm muitos campos.
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
Lista 5-5: Uma função build_user que usa a sintaxe campo init porque os parâmetros e-
mail e username têm o mesmo nome dos campos da struct
É frequentemente útil criar uma nova instância a partir de uma antiga instância, usando a
maioria dos valores da antiga instância mas mudando alguns. A Lista 5-6
mostra um
exemplo da criação de uma nova instância do user1 em user2 através
da definição dos
valores de e-mail e username mas usando os mesmos valores para o resto dos campos do
exemplo user1 que criamos na Lista 5-2:
email: String::from("outro@exemplo.com"),
username: String::from("outronome567"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
Lista 5-6: Criação de uma nova instância do User , user2 , e a definição de alguns campos
para os valores dos mesmos campos
do user1
A struct update syntax (Sintaxe de Atualização da Struct) alcança o mesmo efeito que o
código na Lista 5-6 usando menos código. A sintaxe de atualização
struct usa .. para
especificar que os campos restantes não explicitamente configurados devem ter o mesmo
valor que os campos na determinada instância. O código na Lista 5-7 também cria uma
instância no user2 , que tem um valor
diferente de e-mail e nome de usuário mas tem os
mesmos valores para os active e sign_no_count campos que user1 :
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};
Podemos também definir structs que parecem semelhantes a tuplas, chamadas tuple
structs, que têm o significado que o nome struct fornece, mas não têm os nomes associados
com os seus campos, apenas os tipos dos campos. A definição de uma struct-tupla, ainda
começa com a palavra-chave struct e o
nome da struct, que é seguida pelos tipos na
tupla. Por exemplo, aqui estão
as definições e usos da struct-tupla chamados Color e
Point :
Note que os valores black e origin são diferentes tipos, uma vez que eles
são de
diferentes instâncias struct-tupla. Cada struct que definimos é o seu próprio tipo, embora
os campos dentro do struct tenham os mesmos tipos. No geral as struct-tuplas comportam-
se como instâncias de tuplas, que discutimos no Capítulo 3.
Podemos também definir structs que não têm quaisquer campos! Estes são chamados
de
unit-like structs (unidades como structs) porque eles se comportam da mesma
forma que
() , o tipo unidade. Unit-like structs podem ser úteis em situações,
como quando você
precisa implementar um trait de algum tipo, mas você não tem quaisquer dados que você
deseja armazenar no tipo em si. Traits será discutido no Capítulo 10.
Na definição de struct User , na Lista 5-1, utilizamos a propriedade tipo String em vez de
&str , uma ‘fatia’ tipo string. Esta é uma escolha deliberada, porque queremos que
instâncias deste struct possuam todos os seus dados e para que os dados sejam válidos por
todo o tempo que o struct é válido.
É possível para structs armazenar referências a dados que são propriedade de algo
diferente, mas para isso requer o uso de lifetimes (tempo de vida), uma característica de
Rust que é discutida no Capítulo 10. Lifetimes garantem
que os dados referenciados por um
struct são válidos enquanto struct existir.
Vamos dizer que você tenta armazenar uma
referência em um struct sem especificar lifetimes, como este:
Filename: src/main.rs
struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
fn main() {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
-->
2 | username: &str,
-->
3 | email: &str,
Vamos discutir como corrigir estes erros, assim você pode armazenar referências
em structs
no Capítulo 10, mas por agora, vamos corrigir erros como estes usando tipos de
propriedade, utilizando String em vez de referências como &str .
Vamos fazer um novo projeto binário com Cargo, chamado retângulos que terá
o
comprimento e a largura do retângulo especificados em pixels e irá calcular a área do
retângulo. A Lista 5-8 mostra um programa curto com uma maneira de fazer isso no nosso
projeto src/main.rs:
Filename: src/main.rs
fn main() {
println!(
area(length1, width1)
);
length * width
Lista 5-8: Calcular a área de um retângulo especificado pelo seu comprimento e largura em
variáveis separadas
Filename: src/main.rs
fn main() {
println!(
area(rect1)
);
dimensions.0 * dimensions.1
Não importa, para o cálculo da área, trocar-se comprimento e largura, mas se queremos
desenhar o retângulo na tela, já importa! Temos de ter em mente que comprimento é a
tupla índice 0 e largura é o tupla índice 1 .
Se alguém trabalhar com este código, terá de
descobrir isso e mantê-lo em mente. Seria fácil esquecer ou misturar estes valores e causar
erros, porque
não se transmitiu o significado dos nossos dados no nosso código.
Usamos structs para dar significado aos dados usando rótulos. Podemos transformar a
tupla que estamos usando em um tipo de dados, com um nome
para o conjunto bem como
nomes para as partes, como mostra a Lista 5-10:
Filename: src/main.rs
struct Rectangle {
length: u32,
width: u32,
fn main() {
println!(
area(&rect1)
);
rectangle.length * rectangle.width
A nossa função área agora é definida com um parâmetro, que chamamos rectangle , cujo
tipo é um empréstimo de uma instância da struct imutável
Rectangle . Como mencionado
no capítulo 4, queremos usar a struct, em vez de
tomar posse dela. Desta forma, main
mantém-se a sua proprietaria e pode continuar a usar o rect1 , que é a razão para usar o
& na assinatura da função e onde chamamos a função.
Seria útil para ser capaz de imprimir uma instância do Rectangle enquanto
estamos
depurando o nosso programa, a fim de consultar os valores para todos
os seus campos.
Lista 5-11 usa a macro 'println!' como temos usado nos capítulos anteriores:
Filename: src/main.rs
struct Rectangle {
length: u32,
width: u32,
fn main() {
Quando executamos este código, obtemos um erro com esta mensagem interna:
A macro 'println!' pode fazer muitos tipos de formatação, e por padrão, {} diz a println! ,
para utilizar a formatação conhecida como Display :
saída destinada para consumo do
utilizador final. Os tipos primitivos que vimos
até agora implementam Display por padrão,
porque só há uma maneira que você deseja mostrar um 1 ou qualquer outro tipo primitivo
para um usuário. Mas com
Structs, a forma como println! deve formatar a saída é menos
clara, pois existem mais possibilidades de exibição: você quer vírgulas ou não? Deseja
imprimir as chavetas {} ? Todos os campos devem ser mostrados? Devido a esta
ambiguidade, Rust não tenta adivinhar o que queremos e as structs não têm uma
implementação de Display .
Avisa que `Rectangle` não pode ser formatado com o formato padrão;
Vamos tentar! A chamada da macro println! agora vai ficar como println!("rect1 is
{:?}", rect1); . Colocando o especificador :? dentro de {} diz à println! que nós
queremos usar um formato de saída chamado Debug . Debug é uma trait (característica)
que nos permite imprimir as nossas
structs de uma maneira útil para os desenvolvedores
para que possamos ver o seu
valor enquanto estamos a depurar do nosso código.
Executamos o código com esta mudança. Pô! Nós ainda obtemos um erro:
nota: Rectangle não pode ser formatado usando :? ; se estiver definido no nosso crate,
adicionamos #[derive(Debug)] ou adicionamos manualmente.
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
length: u32,
width: u32,
fn main() {
Lista 5-12: Adicionando a anotação para derivar caracteristica Debug e imprimir a instância
Rectangle usando a formatação debug
Agora, quando executamos o programa, não teremos quaisquer erros e vamos ver
a
seguinte informação:
Boa! Não é o mais bonito, mas mostra os valores de todos os campos para essa instância,
que irá ajudar durante a depuração. Quando temos structs maiores, é útil ter a informação
um pouco mais fácil de ler; nesses casos, podemos usar
{:#?} ao invés de {:?} na frase
println! . Quando utilizamos o {:#?} no
exemplo, vamos ver a informação como esta:
rect1 is Rectangle {
length: 50,
width: 30
Rust forneceu um número de caracteristicas (traits) para usarmos com a notação derive
que pode adicionar um comportamento útil aos nossos tipos personalizados. Esses traits e
seus comportamentos estão listadas no Apêndice C.
Abordaremos como implementar estes
traits com comportamento personalizado, bem como a forma de criar as suas próprias
traits (características) no Capítulo 10.
A nossa função area é muito específica: ela apenas calcula a área de retângulos. Seria útil
fixar este comportamento à nossa struc Rectangle ,
porque não vai funcionar com
qualquer outro tipo. Vejamos como podemos continuar a refazer este código tornando a
função area em um método area
definido no nosso Rectangle .
Sintaxe do Método
Methods (métodos) são semelhantes às funções: eles são declarados com a chave
fn e o
seu nome, eles podem ter parâmetros e valor de retorno, e eles contêm algum código que é
executado quando eles são chamados de algum outro lugar. No entanto, métodos são
diferentes das funções, porque são definidos no contexto
de uma struct (ou um objeto
enum ou uma trait, que nós cobrimos nos Capítulos 6
e 17, respectivamente), o seu
primeiro parâmetro é sempre self , que representa
a instância da struct do método que
está a ser chamado.
Definindo Métodos
Vamos alterar a função area que tem uma instância de Rectangle como um parâmetro e,
em vez disso, fazer um método area definido na struct Rectangle
como mostrado na Lista
5-13:
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
length: u32,
width: u32,
impl Rectangle {
self.length * self.width
fn main() {
println!(
rect1.area()
);
Escolhemos &selft aqui pela mesma razão usamos &Rectangle na versão função:
nós
não queremos tomar posse, nós apenas queremos ler os dados da struct, e não escrever
nela. Se quisermos mudar a instância da qual chamamos
o método como parte do que o
método faz, usariamos &mut self como o primeiro parâmetro. Ter um método que toma
posse da instância, usando apenas self como primeiro parâmetro é raro; esta técnica é
geralmente utilizada quando o método transforma o self em algo mais e queremos evitar
que o chamador use a instância original após a transformação.
Em linguagens como C++, dois operadores diferentes são usados para chamar métodos:
você usa . se você está chamando um método do objeto diretamente
e -> se você está
chamando o método em um apontador para o objeto e necessita de desreferenciar o
apontadr primeiro. Em outras palavras, se objeto é um apontador, objeto->algo() é
semelhante a (*objeto).algo() .
Rust não tem um equivalente para o operador -> ; em vez disso, Rust tem um recurso
chamado referenciamento e desreferenciamento automático. Chamada de métodos é um dos
poucos lugares em Rust que têm este comportamento.
Eis como funciona: quando você chamar um método com objecto.algo() , Rust adiciona
automaticamente & , &mut ou * para que objecto corresponda
à assinatura do método.
Em outras palavras, as seguintes são as mesmas:
p1.distance(&p2);
(&p1).distance(&p2);
Filename: src/main.rs
fn main() {
Sabemos que queremos definir um método, por isso vai ser dentro do bloco impl
Rectangle . O nome do método será can_hold , e vai tomar um empréstimo
imutável de um
outro Rectangle como parâmetro. Podemos dizer qual o tipo
do parâmetro, olhando o
código que chama o método: rect1.can_hold(&rect2) passa &rect2 , que é um
empréstimo imutável de rect2 , uma instância do Rectangle . Isso faz sentido porque nós
só precisamos de ler rect2 (em vez de escrever, que precisaria de um empréstimo
mutável),e nós queremos
que main conserve a propriedade de rect2 para que possamos
utilizá-lo novamente depois de chamar o método can_hold . O valor de retorno de
can_hold
será um booleano, e a aplicação irá verificar se o comprimento e a largura de
self são ambos maiores que o comprimento e a largura do outro Rectangle ,
respectivamente. Vamos adicionar o novo método can_hold ao bloco impl da Lista 5-13,
mostrado na Lista 5-15:
Filename: src/main.rs
impl Rectangle {
self.length * self.width
Quando executamos este código com a função main na Lista 5-14, vamos obter a
informação desejada. Métodos podem ter vários parâmetros que nós adicionamos à
assinatura depois do parametro de self , e esses parâmetros funcionam como parâmetros
em funções.
Funções Associadas
Outro recurso útil dos blocos impl é que podemos definir funções dentro dos blocos impl
que não recebem self como um parâmetro.
Estas são chamadas de funções associadas
porque elas estão associados com a struct. Elas ainda são funções, não métodos, porque
elas não têm uma instância da struct para trabalhar. Você já usou a função associada
String::from .
Funções associadas são usados frequentemente para construtores que retornam uma
nova
instância da struct. Poderíamos, por exemplo, fornecer uma função associada
que teria um
parametro dimensão e usar esse parâmetro como comprimento e largura, tornando assim
mais fácil criar um retângulo Rectangle em vez de ter
que especificar o mesmo valor duas
vezes:
Filename: src/main.rs
impl Rectangle {
Para chamar esta função associada, usamos a sintaxe :: com o nome da struct,
como let
sq = Rectangle::square(3); , por exemplo. Esta função é nomeada pela struct: a sintaxe
:: é utilizada tanto para funções associadas e namespaces criados por módulos, que
discutiremos no Capítulo 7.
Cada struct pode ter vários blocos impl . Por exemplo, a Lista 5-15 é equivalente ao código
mostrado na Lista 5-16, que tem cada método em seu próprio bloco impl :
impl Rectangle {
self.length * self.width
impl Rectangle {
Não há nenhuma razão para separar estes métodos em vários blocos impl aqui,
mas é
uma sintaxe válida. Vamos ver um caso quando vários blocos impl são úteis no Capítulo 10
quando falamos de tipos genéricos e traços.
Sumário
As Structs permitem-nos criar tipos personalizados que são significativos para o nosso
domínio. Usando structs, podemos manter pedaços de dados associados ligados uns aos
outros e nomear cada pedaço para fazer nosso código claro.
Métodos ajudam-nos a
especificar o comportamento que as instâncias das nossas
structs têm, funções associadas
dão-nos a funcionalidade de namespace que é particular à nossa struct sem ter uma
instância disponível.
Mas structs não são a única maneira que nós podemos criar tipos personalizados:
vamos ao
recurso do Rust, enum, para adicionar uma outra ferramenta à nossa caixa de ferramentas.
enum VersaoIp {
V4,
V6,
VersaoIp é um tipo de dados que agora nós podemos usar em qualquer lugar no
nosso
código.
fn rotear(versao_ip: VersaoIp) { }
E podemos ainda chamar esta função passando qualquer uma das variantes:
rotear(VersaoIp::V4);
rotear(VersaoIp::V6);
O uso de enums tem ainda mais vantagens. Pensando mais a fundo sobre o nosso
tipo de
endereço IP, ainda não temos uma forma de representar o endereço em
si, apenas sabemos
qual a versão dele. Tendo em vista o que você acabou de
aprender sobre structs no Capítulo
5, você poderia abordar esse problema assim
como visto na Listagem 6-1:
enum VersaoIp {
V4,
V6,
struct EnderecoIp {
versao: VersaoIp,
endereco: String,
versao: VersaoIp::V4,
endereco: String::from("127.0.0.1"),
};
versao: VersaoIp::V6,
endereco: String::from("::1"),
};
Aqui nós definimos uma struct EnderecoIp que tem dois membros: versao , do
tipo
VersaoIp (que definimos anteriormente) e endereco , do tipo String .
Temos duas
instâncias dessa struct. A primeira, local , tem o valor
VersaoIp::V4 como sua versao , e
um endereço associado igual a 127.0.0.1 .
A segunda instância, loopback , tem como sua
versao a outra variante de
VersaoIp , V6 , e o endereço ::1 associado a ela. Nós usamos
uma struct
para encapsular os valores de versao e endereco , agora a variante está
associada ao valor.
Podemos representar o mesmo conceito de uma forma mais concisa usando apenas
uma
enum, em vez de uma enum dentro de uma struct, colocando dados dentro de
cada
variante da enum, diretamente. Esta nova definição da enum EnderecoIp
diz que ambas as
variantes, V4 e V6 , terão uma String associada:
enum EnderecoIp {
V4(String),
V6(String),
Podemos anexar dados a cada variante da enum diretamente, assim não existe mais
a
necessidade de uma struct adicional.
Há uma outra vantagem de se usar uma enum em vez de uma struct: cada variante
pode
conter dados de diferentes tipos e quantidades. Os endereços IP da versão
quatro têm
sempre quatro componentes numéricas, cada uma com valor de 0 a 255.
Se quiséssemos
representar endereços V4 como quatro valores u8 , e ao mesmo
tempo manter os
endereços V6 como uma String , não poderíamos usar uma
struct. Já as enums podem
facilmente atender a este caso:
enum EnderecoIp {
V6(String),
// detalhes omitidos
struct Ipv6Addr {
// detalhes omitidos
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
Esse código mostra que você pode colocar qualquer tipo de dados dentro de uma
variante
de enum: strings, tipos numéricos ou structs, por exemplo. Você pode
até mesmo incluir
outra enum! Além disso, os tipos definidos pela biblioteca
padrão não são tão mais
complicados do que o que talvez você pensaria em fazer.
Repare que, mesmo havendo um IpAddr definido pela biblioteca padrão, nós ainda
podemos criar e utilizar nossa própria definição (com o mesmo nome, inclusive)
sem
nenhum conflito, porque não trouxemos a definição da biblioteca padrão para
dentro do
nosso escopo. Falaremos mais sobre a inclusão de tipos em um escopo
no Capítulo 7.
Vamos ver outro exemplo de uma enum na Listagem 6-2: esta tem uma grande
variedade
de tipos embutidos nas suas variantes:
enum Mensagem {
Sair,
Escrever(String),
Definir uma enum com variantes iguais às da Listagem 6-2 é similar a definir
diferentes
tipos de struct, exceto que a enum não usa a palavra-chave struct ,
e todas as variantes
são agrupadas dentro do tipo Mensagem . As structs
seguintes podem guardar os mesmos
dados que as variantes da enum anterior:
struct MensagemSair; // unit struct
struct MensagemMover {
x: i32,
y: i32,
Mas se usarmos structs diferentes, cada uma tendo seu próprio tipo, não vamos
conseguir
tão facilmente definir uma função que possa receber qualquer um
desses tipos de
mensagens, assim como fizemos com a enum Mensagem , definida
na Listagem 6-2, que
consiste em um tipo único.
Há mais uma similaridade entre enums e structs: da mesma forma como podemos
definir
métodos em structs usando impl , também podemos definir métodos em
enums. Aqui está
um método chamado invocar , que poderia ser definido na nossa
enum Mensagem :
impl Mensagem {
fn invocar(&self) {
let m = Mensagem::Escrever(String::from("olá"));
m.invocar();
O corpo do método usaria o valor self para obter a mensagem sobre a qual o
método foi
chamado. Neste exemplo, criamos a variável m , que contém o valor
Vamos ver agora outra enum da biblioteca padrão que também é muito útil e
comum:
Option .
Na seção anterior, vimos como a enum EnderecoIp nos permite usar o sistema de
tipos do
Rust para codificar em nosso programa mais informação do que apenas os
dados que
queremos representar. Essa seção explora um caso de estudo da
Option , que é outra
enum definida pela biblioteca padrão. O tipo Option é
muito utilizado, pois engloba um
cenário muito comum, em que um valor pode ser
algo ou pode não ser nada. Expressar
esse conceito por meio do sistema de tipos
significa que o compilador pode verificar se você
tratou, ou não, todos os
casos que deveriam ser tratados, podendo evitar bugs que são
extremamente
comuns em outras linguagens de programação.
O design de uma linguagem de programação é geralmente tratado em termos de
quais
características são incluídas, mas as que são excluídas também têm importância. Rust não
tem o valor nulo (null) que outras linguagens têm. O
valor nulo quer dizer que não há
nenhum valor. Em linguagens que têm essa
característica, as variáveis sempre estão em um
dos dois estados: nulo ou não
nulo.
O problema com valores nulos é que, se você tentar usar um valor nulo como se
fosse não
nulo, vai acontecer algum tipo de erro. Pelo fato dessa propriedade
de nulo e não nulo ser
tão sutil, é extremamente fácil cometer esse tipo de
erro.
Porém, o conceito que o valor nulo tenta expressar ainda é útil: um valor nulo
representa
algo que, por algum motivo, está inválido ou ausente no momento.
enum Option<T> {
A enum Option<T> é tão útil que ela já vem inclusa no prelúdio: você não
precisa trazê-la
explicitamente para o seu escopo. Além disso, o mesmo ocorre
com suas variantes: você
pode usar Some e None diretamente sem prefixá-las
com Option:: . Option<T> continua
sendo uma enum como qualquer outra, e
Some(T) e None ainda são variantes do tipo
Option<T> .
Quando temos um Some , sabemos que um valor está presente, contido dentro do
Some . Já
quando temos um None , de certa forma, significa o mesmo que um
valor nulo: não temos
um valor que seja válido. Então por que a Option<T> é
tão melhor que usar um valor nulo?
let x: i8 = 5;
let soma = x + y;
Quando executamos esse código, temos uma mensagem de erro como essa:
not satisfied
-->
5 | let sum = x + y;
Intenso! O que essa mensagem quer dizer é que o Rust não consegue entender como
somar um i8 e um Option<i8> , porque eles são de tipos diferentes. Quando
temos um
valor de um tipo como i8 em Rust, o compilador tem certeza de que
temos sempre um
valor válido. Podemos prosseguir com confiança, sem ter de
verificar se o valor é nulo antes
de usá-lo. Somente quando temos um
Option<i8> (ou qualquer que seja o tipo com que
estamos trabalhando), vamos
ter de nos preocupar com a possibilidade de não haver um
valor, e o compilador
vai se certificar de que nós estamos tratando este caso antes de usar
o valor.
Em geral, pra usar um valor Option<T> , queremos ter um código que trate cada
uma das
variantes. Queremos um código que só será executado quando tivermos um
valor Some(T) ,
e esse código terá permissão para usar o valor T que está
embutido. Queremos também
um outro código que seja executado se tivermos um
valor None , e esse código não terá um
valor T disponível. A expressão
match é uma instrução de controle de fluxo que faz
exatamente isso quando
usada com enums: ela executa códigos diferentes dependendo de
qual variante
tiver a enum, e esse código poderá usar os dados contidos na variante
encontrada.
Imagine que expressão match funciona como uma máquina de contar moedas: as
moedas
passam por um canal que possui furos de vários tamanhos, e cada moeda
cai no primeiro
furo em que ela couber. Da mesma forma, os valores passam por
cada padrão de um
match , e logo no primeiro padrão que o valor "se encaixar",
o bloco de código que estiver
associado a ele será executado.
Aproveitando que acabamos de falar sobre moedas, vamos usá-las como exemplo de
utilização do match ! Podemos escrever uma função que recebe uma moeda
qualquer dos
Estados Unidos e, assim como uma máquina, determina qual moeda ela
é e retorna seu
valor em cents, como mostra a Listagem 6-3:
Nota do tradutor: diferentemente do que acontece na maioria dos países,
as moedas
dos Estados Unidos possuem nomes: as de 1 cent são chamadas de
Penny; as de 5
cents, de Nickel; as de 10 cents, de Dime; e as de 25
cents, de Quarter.
enum Moeda {
Penny,
Nickel,
Dime,
Quarter,
match moeda {
Moeda::Penny => 1,
Moeda::Nickel => 5,
Tipicamente não se usa chaves se o braço do match for curto, como é o caso na
Listagem 6-
3, em que cada braço retorna apenas um valor. Se você quiser
executar mais de uma linha
de código em um braço, você pode usar chaves para
delimitá-las. Por exemplo, o código
seguinte vai escrever na tela "Moeda da
sorte!" sempre que o método for chamado com
uma Moeda::Penny , mas ainda vai
retornar o último valor do bloco, 1 :
match moeda {
Moeda::Penny => {
println!("Moeda da sorte!");
},
Moeda::Nickel => 5,
Outra característica útil dos braços do match é que eles podem ser atrelados
a partes dos
valores que se encaixam no padrão. É assim que podemos extrair
valores dentro de uma
variante de uma enum.
Por exemplo, vamos alterar uma das nossas variantes, inserindo dados dentro
dela. De
1999 até 2008, os Estados Unidos cunhou quarters com um design
diferente para cada um
dos 50 estados em um dos lados da moeda. Nenhuma outra
moeda tinha essa diferença no
design, apenas os quarters. Podemos adicionar
essa informação à nossa enum alterando a
variante Quarter para incluir o
valor Estado , como é feito na Listagem 6-4:
enum Estado {
Alabama,
Alaska,
// ... etc
enum Moeda {
Penny,
Nickel,
Dime,
Quarter(Estado),
Vamos imaginar que um amigo nosso está tentando colecionar todas os quarters
dos 50
estados. Enquanto separamos nosso troco por tipo de moeda, vamos também
dizer o nome
do estado associado a cada quarter. Se for um dos que o nosso
amigo ainda não tem, ele
pode colocá-lo na sua coleção.
Na expressão match desse código, vamos adicionar uma variável chamada
estado ao
padrão que casa com os valores da variante Moeda::Quarter . Quando
uma
Moeda::Quarter é testada, a variável estado vai ser atrelada ao valor
do estado daquele
quarter. Assim vamos poder usar o estado no código do
braço, desse jeito:
match moeda {
Moeda::Penny => 1,
Moeda::Nickel => 5,
Moeda::Quarter(estado) => {
25
},
Essa função é bem fácil de implementar, graças ao match , e vai ficar conforme
visto na
Listagem 6-5:
fn mais_um(x: Option<i32>) -> Option<i32> {
match x {
Casando Some(T)
O valor Some(5) não casa com o padrão None , então seguimos para o
próximo braço.
Some(5) casa com Some(i) ? Sim, casa! Temos a mesma variante. O i está atrelado ao
valor contido em Some , então i passa a ter o valor 5 . O código desse braço é
executado, então somamos um ao valor de i e criamos um novo Some contendo nosso
total de 6`.
Casando None
Confere! Não há nenhum valor para somar, então o programa pára e retorna o
valor None
do lado direito do => . Como o primeiro braço já casou, nenhum
dos demais será testado.
match x {
Nós não tratamos o caso None , logo vai ocorrer um bug no nosso código. Por
sorte, é um
bug que o Rust sabe detectar. Se tentarmos compilar esse código,
vamos ter esse erro:
-->
6 | match x {
O Rust sabe que nós não cobrimos todos os casos possíveis, e sabe até de qual
padrão nos
esquecemos! Matches em Rust são exaustivos: precisamos extinguir
até a última
possibilidade pra que o nosso código seja válido. Especialmente no
caso de uma
Option<T> , em que o Rust não nos deixa esquecer de tratar
explicitamente o caso None .
Ele nos impede de assumir que temos um valor
válido quando possivelmente temos um
valor nulo, e portanto, cometer o erro de
um bilhão de dólares que vimos mais cedo.
The _ Placeholder
O Placeholder _
O Rust também tem um padrão que podemos usar em situações em que não queremos
listar todos os valores possíveis. Por exemplo, um u8 pode ter valores
válidos de 0 a 255.
Se nos importamos apenas com os valores 1, 3, 5 e 7, não
queremos ser obrigados a listar o
0, 2, 4, 6, 8, 9, e assim por diante até 255.
Felizmente, nem precisamos: em vez disso,
podemos usar o padrão especial _ .
match algum_valor_u8 {
1 => println!("um"),
3 => println!("três"),
5 => println!("cinco"),
7 => println!("sete"),
_ => (),
O padrão _ casa com qualquer valor. Colocando ele depois dos demais
braços, o _ vai
casar com todos os casos possíveis que não foram
especificados antes dele. O () é só o
valor-unidade, pra que nada aconteça no
caso _ . Como resultado, podemos dizer que não
queremos fazer nada com os
possíveis valores que não listamos antes do placeholder _ .
Contudo, a expressão match pode ser um tanto verbosa em uma situação em que
queremos apenas lidar com um dos casos. Pra essa situação, o Rust oferece o
if let .
match algum_valor_u8 {
_ => (),
Queremos fazer alguma coisa com o Some(3) , mas não queremos fazer nada com
nenhum
outro valor, seja Some<u8> ou None . Pra satisfazer a expressão
match , temos que colocar
_ => () após processar apenas uma variante, ou
seja, é muito código para pouca coisa.
Em vez disso, poderíamos escrever o mesmo código de uma forma mais compacta,
usando
if let . O código seguinte tem o mesmo comportamento do match na
Listagem 6-6:
println!("três");
Usar o if let implica menos código pra digitar e menos indentação. Porém,
perdemos a
verificação exaustiva que é garantida pelo match . A escolhe entre
match e if let
depende do que você está fazendo em uma situação particular,
e se a redução no volume
de código compensa a perda da verificação exaustiva.
Em outras palavras, você pode enxergar o if let como um syntax sugar (um
atalho) para
um match que só executa um código quando o valor casa com um
único padrão, e ignora
todos os outros valores.
match moeda {
_ => contagem += 1,
} else {
contagem += 1;
Se a lógica do seu programa fica muito verbosa quando é expressa por meio de um
match ,
lembre-se que você também dispõe do if let .
Resumo
Nós acabamos de ver como usar enums para criar tipos customizados a partir de
um
conjunto de valores enumerados. Mostramos como o tipo Option<T> , da
biblioteca padrão,
ajuda você a usar o sistema de tipos para evitar erros.
Quando as enums contêm dados,
você pode usar match ou if let para extrair
e usar esses valores, dependendo de
quantos casos você precisa tratar.
Agora, seus programas em Rust podem expressar conceitos em seu domínio usando
structs
e enums. Criar tipos customizados para a sua API aumenta sua
segurança: o compilador vai
se certificar de que suas funções recebem apenas
os valores que correspondem aos tipos
esperados.
Para fornecer uma API bem organizada aos seus usuários, que seja simples de
usar, e que
exponha apenas o que é necessário aos usuários, vamos agora passar
para os módulos em
Rust.
Da mesma forma que você extrai linhas de código em uma função, você pode extrair
funções (e outros códigos, como structs e enums) em diferentes módulos. Um
módulo é um
namespace que contém definições de funções ou tipos, e
você pode escolher se essas
definições são visíveis fora de seu módulo
(público) ou não (privado). Aqui está uma visão
geral de como os módulos funcionam:
Examinaremos cada uma dessas partes para ver como elas se encaixam no todo.
$ cd communicator
Arquivo: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
Cargo cria um teste de exemplo para nos ajudar a começar nossa biblioteca, em vez de
o
binário “Hello, world!” que recebemos quando usamos a opção --bin . Olharemos
a sintaxe
#[] e mod tests no “Usando super para Acessar um
Módulo Pai” mais adiante neste
capítulo, mas por agora, deixe este código
na parte inferior de src/lib.rs.
Como não temos um arquivo src/main.rs, não há nada para ser executado pelo Cargo
com o
comando cargo run . Portanto, usaremos o comando cargo build
para compilar o código
da nossa biblioteca.
Examinaremos diferentes opções para organizar o código da sua biblioteca que serão
adequados em uma variedade de situações, dependendo da intenção do código.
Definições do Módulo
Arquivo: src/lib.rs
mod network {
fn connect() {
Também podemos ter múltiplos módulos, lado a lado, no mesmo arquivo src/lib.rs.
Por
exemplo, para ter mais um módulo client que possui uma função chamada connect
,
podemos adicioná-lo como mostrado na Listagem 7-1:
Arquivo: src/lib.rs
mod network {
fn connect() {
mod client {
fn connect() {
Nesse caso, como estamos construindo uma biblioteca, o arquivo que serve como
ponto de
entrada para construir nossa biblioteca é src/lib.rs. No entanto, em relação a
criação de
módulos, não há nada de especial sobre src/lib.rs. Poderíamos também
criar módulos em
src/main.rs para um crate binário da mesma forma que nós
criamos módulos em src/lib.rs
para o crate de biblioteca. Na verdade, podemos colocar módulos dentro de módulos, o
que pode ser útil à medida que seus módulos crescem para manter juntas funcionalidades
relacionadas e separar funcionalidades não relacionadas. A
escolha de como você organiza
seu código depende do que você pensa sobre a
relação entre as partes do seu código. Por
exemplo, o código client
e a função connect podem ter mais sentido para os usuários de
nossa biblioteca se
eles estivessem dentro do namespace network , como na Listagem 7-2:
Arquivo: src/lib.rs
mod network {
fn connect() {
mod client {
fn connect() {
communicator
├── network
└── client
communicator
└── network
└── client
Os módulos formam uma estrutura hierárquica, bem parecida com outra estrutura
computacional que você conhece: sistemas de arquivos! Podemos usar o sistema de
módulos do Rust juntamente com
vários arquivos para dividir projetos Rust de forma que
nem tudo resida em
src/lib.rs ou src/main.rs. Para este exemplo, vamos começar com o
código em
Listagem 7-3:
Arquivo: src/lib.rs
mod client {
fn connect() {
mod network {
fn connect() {
mod server {
fn connect() {
communicator
├── client
└── network
└── server
Arquivo: src/lib.rs
mod client;
mod network {
fn connect() {
mod server {
fn connect() {
mod client {
// conteúdo de client.rs
Arquivo: src/client.rs
fn connect() {
Observe que não precisamos de uma declaração mod neste arquivo porque já fizemos
a
declaração do módulo client com mod em src/lib.rs. Este arquivo apenas
fornece o
conteúdo do módulo client . Se colocarmos um mod client aqui,
nós estaríamos dando
ao módulo client seu próprio submódulo chamado client !
Agora, o projeto deve compilar com sucesso, embora você obtenha alguns
warnings
(avisos). Lembre-se de usar cargo build , em vez de cargo run , porque temos
um crate de
biblioteca em vez de um crate binário:
$ cargo build
--> src/client.rs:1:1
1 | / fn connect() {
2 | | }
| |_^
--> src/lib.rs:4:5
4 | / fn connect() {
5 | | }
| |_____^
--> src/lib.rs:8:9
8 | / fn connect() {
9 | | }
| |_________^
Esses warnings nos dizem que temos funções que nunca são usadas. Não se preocupe
com
esses warnings por enquanto; vamos abordá-los mais adiante neste capítulo, na
seção
“Controlando a visibilidade com pub ”. A boa notícia é que eles são apenas
warnings; nosso
projeto foi construído com sucesso!
Em seguida, vamos extrair o módulo network em seu próprio arquivo usando o mesmo
procedimento. Em src/lib.rs, exclua o corpo do módulo network e adicione um
ponto e
vírgula à declaração, assim:
Arquivo: src/lib.rs
mod client;
mod network;
Arquivo: src/network.rs
fn connect() {
mod server {
fn connect() {
Observe que ainda temos uma declaração mod dentro deste arquivo de módulo; isto é
porque ainda queremos que server seja um submódulo de network .
Execute cargo build novamente. Sucesso! Temos mais um módulo para extrair: server .
Como ele é um submódulo - ou seja, um módulo dentro de outro - nossa tática atual de
extrair um módulo para um arquivo com o nome do módulo não funcionará. Iremos
tentar,
de qualquer maneira, para que você possa ver o erro. Primeiro, altere o arquivo
src/network.rs colocando
mod server; no lugar do conteúdo do módulo server :
Arquivo: src/network.rs
fn connect() {
mod server;
Arquivo: src/server.rs
fn connect() {
$ cargo build
--> src/network.rs:4:5
4 | mod server;
| ^^^^^^
note: maybe move this module `src/network.rs` to its own directory via
`src/network/mod.rs`
--> src/network.rs:4:5
4 | mod server;
| ^^^^^^
note: ... or maybe `use` the module `server` instead of possibly redeclaring it
--> src/network.rs:4:5
4 | mod server;
| ^^^^^^
O erro diz que não podemos declarar um novo módulo neste local ( cannot declare a new
module at this location ) e está apontando para a linha mod server ; em src/network.rs.
Então src/network.rs é
diferente de src/lib.rs de alguma forma: continue lendo para entender
o porquê.
A nota no meio da Listagem 7-5 é realmente muito útil, porque ela aponta para algo de que
não falamos ainda:
note: maybe move this module `network` to its own directory via
`network/mod.rs`
(Tradução: talvez mover este módulo network para o seu próprio diretório via
network/mod.rs )
$ mkdir src/network
$ mv src/network.rs src/network/mod.rs
$ mv src/server.rs src/network
Agora, quando tentamos executar cargo build , a compilação funcionará (embora ainda
teremos
avisos). O layout dos nossos módulos ainda é exatamente o mesmo de quando
tínhamos todo o código em src/lib.rs na Listagem 7-3:
communicator
├── client
└── network
└── server
├── src
│ ├── client.rs
│ ├── lib.rs
│ └── network
│ ├── mod.rs
│ └── server.rs
communicator
├── client
└── network
└── client
Essas regras se aplicam de forma recursiva, então, se um módulo chamado foo tiver um
submódulo chamado
bar e bar não possui submódulos, você deve ter os seguintes
arquivos
no seu diretório src:
├── foo
Os módulos devem ser declarados no arquivo do módulo pai usando a palavra-chave mod .
Em seguida, vamos falar sobre a palavra-chave pub e nos livrar dessas warnings!
Controlando a Visibilidade com pub
Resolvemos as mensagens de erro mostradas na Listagem 7-5 movendo o código de
network e
network::server para os arquivos src/network/mod.rs e
src/network/server.rs,
respectivamente. Nesse ponto, cargo build era
capaz de construir nosso projeto, mas
ainda recebemos mensagens de warning sobre as
funções client::connect ,
network::connect , e network::server::connect
não estarem em uso:
--> src/client.rs:1:1
1 | / fn connect() {
2 | | }
| |_^
--> src/network/mod.rs:1:1
1 | / fn connect() {
2 | | }
| |_^
--> src/network/server.rs:1:1
1 | / fn connect() {
2 | | }
| |_^
Então, por que estamos recebendo esses warnings(avisos)? Afinal, estamos construindo
uma biblioteca
com funções que se destinam a ser usadas pelos nossos usuários, não
necessariamente por
nós dentro de nosso próprio projeto, por isso não deveria importar
que essas funções connect
não sejam utilizadas. O ponto de criá-las é que elas serão
usadas por
outro projeto, não o nosso.
Para entender por que esse programa invoca esses warnings(avisos), vamos tentar usar a
biblioteca connect de outro projeto, chamando-a externamente. Para fazer isso,
vamos
criar um crate binário no mesmo diretório que o nosso crate de biblioteca
inserindo um
arquivo src/main.rs que contém esse código:
Arquivo: src/main.rs
fn main() {
communicator::client::connect();
Observe também que, mesmo que estejamos usando um crate externo dentro de um
submódulo do nosso
projeto, o extern crate deve entrar em nosso módulo raiz (então em
src/main.rs
ou src/lib.rs). Então, em nossos submódulos, podemos consultar itens de crates
externos
como se os itens fossem módulos de nível superior.
Agora, nosso crate binário apenas chama a função connect da nossa biblioteca do
módulo
client . No entanto, invocar agora cargo build nos dará um erro
após os warnings:
--> src/main.rs:4:5
4 | communicator::client::connect();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Ah ha! Este erro nos diz que o módulo client é privado, que é o
cerne das advertências. É
também a primeira vez em que nos encontramos com os conceitos de
público e privado no
contexto do Rust. O estado padrão de todos os códigos em
Rust é privado: ninguém mais
tem permissão para usar o código. Se você não usar uma
função privada dentro do seu
programa, como ele é o único código
permitido a usar essa função, Rust irá avisá-lo de que
a função não foi utilizada.
Para dizer ao Rust que torne pública uma função, adicionamos a palavra-chave pub ao
início
da declaração. Nos focaremos em corrigir o warning que indica
client::connect não
foi utilizado por enquanto, assim como o erro module `client` is private ( módulo
`client` é privado ) do nosso crate binário. Modifique src/lib.rs para tornar
o módulo
client público, assim:
Arquivo: src/lib.rs
mod network;
A palavra-chave pub é colocada logo antes do mod . Vamos tentar fazer o build novamente:
--> src/main.rs:4:5
4 | communicator::client::connect();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Opa! Temos um erro diferente! Sim, mensagens diferentes de erro são motivo para
comemorar. O novo erro mostra que que a função connect é privada (function connect is
private), então vamos editar src/client.rs para torná-la pública também:
Arquivo: src/client.rs
pub fn connect() {
--> src/network/mod.rs:1:1
1 | / fn connect() {
2 | | }
| |_^
--> src/network/server.rs:1:1
1 | / fn connect() {
2 | | }
| |_^
Os avisos de código não utilizados nem sempre indicam que um item no seu código precisa
se tornar público: se você não quiser que essas funções façam parte de sua API pública,
warnings de código não utilizado podem alertá-lo de que esses códigos não são mais
necessários,
e que podem ser excluídos com segurança. Eles também podem estar
alertando você para um bug, caso você tivesse apenas
acidentalmente removido todos os
lugares dentro da sua biblioteca onde esta função é
chamada.
Mas neste caso, nós queremos que as outras duas funções façam parte da nossa
API pública
do crate, então vamos marcá-las como pub também para nos livrar dos
warnings
remanescentes. Modifique src/network/mod.rs dessa forma:
Arquivo: src/network/mod.rs
pub fn connect() {
mod server;
--> src/network/mod.rs:1:1
1 | / pub fn connect() {
2 | | }
| |_^
--> src/network/server.rs:1:1
1 | / fn connect() {
2 | | }
| |_^
Arquivo: src/lib.rs
--> src/network/server.rs:1:1
1 | / fn connect() {
2 | | }
| |_^
Regras de Privacidade
1. Se um item for público, ele pode ser acessado através de qualquer um dos seus
módulos pais.
2. Se um item é privado, ele só pode ser acessado por seu módulo pai imediato e
qualquer um dos módulos filhos do pai.
Exemplos de Privacidade
Vejamos mais alguns exemplos de privacidade para obter alguma prática. Crie um novo
projeto de biblioteca e digite o código da Listagem 7-6 no arquivo src/lib.rs desse novo
projeto:
Arquivo: src/lib.rs
mod outermost {
pub fn middle_function() {}
fn middle_secret_function() {}
mod inside {
pub fn inner_function() {}
fn secret_function() {}
fn try_me() {
outermost::middle_function();
outermost::middle_secret_function();
outermost::inside::inner_function();
outermost::inside::secret_function();
O módulo denominado inside é privado e não tem módulos filhos, portanto, ele só pode
ser acessado pelo seu módulo atual outermost . Isso significa que a função try_me
não tem
permissão de chamar outermost::inside::inner_function ou
outermost::inside::secret_function .
Reparando os Erros
Sinta-se livre para projetar mais experimentos que lhe vierem à mente!
Em seguida, vamos falar sobre trazer itens ao escopo com a palavra-chave use .
Arquivo: src/main.rs
pub mod a {
pub mod of {
pub fn nested_modules() {}
fn main() {
a::series::of::nested_modules();
Como você pode ver, referir-se ao nome totalmente qualificado pode ficar bastante longo.
Felizmente, Rust tem uma palavra-chave para tornar estas chamadas mais concisas.
Arquivo: src/main.rs
pub mod a {
pub mod of {
pub fn nested_modules() {}
use a::series::of;
fn main() {
of::nested_modules();
Poderíamos ter escolhido trazer a função para o escopo, em vez de especificar a função
no
use da seguinte forma:
pub mod a {
pub mod of {
pub fn nested_modules() {}
use a::series::of::nested_modules;
fn main() {
nested_modules();
Como as enums também formam uma espécie de namespace, assim como os módulos,
podemos trazer
as variantes de uma enum para o escopo com use também. Para qualquer
tipo de declaração de use
se você estiver trazendo vários itens de um namespace para o
escopo, você pode listá-los
usando chaves e vírgulas na última posição, assim:
enum TrafficLight {
Red,
Yellow,
Green,
fn main() {
Para trazer todos os itens de um namespace para o escopo ao mesmo tempo, podemos usar
a sintaxe * , que é chamada de operador glob. Este exemplo traz todas as variantes de uma
enum ao escopo sem ter que listar cada uma especificamente:
enum TrafficLight {
Red,
Yellow,
Green,
use TrafficLight::*;
fn main() {
O * trará para o escopo todos os itens visíveis no namespace TrafficLight . Você deve
usar globs com moderação: eles são convenientes, mas isso pode
também trazer mais itens
do que se esperava e causar conflitos de nomeação.
Como vimos no início deste capítulo, quando você cria um crate de biblioteca,
o Cargo faz
um módulo tests para você. Vamos ver isso em mais detalhes agora.
No seu projeto
communicator , abra src/lib.rs:
Arquivo: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
O Capítulo 11 explica mais sobre testes, mas algumas partes deste exemplo devem fazer
sentido agora: temos um módulo chamado tests que se situa ao lado de nossos outros
módulos
e contém uma função chamada it_works . Embora existam anotações especiais, o
módulo tests é apenas outro módulo! Então nossa hierarquia de módulos
se parece com
isso:
communicator
├── client
├── network
| └── client
└── tests
Os testes servem para exercitar o código dentro da nossa biblioteca, então vamos tentar
chamar nossa
função client :: connect a partir da função it_works , mesmo que não
verefiquemos nenhuma funcionalidade agora. Isso ainda não funcionará:
Arquivo: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
client::connect();
$ cargo test
--> src/lib.rs:9:9
9 | client::connect();
::client::connect();
Ou, podemos usar super para voltar um módulo na hierarquia a partir de nosso módulo
atual, assim:
super::client::connect();
Essas duas opções não parecem tão diferentes neste exemplo, mas se você estiver
mais
fundo em uma hierarquia de módulos, começar sempre a partir da raiz tornaria
seu código
muito longo. Nesses casos, usar super para ir do módulo atual aos
módulos irmãos é um
bom atalho. Além disso, se você especificou o caminho a partir da
raiz em muitos lugares do
seu código e depois vai reorganizar seus módulos movendo
uma sub-árvore para outro
lugar, você acabaria precisando atualizar o caminho em vários
lugares, o que seria tedioso.
Também seria chato ter que digitar super :: em cada teste, mas você
já viu a ferramenta
para essa solução: use ! A funcionalidade super ::
altera o caminho que você dá para
use , tornando-o relativo ao módulo pai
em vez do módulo raiz.
Arquivo: src/lib.rs
#[cfg(test)]
mod tests {
use super::client;
#[test]
fn it_works() {
client::connect();
$ cargo test
Running target/debug/communicator-92007ddb5330fa5a
running 1 test
Resumo
Agora você conhece algumas técnicas novas para organizar o seu código! Use estas técnicas
para agrupar as funcionalidades relacionadas, evitar que os arquivos tornem-se muito
longos, e
apresentar uma API pública arrumada para os usuários da sua biblioteca.
Coleções Comuns
A biblioteca padrão do Rust inclui uma série de estruturas de dados
chamadas coleções. A
maioria dos tipos de dados representa um valor específico, mas
coleções podem conter
múltiplos valores. Diferente dos tipos embutidos array e tupla,
os dados que essas coleções
apontam estão guardados na heap, que significa
que a quantidade de dados não precisa
ser conhecida em tempo de compilação e pode aumentar ou diminuir conforme a execução
do programa. Cada tipo de coleção possui capacidades diferentes
e custos, e escolher o
apropriada para cada tipo de situação em que se encontra é uma
habilidade que com o
tempo você irá adquirir. Nesse capítulo, veremos três coleções que são usadas
frequentemente em programas Rust:
Para aprender mais sobre outros tipos de coleções fornecidas pela biblioteca padrão,
veja a
documentação.
Nós iremos discutir como criar e atualizar vetores, strings, e hash maps, bem como o que os
tornam especiais.
Vetores
O primeiro tipo que iremos ver é Vec<T> , também conhecido como vetor. Vetores
permitem guardar mais de um valor na mesma estrutura de dados que coloca todos
os
valores um ao lado do outro na memória. Vetores só podem guardar valores do
mesmo
tipo. Eles são úteis em situações onde há uma lista de itens, como
as linha de texto em um
arquivo ou preços de itens em um carrinho de compras.
Note que adicionamos uma anotação de tipo aqui. Como não estamos inserindo nenhum
valor
no vetor, Rust não sabe o tipo de elementos que irá guardar.
Isto é um ponto
importante. Vetores são homogêneos: eles podem guardar muitos valores, mas todos esses
valores devem ser do mesmo tipo. Vetores são implementados
usando genéricos, onde o
capítulo 10 irá cobrir como usar em seus tipos. Por
agora, tudo o que precisa saber é que o
tipo Vec fornecido pela biblioteca
padrão pode conter qualquer tipo, e quando um Vec
específico possui um tipo específico, o
tipo vai dentro de < > . Falamos para Rust que Vec
em v guardará
elementos do tipo i32 .
No código real, a Rust pode inferir o tipo de valor que queremos armazenar uma vez que
inserimos
valores, então você raramente precisa fazer essa anotação de tipo. É mais comum
criar um Vec que possui valores iniciais, e o Rust fornece a macro vec! por
conveniência.
A macro criará um novo Vec que contém os valores que damos
. Isso criará um novo Vec
<i32> que contém os valores 1 , 2 e 3 :
Como nós damos valores iniciais i32 , Rust pode inferir que o tipo de v
é Vec <i32> , e a
anotação de tipo não é necessária. Vejamos a seguir como
modificar um vetor.
Modificando um Vetor
Para criar um vetor e adicionar elementos a ele, podemos usar o método push :
v.push(5);
v.push(6);
v.push(7);
v.push(8);
Como qualquer outro struct , um vetor será liberado quando ele sair do escopo:
// use as informações em v
Quando o vetor é descartado, todos os seus conteúdos também será descartado, o que
significa
esses inteiros que ele contém serão limpos. Isso pode parecer um
ponto direto,
mas pode ficar um pouco mais complicado quando começamos a
introduzir referências aos
elementos do vetor. Vamos abordar isso em seguida!
Agora que você sabe como criar, atualizar e destruir vetores, saber ler o seu conteúdo é um
bom passo seguinte. Existem duas maneiras de fazer referência a
valores armazenados em
um vetor. Nos exemplos, anotamos os tipos de
valores que são retornados dessas funções
para maior clareza.
Há algumas coisas a serem observadas aqui. Primeiro, que usamos o valor do índice de 2
para obter o terceiro elemento: os vetores são indexados por número, começando em zero.
Em segundo lugar, as duas maneiras diferentes de obter o terceiro elemento são: usando &
e
[] , que nos dá uma referência, ou usando o método get com o índice
passado como
um argumento, o que nos dá uma Option<&T> .
A razão pela qual Rust tem duas maneiras de fazer referência a um elemento é para que
você possa escolher
como o programa se comporta quando você tenta usar um valor de
índice para o qual o vetor não tem um elemento correspondente. Por exemplo, o que um
programa deve fazer se tiver
um vetor que contém cinco elementos, então tenta acessar
um elemento no índice 100
dessa maneira:
Quando você executar isso, você verá que com o primeiro método [] , Rust irá
causar um
panic! quando um elemento inexistente é referenciado. Este método seria
preferível se
você quiser que seu programa considere uma tentativa de acessar um
elemento, passado o
fim do vetor, para ser um erro fatal que deve finalizar o
programa.
Quando é passado um índice que está fora da matriz para o método get , ele retorna None
sem entrar em pânico. Você usaria isso se acessar um elemento
além do alcance do vetor
ocorrerá ocasionalmente sob circunstâncias normais. Seu código pode então ter lógica para
lidar tanto com
Some(&element) ou None , como discutimos no Capítulo 6. Por exemplo, o
O índice pode ser proveniente de uma pessoa que digite um número. Se eles
acidentalmente
insira um número que é muito grande e seu programa recebe um valor
None , você poderia
dizer ao usuário quantos itens estão no atual Vec e dar uma nova
chance de inserir um valor válido. Isso seria mais amigável do que quebrar o
programa por
um erro de digitação!
Referências Inválidas
Uma vez que o programa tenha uma referência válida, o verificador de empréstimo (borrow
checker) faz valer
as regras de propriedade e empréstimo abrangidas no Capítulo 4 para
garantir que essa referência e
quaisquer outras referências aos conteúdos do vetor
permaneçam válidas. Lembre-se da regra
que diz que não podemos ter referências
mutáveis e imutáveis no mesmo escopo.
Essa regra se aplica neste exemplo, onde
mantemos uma referência imutável ao
primeiro elemento em um vetor e tentamos
adicionar um elemento ao final:
v.push(6);
immutable
5 |
6 | v.push(6);
7 | }
Este código pode parecer que deveria funcionar: por que uma referência ao primeiro
elemento deveria se preocupar com o que muda sobre o final do vetor? A razão porque
este
código não é permitido é devido à forma como os vetores funcionam. Adicionando um novo
elemento
no final do vetor pode exigir a atribuição de nova alocação de memória e copiar
os
elementos antigos para o novo espaço, na circunstância de não haver espaço suficiente
para colocar todos os elementos próximos um do outro onde o vetor estava. Nesse
caso, a
referência ao primeiro elemento apontaria para memória não alocada.
As regras de
empréstimo impedem que os programas acabem nessa situação.
Por exemplo, digamos que queremos obter valores de uma linha em uma planilha, onde
algumas das colunas da linha contêm números inteiros, alguns números de ponto flutuante,
e algumas strings. Podemos definir um enum cujas variantes guardarão os diferentes
tipos
de valor, e então todas as variantes de enum serão consideradas do mesmos
tipo, o do
enum. Então, podemos criar um vetor que contenha esse enum e
então, em última
instância, possui diferentes tipos:
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
A razão pela qual Rust precisa saber exatamente quais tipos estarão no vetor em
tempo de
compilação é para que ele saiba exatamente a quantidade de memória no heap que será
necessária para armazenar cada elemento. Uma vantagem secundária para isso é que
podemos ser
explícitos sobre quais tipos são permitidos neste vetor. Se Rust permitisse um
vetor
guardar qualquer tipo, haveria uma chance de que um ou mais dos tipos
causar erros
com as operações realizadas nos elementos do vetor. Usando
um enum mais um match
significa que a Rust garantirá no tempo de compilação que nós
sempre lidaremos com
todos os casos possíveis, como discutimos no Capítulo 6.
Se você não sabe no momento em que você está escrevendo um programa, o conjunto
exaustivo
dos tipos que o programa irá precisar no tempo de execução para armazenar em
um vetor, a técnica de usar
o enum não funcionará. Em vez disso, você pode usar um objeto
trait, que abordaremos no
Capítulo 17.
Agora que examinamos algumas das maneiras mais comuns de usar vetores, certifique-se
para dar uma olhada na documentação da API para todos os muitos métodos úteis
definidos no Vec pela biblioteca padrão. Por exemplo, além de push
existe um método
pop que irá remover e retornar o último elemento. Vamos mover
para o próximo tipo de
coleção: String !
Strings
Nós já conversamos sobre as strings no capítulo 4, mas vamos dar uma olhada mais
em
profundidade agora. As strings são uma área que os novos Rustáceos geralmente tem
maior
dificuldade. Isto é devido a uma combinação de três coisas: a propensão de Rust de
certificar-se de expor possíveis erros, as strings são estruturas de dados mais complicadas
que muitos programadores lhes dão crédito, e UTF-8. Essas coisas
combina de tal forma
que parecem difícil quando se vem de outras linguagens.
A razão pela qual as strings estão no capítulo de coleções é que as strings são
implementadas como uma coleção de bytes mais alguns métodos para fornecer
informações úteis e
funcionalidade quando esses bytes são interpretados como texto.
Nesta seção, iremos
falar sobre as operações em String que todo tipo de coleção tem,
como
criar, atualizar e ler. Também discutiremos as formas em que String
é diferente das
outras coleções, a saber, como a indexação em um String é
complicada pelas diferenças
entre como as pessoas e os computadores interpretam
dados String .
O que é String?
Antes de podermos explorar esses aspectos, precisamos falar sobre o que exatamente
significa o termo string. Rust realmente só tem um tipo de string no núcleo
da própria
linguagem: str , a fatia de string, que geralmente é vista na forma emprestada
, &str . Nós
falamos sobre fatias de strings no Capítulo 4: estas são uma
referência a alguns dados de
string codificados em UTF-8 armazenados em outro lugar. Literais de strings,
por exemplo,
são armazenados na saída binária do programa e, portanto, são
fatias de string.
A biblioteca padrão do Rust também inclui uma série de outros tipos de string, como
Muitas das mesmas operações disponíveis com Vec também estão disponíveis em String ,
começando com a função new para criar uma string, assim:
Isso cria uma nova string vazia chamada s na qual podemos carregar dados.
Muitas vezes, teremos alguns dados iniciais que gostaríamos de já colocar na string. Para
isso, usamos o método to_string , que está disponível em qualquer tipo
que implementa a
trait Display , como as strings literais:
let s = data.to_string();
Também podemos usar a função String :: from para criar uma String de uma string
literal. Isso equivale a usar to_string :
Como as strings são usadas para tantas coisas, existem várias APIs genéricas diferentes
que
podem ser usadas para strings, então há muitas opções. Algumas delas
podem parecer
redundantes, mas todas têm seu lugar! Nesse caso, String :: from
e .to_string acabam
fazendo exatamente o mesmo, então a que você escolher é uma
questão de estilo.
Lembre-se de que as string são codificadas em UTF-8, para que possamos incluir qualquer
dados apropriadamente codificados
neles:
let hello = "
;"السالم عليكم
let hello = "Dobrý den";
Uma String pode crescer em tamanho e seu conteúdo pode mudar assim como o
conteúdo
de um Vec , empurrando mais dados para ela. Além disso, String tem
operações de concatenação implementadas com o operador + por conveniência.
Podemos criar uma String usando o método push_str para adicionar uma seqüência de
caracteres:
s.push_str("bar");
let s2 = String::from("bar");
s1.push_str(&s2);
O método push é definido para ter um único caractere como parâmetro e adicionar
à
String :
s.push('l');
Muitas vezes, queremos combinar duas strings existentes. Uma maneira é usar
o operador
+ dessa forma:
let s2 = String::from("world!");
let s3 = s1 + &s2; // Note que s1 foi movido aqui e não pode ser mais usado
Antes de tudo, s2 tem um & , o que significa que estamos adicionando uma referência da
segunda string para a primeira string. Isso é devido ao parâmetro s na
função add : só
podemos adicionar um &str à String , não podemos adicionar dois
valores String
juntos. Mas espere - o tipo de &s2 é &String , não
&str , conforme especificado no
segundo parâmetro para add . Por que nosso exemplo
compila? Podemos usar &s2 na
chamada para add porque um &String
o argumento pode ser coerced em um &str -
quando a função add é chamada,
Rust usa algo chamado de deref coercion, o que você
poderia pensar aqui como
virando &s2 para &s2[..] para uso na função add . Vamos
discutir deref coercion em maior profundidade no Capítulo 15. Como o add não se
apropria
o parâmetro s2 ainda será uma String válida após essa operação.
Em segundo lugar, podemos ver na assinatura que add toma posse de self ,
porque
self não tem & . Isso significa s1 no exemplo acima
será transferido para a chamada add
e não será mais válido depois disso. Por enquanto
let s3 = s1 + &s2; parece que irá
copiar ambas as strings e criar uma nova,
esta declaração realmente adere a s1 ,
acrescenta uma cópia do conteúdo
de s2 , então retorna ownership do resultado. Em
outras palavras, parece
estar fazendo muitas cópias, mas não é: a implementação é mais
eficiente
do que copiar.
let s2 = String::from("tac");
let s3 = String::from("toe");
s será “tic-tac-toe” neste momento. Com todos os + e " , fica difícil ver o que está
acontecendo. Para strings mais complicadas
, podemos usar o macro format! :
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
Indexação em Strings
let s1 = String::from("hello");
let h = s1[0];
|>
|> ^^^^^
O erro e a nota contam a história: as strings em Rust não suportam a indexação. Assim
a
próxima pergunta é, por que não? Para responder a isso, temos que conversar um
pouco
sobre como o Rust armazena strings na memória.
Representação Interna
Uma String é um invólucro sobre um Vec <u8> . Vejamos alguns dos nossos
exemplos
UTF-8, codificadas corretamente, de strings vistas anteriormente. Primeiro, este:
Neste caso, len terá valor de quatro, o que significa que o Vec armazena a string
”Hola”
tem quatro bytes de comprimento: cada uma dessas letras leva um byte quando codificado
em
UTF-8. E o que acontece para esse exemplo?
Uma pessoa que pergunte pelo comprimento da string pode dizer que ela deva ter 12.No
entanto, a resposta de Rust
é 24. Este é o número de bytes que é necessário para codificar
“Здравствуйте“ em
UTF-8, uma vez que cada valor escalar Unicode leva dois bytes de
armazenamento. Assim sendo,
um índice nos bytes da string nem sempre se correlaciona
com um valor escalar Unicode válido.
Isso leva a outro ponto sobre UTF-8: existem realmente três maneiras relevantes
de olhar
para as strings, da perspectiva do Rust: como bytes, valores escalares e
clusters de
grafemas (a coisa mais próxima do que as pessoas chamariam letras).
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
Existem seis valores char aqui, mas o quarto e o sexto não são letras,
Eles são diacríticos
que não fazem sentido por conta própria. Finalmente, se olharmos para
eles como clusters
de grafemas, teríamos o que uma pessoa chamaria as quatro letras
que compõem esta
palavra:
Rust fornece diferentes maneiras de interpretar os dados de uma string bruta que os
computadores
armazenem para que cada programa possa escolher a interpretação que
necessite, não importa
em que idioma humano os dados estão.
Uma razão final do Rust não permitir que você indexe uma String para obter um
caracter
é que as operações de indexação sempre esperam um tempo constante
(O(1)). Não é
possível garantir que o desempenho com uma String ,
entretanto, já que o Rust teria que
percorrer todo o conteúdo desde o início
até o índice para determinar quantos caracteres
válidos havia.
Fatiando Strings
Porque não está claro qual seria o tipo de retorno da indexação de string, e
muitas vezes é
uma má idéia indexar uma string, Rust dissuade-o de fazê-lo
pedindo que você seja mais
específico se você realmente precisar disso. Do jeito que você pode ser
mais específico que
a indexação usando [] com um único número é usando [] com
um intervalo para criar
uma fatia de string contendo bytes específicos:
let s = &hello[0..4];
Você deve usar isso com cautela, pois isso pode fazer com que seu programa falhe.
for c in "नमस्ते".chars() {
println!("{}", c);
O método bytes retorna cada byte bruto, que pode ser apropriado para o seu
domínio:
for b in "नमस्ते".bytes() {
println!("{}", b);
Este código imprimirá os 18 bytes que compõem esse String , começando por:
224
164
168
224
// ... etc
Mas lembre-se de que os valores escalares Unicode válidos podem ser constituídos por
mais de um byte.
Hash Maps
A última das nossas coleções comuns é o hash map. O tipo HashMap <K, V>
armazena um
mapeamento de chaves do tipo K para valores do tipo V . Ele faz isso através de um
hashing function, que determina como ele coloca essas chaves e valores em
memória.
Muitas linguagens de programação diferentes suportam este tipo de estrutura de dados,
mas muitas vezes com um nome diferente: hash, map, object, hash table ou
associative
array, apenas para citar alguns.
Os Hash maps são úteis para quando você deseja poder procurar dados sem uso de
índice,
como você pode com vetores, mas usando uma chave que pode ser de qualquer tipo. Por
exemplo, em um jogo, você poderia acompanhar a pontuação de cada equipe em um hash
map
onde cada chave é o nome de uma equipe e os valores são cada pontuação da equipe.
Dado um
nome da equipe, você pode recuperar sua pontuação.
Examinaremos a API básica dos hash map neste capítulo, mas há muitos
mais coisas
escondidas nas funções definidas no HashMap pela biblioteca
padrão. Como sempre,
verifique a documentação da biblioteca padrão para mais
informação.
Podemos criar um HashMap vazio com new , e adicionar elementos com insert .
Aqui,
estamos acompanhando as pontuações de duas equipes cujos nomes são Blue e
Yellow. A
equipe blue começará com 10 pontos e a equipe yellow começa com
50:
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Assim como os vetores, os mapas hash armazenam seus dados no heap. Este HashMap tem
chaves do tipo String e valores do tipo i32 . Como vetores, os hash maps são
homogêneos: todas as chaves devem ter o mesmo tipo e todos os valores
devem ter o
mesmo tipo.
use std::collections::HashMap;
Para os tipos que implementam a Copy trait, como i32 , os valores são copiados
no hash
map. Para valores owned como String , os valores serão movidos e
o hash map será o
owner desses valores:
use std::collections::HashMap;
map.insert(field_name, field_value);
Podemos obter um valor do hash map fornecendo a chave para o método get :
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Podemos iterar sobre cada par chave/valor em um hash map de uma maneira similar à que
fazemos com vetores, usando um loop for :
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
Yellow: 50
Blue: 10
Embora o número de chaves e valores sejam crescentes, cada chave individual pode apenas
tem um valor associado a ele por vez. Quando queremos mudar os dados em
um hash
map, temos que decidir como lidar com o caso quando uma chave já possui uma
valor
atribuído. Poderíamos optar por substituir o valor antigo pelo novo valor,
desconsiderando
completamente o valor antigo. Poderíamos escolher manter o valor antigo
e ignorar o novo
valor, e apenas adicione o novo valor se a chave ainda não
tem um valor. Ou podemos
combinar o valor antigo ao valor novo.
Vejamos como fazer cada um desses!
Sobrescrevendo um Valor
Se inserimos uma chave e um valor em um hash map, então se inserir essa mesma chave
com
um valor diferente, o valor associado a essa chave será substituído. Eembora o
seguinte código chame insert duas vezes, o hash map só conterá
um par de chave/valor
porque inserimos o valor da chave da equipe Blue
ambas as vezes:
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
use std::collections::HashMap;
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);
Este código imprimirá {"Yellow": 50, "Blue": 10} . A primeira chamada para entry
irá
inserir a chave para a equipe Yellow com o valor 50, uma vez que o time Yellow
já não
possua um valor. A segunda chamada para entry não vai mudar
o hash map pois o time
Blue já possui o valor 10.
Outro caso de uso comum para hash maps é procurar o valor de uma chave e, em seguida,
atualiza-la
, com base no valor antigo. Por exemplo, se quisermos contar quantas vezes
cada palavra apareceu em algum texto, podemos usar um hash map com as palavras como
chaves
e incrementar o valor para acompanhar quantas vezes vimos essa palavra.
Se esta é
a primeira vez que vimos uma palavra, primeiro inseriremos o valor 0 .
use std::collections::HashMap;
*count += 1;
println!("{:?}", map);
Funções Hashing
Por padrão, HashMap usa uma função de hashing criptográficamente segura que pode
fornecer resistência aos ataques de Negação de Serviço (DoS). Este não é o algoritmo mais
rápido de hashing por aí, mas a compensação por uma melhor segurança que vem
com a
queda na performance vale a pena. Se você testar a velocidade do seu código e encontrar
que a função de hash padrão é muito lenta para seus propósitos, você pode mudar para
outra função especificando um hasher diferente. Um hasher é um tipo que
implementa a
trait BuildHasher . Vamos falar sobre traits e como
implementá-los no Capítulo 10. Você
não precisa necessariamente implementar o seu próprio
hasher do zero; crates.io tem
bibliotecas de hashers de uso comum que outras pessoas compartilharam lá.
Sumário
Vetores, strings e hash maps irão levá-lo longe em programas onde você precisa
armazenar,
acessar e modificar dados. Aqui estão alguns exercícios que você deve estar
capacitado
para resolver:
A documentação da API da biblioteca padrão descreve métodos que esses tipos possuem
que será útil para esses exercícios!
Estamos entrando em programas mais complexos onde as operações podem falhar, o que
significa
que é um momento perfeito para passar pelo tratamento de erros em seguida!
Tratamento de Erros
O comprometimento de Rust à segurança se extende ao tratamento de erros. Erros
são um
fato da vida em software, portanto Rust possui um número de features
para lidar com
situações em que algo dá errado. Em vários casos, Rust requer que
você reconheça a
possibilidade de um erro acontecer e aja preventivamente antes
que seu código compile.
Esse requisito torna seu programa mais robusto ao assegurar
que voce irá descobrir erros e
lidar com eles apropriadamente antes de mandar seu
código para produção!
A maioria das linguagens não distingue esses dois tipos de erros e lida
com ambos da
mesma maneira usando mecanismos como exceções. Rust não tem
exceções. Em vez disso,
ele tem o valor Result<T, E> para erros recuperáveis
e a macro panic! que para a
execução ao encontrar um erro irrecuperável. Esse
capítulo cobre primeiro como chamar
panic! e depois fala sobre retornar valores
Result<T, E> . Adicionalmente, vamos
explorar o que se levar em consideração
para decidir entre tentar se recuperar de um erro
ou parar execução.
[profile.release]
panic = 'abort'
Arquivo: src/main.rs
fn main() {
panic!("Quebra tudo");
$ cargo run
Running `target/debug/panic`
A chamada a panic! causa a mensagem de erro contida nas últimas três linhas.
A primeira
linha mostra nossa mensagem de pânico e a posição no código fonte
em que ocorreu o
pânico: src/main.rs:2 indica que é a segunda linha do nosso arquivo src/main.rs.
Nesse caso, a linha indicada é parte do nosso código, e se formos àquela linha
veremos a
chamada à macro panic! . Em outros casos, a chamada a panic! pode
estar em código
que nosso código chama. O nome do arquivo e número de linha
reportado pela mensagem
de erro será no código de outra pessoa quando a macro panic! for chamada, não a linha
do nosso código que eventualmente levou a chamada
de panic! . Podemos usar o
backtrace das funções de onde veio a chamada a panic!
para entender qual parte de
nosso código está causando o problema. Vamos discutir
o que é um backtrace em seguida.
Usando um Backtrace de panic!
Vamos ver outro exemplo para ver o que acontece quando uma chamada panic! vem de
uma
biblioteca por causa de um bug no nosso código em vez de nosso código chamar
a
macro diretamente. A Listagem 9-1 tem código que tenta acessar um elemento em
um
vetor por meio de um índice:
Arquivo: src/main.rs
fn main() {
v[99];
Aqui, estamos tentando acessar o centésimo elemento (centésimo pois o índice começa em
zero) de nosso vetor, mas ele só tem três elementos. Nesse caso, Rust
entrará em pânico.
Supostamente [] devolve um elemento, mas se você passa um
índice inválido, não há
elemento que Rust possa retornar que fosse correto.
Outras linguagens, como C, vão tentar te dar exatamente o que você pediu nessa
situação,
mesmo que não seja o que você quer: você vai receber o que quer que esteja na localização
na memória que corresponderia àquele elemento no vetor,
mesmo que a memória não
pertença ao vetor. Isso se chama um buffer overread e pode levar a vulnerabilidades de
segurança se um agressor for capaz de manipular
o índice de forma a ler dados guardados
depois do array aos quais ele não deveria
ter acesso.
Para proteger seu programa desse tipo de vulnerabilidade, se você tentar ler
um elemento
em um índice que não exista, Rust vai parar a execução e se recusar
a continar. Vamos fazer
isso e ver o que acontece:
$ cargo run
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
100', /stable-dist-rustc/build/src/libcollections/vec.rs:1362
A próxima linha nos diz que podemos definir a variável de ambiente RUST_BACKTRACE
para
ter um backtrace (rastro) do que aconteceu, exatamente, para causar o erro. Um backtrace é
uma lista de todas as funções que foram chamadas para chegar a esse
ponto. Backtraces
em Rust funcionam como em outras linguagens: a chave para ler
o backtrace é começar do
topo e ler até você ver os arquivos que você escreveu.
Esse é o ponto em que o problema se
originou. As linhas acima das que mencionam seu
código são funções que você chamou; as
linhas abaixo são funções que chamaram seu
código. Essas linhas podem incluir código do
núcleo do Rust, código das bibliotecas
padrão, ou crates que você está usando. Vamos
tentar ver um backtrace: a Listagem 9-2
mostra uma saída semelhante a o que você verá:
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
100', /stable-dist-rustc/build/src/libcollections/vec.rs:1392
stack backtrace:
1: 0x560ed90ec04c -
std::sys::imp::backtrace::tracing::imp::write::hf33ae72d0baa11ed
at /stable-dist-
rustc/build/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:42
2: 0x560ed90ee03e - std::panicking::default_hook::
{{closure}}::h59672b733cc6a455
at /stable-dist-rustc/build/src/libstd/panicking.rs:351
3: 0x560ed90edc44 - std::panicking::default_hook::h1670459d2f3f8843
at /stable-dist-rustc/build/src/libstd/panicking.rs:367
4: 0x560ed90ee41b -
std::panicking::rust_panic_with_hook::hcf0ddb069e7abcd7
at /stable-dist-rustc/build/src/libstd/panicking.rs:555
5: 0x560ed90ee2b4 - std::panicking::begin_panic::hd6eb68e27bdf6140
at /stable-dist-rustc/build/src/libstd/panicking.rs:517
6: 0x560ed90ee1d9 - std::panicking::begin_panic_fmt::abcd5965948b877f8
at /stable-dist-rustc/build/src/libstd/panicking.rs:501
7: 0x560ed90ee167 - rust_begin_unwind
at /stable-dist-rustc/build/src/libstd/panicking.rs:477
8: 0x560ed911401d - core::panicking::panic_fmt::hc0f6d7b2c300cdd9
at /stable-dist-rustc/build/src/libcore/panicking.rs:69
9: 0x560ed9113fc8 -
core::panicking::panic_bounds_check::h02a4af86d01b3e96
at /stable-dist-rustc/build/src/libcore/panicking.rs:56
at /stable-dist-
rustc/build/src/libcollections/vec.rs:1392
at /home/you/projects/panic/src/main.rs:4
at /stable-dist-
rustc/build/src/libpanic_unwind/lib.rs:98
at /stable-dist-rustc/build/src/libstd/panicking.rs:436
at /stable-dist-rustc/build/src/libstd/panic.rs:361
at /stable-dist-rustc/build/src/libstd/rt.rs:57
Isso é bastante saída! A saída exata que você recebe pode ser diferente dependendo
do seu
sistema operacional e versão de Rust. Para conseguir backtraces com essa informação,
símbolos de debug devem estar ativados. Símbolos de debug estão ativados
por padrão
quando usamos cargo build ou cargo run sem a opção de --release, como temos aqui.
Ok(T),
Err(E),
Vamos chamar uma função que retorna um valor Result porque a função poderia
falhar:
na Listagem 9-3 tentamos abrir um arquivo:
Arquivo: src/main.rs
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
--> src/main.rs:4:18
`std::result::Result`
Esse tipo de retorno significa que a chamada a File::open pode dar certo
e retornar para
nós um handle de arquivo que podemos usar pra ler ou escrever
nele. Essa chamada de
função pode também falhar: por exemplo, o arquivo pode não
existir ou talvez não
tenhamos permissão para acessar o arquivo. A função File::open
precisa ter uma
maneira de nos dizer se ela teve sucesso ou falhou e ao mesmo tempo
nos dar ou o handle
de arquivo ou informação sobre o erro. Essa informação é exatamente o que o enum
Result comunica.
No caso em que File::open tem sucesso, o valor na variável f será uma instância
de Ok
que contém um handle de arquivo. No caso em que ela falha, o valor em f
será uma
instância de Err que contém mais informação sobre o tipo de erro que
aconteceu.
Devemos fazer com que o código na Listagem 9-3 faça diferentes ações dependendo
do
valor retornado por File::open . A Listagem 9-4 mostra uma maneira de lidar
com o
Result usando uma ferramenta básica: a expressão match que discutimos
no Capítulo 6.
Arquivo: src/main.rs
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Err(error) => {
},
};
Note que, como no enum Option , o enum Result e suas variantes foram importadas
no
prelúdio, então não precisamos especificar Result:: antes das variantes Ok e Err nas
linhas de match .
Aqui dizemos ao Rust que quando o resultado é Ok ele deve retornar o valor
interno file
de dentro da variante Ok e nós então podemos atribuir este
valor de handle de arquivo à
variável f . Depois do match , nós podemos então
usar o handle de arquivo para ler ou
escrever.
Como sempre, essa saída nos diz exatamente o que aconteceu de errado.
Usando match com Diferentes Erros
O código na Listagem 9-4 chamará panic! não importa a razão pra File::open
ter
falhado. O que queremos fazer em vez disso é tomar diferentes ações para diferentes
motivos de falha: se File::open falhou porque o arquivo não existe, nós queremos criar
um arquivo e retornar o handle para ele. Se File::open
falhou por qualquer outra razão,
por exemplo porque não temos a permissão para
abrir o arquivo, nós ainda queremos
chamar panic! da mesma maneira que fizemos
na Listagem 9-4. Veja a Listagem 9-5, que
adiciona outra linha ao match :
Arquivo: src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
match File::create("hello.txt") {
Err(e) => {
panic!(
},
},
Err(error) => {
panic!(
error
},
};
Usar match funciona bem o suficiente, mas pode ser um pouco verboso e nem
sempre
comunica tão bem a intenção. O tipo Result<T, E> tem vários métodos auxiliares
definidos para fazer diversas tarefas. Um desses métodos, chamado
unwrap , é um método
de atalho que é implementado justamente como o match que
escrevemos na Listagem 9-4.
Se o valor de Result for da variante Ok , unwrap
vai retornar o valor dentro de Ok . Se o
Result for da variante Err , unwrap
vai chamar a macro panic! . Aqui um exemplo de
unwrap em ação:
Arquivo: src/main.rs
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
Se rodarmos esse código sem um arquivo hello.txt, veremos uma mensagem de erro
da
chamada de panic! que o método unwrap faz:
/stable-dist-rustc/build/src/libcore/result.rs:868
Outro método, expect , que é semelhante a unwrap , nos deixa também escolher
a
mensagem de erro do panic! . Usar expect em vez de unwrap e fornecer
boas mensagens
de erros podem transmitir sua intenção e tornar a procura pela
fonte de pânico mais fácil. A
sintaxe de expect é a seguinte:
Arquivo: src/main.rs
use std::fs::File;
fn main() {
Nós usamos expect da mesma maneira que unwrap : para retornar o handle de arquivo
ou
chamar a macro de panic! . A mensagem de erro usada por expect na sua chamada
de
panic! será o parâmtero que passamos para expect em vez da mensagem padrão
que o
unwrap usa. Aqui está como ela aparece:
/stable-dist-rustc/build/src/libcore/result.rs:868
Como essa mensagem de erro começa com o texto que especificamos, Falhou ao abrir
hello.txt , será mais fácil encontrar o trecho do código de onde vem essa mensagem de
erro. Se usamos unwrap em diversos lugares, pode tomar mais tempo encontrar
exatamente qual dos unwrap está causando o pânico, dado que todas as chamadas
a
unwrap chamam o print de pânico com a mesma mensagem.
Propagando Erros
Quando você está escrevendo uma função cuja implementação chama algo que pode
falhar, em vez de tratar o erro dentro dessa função, você pode retornar o
erro ao código
que a chamou de forma que ele possa decidir o que fazer. Isso é
conhecido como propagar
o erro e dá mais controle ao código que chamou sua
função, onde talvez haja mais
informação sobre como tratar o erro
do que você tem disponível no contexto do seu
código.
Por exemplo, a Listagem 9-6 mostra uma função que lê um nome de usuário de um arquivo.
Se o arquivo não existe ou não pode ser lido, essa função vai retornar esses erros
ao código
que chamou essa função:
Arquivo: src/main.rs
use std::io;
use std::io::Read;
use std::fs::File;
let f = File::open("hello.txt");
};
match f.read_to_string(&mut s) {
Listagem 9-6: Uma função que retorna erros ao código que a chamou
usando match
read_to_string também retorna um Result porque ele pode falhar, mesmo que
File::open teve sucesso. Então precisamos de outro match para tratar esse
Result : se
read_to_string teve sucesso, então nossa função teve sucesso, e nós
retornamos o nome
de usuário lido do arquivo que está agora em s , encapsulado em um Ok .
Se
read_to_string falhou, retornamos o valor de erro da mesma maneira que retornamos
o
valor de erro no match que tratou o valor de retorno de File::open .
No entanto, não
precisamos explicitamente escrever return , porque essa já é a última expressão na função.
O código que chama nossa função vai então receber ou um valor Ok que
contém um nome
de usuário ou um valor de Err que contém um io::Error . Nós
não sabemos o que o
código que chamou nossa função fará com esses valores. Se o código que chamou recebe
um valor de Err , ele poderia chamar panic! e causar
um crash, usar um nome de usuário
padrão, ou procurar o nome de usuário em outro
lugar que não um arquivo, por exemplo.
Nós não temos informação o suficiente sobre
o que o código que chamou está de fato
tentando fazer, então propagamos toda a informação de sucesso ou erro para cima para
que ele a trate apropriadamente.
Esse padrão de propagação de erros é tão comum em Rust que a linguagem disponibiliza
o
operador de interrogação ? para tornar isso mais fácil.
Arquivo: src/main.rs
use std::io;
use std::io::Read;
use std::fs::File;
f.read_to_string(&mut s)?;
Ok(s)
Arquivo: src/main.rs
use std::io;
use std::io::Read;
use std::fs::File;
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
Vamos ver o que ocorre quando usamos ? na função main , que como vimos, tem
um tipo
de retorno de () :
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
error[E0277]: the `?` operator can only be used in a function that returns
--> src/main.rs:4:13
4 | let f = File::open("hello.txt")?;
| ------------------------
| |
Em algumas situações é mais apropriado escrever código que entra em pânico em vez
de
retornar um Result , mas eles são menos comuns. Vamos explorar porque é apropriado
entrar em pânico em alguns exemplos, protótipos de código e testes; depois situações
em
que você como humano pode saber que um método não vai falhar, mas que o compilador
não
tem como saber; e concluir com algumas diretrizes sobre como decidir entrar ou
não
em pânico em código de biblioteca.
Quando você está escrevendo um exemplo para ilustrar algum conceito, ter código
de
tratamento de erro robusto junto do exemplo pode torná-lo menos claro. Em exemplos,
é
compreensível que uma chamada a um método como unwrap que poderia chamar panic!
apenas substitua a maneira como você trataria erros na sua aplicação,
que pode ser
diferente baseado no que o resto do seu código está fazendo.
Se uma chamada de método falha em um teste, queremos que o teste inteiro falhe,
mesmo
se esse método não é a funcionalidade sendo testada. Como panic! é o modo
que um
teste é marcado como falha, chamar unwrap ou expect é exatamente o que
deveria
acontecer.
Seria também apropriado chamar unwrap quando você tem outra lógica que
assegura que
o Result vai ter um valor Ok , mas essa lógica não é algo
que o compilador entenda. Você
ainda vai ter um valor de Result que precisa lidar: seja qual for a operação que você está
chamando, ela ainda tem uma possibilidade
de falhar em geral, mesmo que seja
logicamente impossível que isso ocorra nessa situação particular. Se você consegue
assegurar ao inspecionar manualmente o código que
você nunca tera uma variante Err , é
perfeitamente aceitável chamar unwrap .
Aqui temos um exemplo:
use std::net::IpAddr;
Nós estamos criando uma instância IpAddr ao analisar uma string hardcoded. Nós
podemos ver que 127.0.0.1 é um endereço de IP válido, então é aceitável usar unwrap
aqui. No entanto, ter uma string válida hardcoded não muda o tipo retornado
pelo método
parse : ainda teremos um valor de Result , e o compilador ainda vai nos fazer tratar o
Result como se a variante Err fosse uma
possibilidade, porque o compilador não é
inteligente o bastante para ver que essa string
é sempre um endereço IP válido. Se a string
de endereço IP viesse de um usuário ao invés
de ser hardcoded no programa, e portanto, de
fato tivesse uma possibilidade de falha, nós
definitivamente iríamos querer tratar o Result
de uma forma mais robusta.
É aconselhável fazer que seu código entre em panic! quando é possível que
ele entre em
um mau estado. Nesse contexto, mau estado é quando
alguma hipótese, garantia, contrato
ou invariante foi quebrada, tal como
valores inválidos, valores contraditórios, ou valores
faltando que são passados
a seu código - além de um ou mais dos seguintes:
Se alguém chama seu código e passa valores que não fazem sentido, a melhor escolha
talvez seja entrar em panic! e alertar a pessoa usando sua biblioteca do bug no
código
dela para que ela possa consertá-la durante o desenvolvimento. Similarmente,
panic! é
em geral apropriado se você está chamando código externo que está fora
do seu controle e
ele retorna um estado inválido que você não tem como consertar.
Quando se chega a um mau estado, mas isso é esperado que aconteça não importa
quão
bem você escreva seu código, ainda é mais apropriado retornar um Result
a fazer uma
chamada a panic! . Um exemplo disso é um parser recebendo dados
malformados ou uma
requisição HTTP retornando um status que indique que você atingiu
um limite de taxa.
Nesses casos, você deveria indicar que falha é uma possibilidade
esperada ao retornar um
Result para propagar esses estados ruins para cima,
de forma que o código que chamou
seu código pode decidir como tratar o problema.
Entrar em panic! não seria a melhor
maneira de lidar com esses casos.
Quando seu código realiza operações em valores, ele deveria verificar que os valores
são
válidos primeiro, e entrar em panic! caso não sejam. Isso é em boa parte por razões de
segurança: tentar operar em dados inválidos pode expor seu
código a vulnerabilidades.
Essa é a principal razão para a biblioteca padrão entrar em
panic! se você tentar um
acesso de memória fora dos limites: tentar acessar memória que não pertence à estrutura
de dados atual é um problema de segurança comum. Funções frequentemente tem
contratos: seu comportamento somente é garantido se os inputs cumprem
requerimentos
específicos. Entrar em pânico quando o contrato é violado faz sentido porque uma violação
de contrato sempre indica um bug da parte do chamador, e não é o tipo de erro que você
quer que seja tratado explicitamente. De fato, não há nenhuma maneira razoável para o
código chamador se recuperar: os programadores
que precisam consertar o código.
Contratos para uma função, especialmente quando uma
violação leva a pânico, devem ser
explicados na documentação da API da função.
No entanto, ter várias checagens de erro em todas suas funções pode ser verboso
e
irritante. Felizmente, você pode usar o sistema de tipos do Rust (e portanto a
checagem que
o compilador faz) para fazer várias dessas checagens para você. Se
sua função tem um tipo
particular como parâmetro, você pode continuar com a lógica
do seu código sabendo que o
compilador já assegurou que você tem um valor válido.
Por exemplo, se você tem um tipo
em vez de uma Option , seu programa espera
ter algo ao invés de nada. Seu código não
precisa tratar dois casos para
as variantes Some e None : ele vai somente ter um caso para
definitivamente ter
um valor. Um código que tente passar nada para sua função não vai
nem compilar,
então sua função não precisa checar esse caso em tempo de execução.
Outro exemplo é usar
um tipo de inteiro sem sinal como u32 , que assegura que o
parâmetro nunca é
negativo.
Vamos dar um passo além na ideia de usar o sistema de tipos de Rust para assegurar que
temos
um valor válido e ver como criar um tipo customizado para validação.
Lembre do
jogo de adivinhação no Capítulo 2 onde nosso código pedia ao usuário para adivinhar um
número entre 1 e 100. Nós nunca validamos que o chute do usuário
fosse entre esses
números antes de compará-lo com o número secreto; nós somente validamos que o chute
era positivo. Nesse caso, as consequências não foram tão
drásticas: nosso output de "Muito
alto" ou "Muito baixo" ainda estariam corretos. Seria
uma melhoria útil guiar o usuário para
chutes válidos, e ter um comportamento distinto
quando um usuário chuta um número
fora do limite e quando um usuário digita letras, por exemplo.
Uma maneira de fazer isso seria interpretar o chute como um i32 em vez de
somente um
u32 para permitir números potenciamente negativos, e então adicionar
uma checagem se
o número está dentro dos limites, conforme a seguir:
loop {
// snip
};
continue;
match palpite.cmp(&numero_secreto) {
// snip
A expressão if checa se nosso valor está fora dos limites, informa o usuário
sobre o
problema, e chama continue para começar a próxima iteração do loop
e pedir por outro
chute. Depois da expressão if podemos proceder com as comparações entre palpite e o
número secreto sabendo que palpite está entre 1 e 100.
Em vez disso, podemos fazer um novo tipo e colocar as validações em uma função
para
criar uma instância do tipo em vez de repetir as validações em todo lugar.
Dessa maneira, é
seguro para funções usarem o novo tipo nas suas assinaturas e confidentemente usar os
valores que recebem. A Listagem 9-9 mostra uma maneira de definir um tipo Palpite que
vai somente criar uma instância de Palpite se a função
new receber um valor entre 1 e
100:
pub struct Palpite {
valor: u32,
impl Palpite {
Palpite {
valor
self.valor
Primeiro, definimos uma struct chamada Palpite que tem um campo chamado valor
que
guarda um u32 . Isso é onde o número vai ser guardado.
Então nós implementamos uma função associada chamada new em Palpite que cria
instâncias de valores Palpite . A função new é definida a ter um parâmetro
chamado
valor de tipo u32 e retornar um Palpite . O código no corpo da função
new testa para
ter certeza que valor está entre 1 e 100. Se valor não passa
nesse teste, fazemos uma
chamada a panic! , que vai alertar ao programador que
está escrevendo o código
chamando a função que ele tem um bug que precisa ser corrigido, porque criar um
Palpite com um valor fora desses limites violaria
o contrato em que Palpite::new se
baseia. As condições em que Palpite::new pode entrar em pânico devem ser discutidas na
sua documentação da API voltada ao público;
no Capítulo 14 nós cobriremos convenções de
documentação indicando a possibilidade de um panic!
na documentação de API. Se
valor de fato passa no
teste, criamos um novo Palpite com o campo valor preenchido
com o parâmetro
valor e retornamos o Palpite .
Em seguida, implementamos um método chamado valor que pega self emprestado, não
tem nenhum outro parâmetro, e retorna um u32 . Esse é o tipo de método às vezes
chamado de getter, pois seu propósito é pegar um dado de um dos campos e o retornar.
Esse método público é necessário porque o campo valor da struct Palpite é privado.
É
importante que o campo valor seja privado para que código usando a struct Palpite
não
tenha permissão de definir o valor de valor diretamente: código de fora do módulo
deve
usar a função Palpite::new para criar uma instância de Palpite , o que certifica
que não
há maneira de um Palpite ter um valor que não foi checado pelas condições
definidas
na função Palpite::new .
Uma função que tem um parâmetro ou retorna somente números entre 1 e 100 pode
então
declarar na sua assinatura que ela recebe ou retorna um Palpite em vez
de um u32 e não
precisaria fazer nenhuma checagem adicional no seu corpo.
Resumo
As ferramentas de tratamento de erros de Rust são feitas para te ajudar a escrever
código
mais robusto. A macro panic! sinaliza que seu programa está num estado que
não
consegue lidar e deixa você parar o processo ao invés de tentar prosseguir com
valores
inválidos ou incorretos. O enum Result usa o sistema de tipos de Rust para indicar que
operações podem falhar de uma maneira que seu código pode se recuperar. Você pode
usar Result para dizer ao código que chama seu código que ele precisa
tratar potenciais
sucessos ou falhas também. Usar panic! e Result nas situações
apropriadas fará seu
código mais confiável em face aos problemas inevitáveis.
Agora que você viu as maneiras úteis em que a biblioteca padrão usa genéricos com
os
enums Option e Result , nós falaremos como genéricos funcionam e como você
pode usá-
los em seu código no próximo capítulo.
Do mesmo modo que uma função aceita parâmetros cujos valores não sabemos
para
escrever código que será processado em múltiplos valores concretos, nós
podemos
escrever funções que recebem parâmetros de alguns tipos genéricos ao
invés de tipos
concretos como i32 ou String . Nós já usamos tipos genéricos
no Capítulo 6 com
Option<T> , no Capítulo 8 com Vec<T> e HashMap<K, V> , e no Capítulo 9 com Result<T,
E> . Nesse capítulo, vamos explorar como definir
nossos próprios tipos, funções e métodos
usando tipos genéricos!
Primeiro, nós vamos revisar as mecânicas de extrair uma função que reduz
duplicação de
código. Então usaremos a mesma mecânica para fazer uma função
genérica usando duas
funções que só diferem uma da outra nos tipos dos seus
parâmetros. Nós vamos usar tipos
genéricos em definições de struct e enum
também.
Depois disso, nós vamos discutir traits, que são um modo de definir
comportamento de
uma forma genérica. Traits podem ser combinados com tipos
genéricos para restringir um
tipo genérico aos tipos que tem um comportamento
particular ao invés de qualquer tipo.
Finalmente, nós discutiremos tempos de vida, que são um tipo de generalização que nos
permite dar ao compilador informações sobre como as referências são
relacionadas umas
com as outras. Tempos de vida são as características em Rust
que nos permitem pegar
valores emprestados em muitas situações e ainda ter a aprovação do compilador de que as
referências serão válidas.
Considere um pequeno programa que acha o maior número em uma lsita, mostrado
na
Listagem 10-1:
fn main() {
maior = numero;
Esse código recebe uma lista de inteiros, guardados aqui na variável lista_numero . Coloca
o primeiro item da lista na variável chamada maior .
Então ele itera por todos os números
da lista, e se o valor atual é maior que o número guardado em maior , substitui o valor em
maior . Se o valor atual é
menor que o valor visto até então, maior não é mudado. Quando
todos os items
da lista foram considerados, maior terá o maior valor, que nesse caso é 100.
Se nós precisássemos encontrar o maior número em duas listas diferentes de
números, nós
poderíamos duplicar o código da Listagem 10-1 e usar a mesma
lógica nas duas partes do
programa, como na Listagem 10-2:
fn main() {
maior = numero;
maior = numero;
Ao passo que esse código funciona, duplicar código é tedioso e tende a causar
erros, e
significa que temos múltiplos lugares para atualizar a lógica se
precisarmos mudá-lo.
Para eliminar essa duplicação, nós podemos criar uma abstração, que nesse caso
será na
forma de uma função que opera em uma lista de inteiros passadas à função como um
parâmetro. Isso aumentará a clareza do nosso código e nos
permitirá comunicar e pensar
sobre o conceito de achar o maior número em uma
lista independentemente do lugar no
qual esse conceito é usado.
No programa na Listagem 10-3, nós extraímos o código que encontra o maior número para
uma função chamada maior . Esse programa pode achar o maior número
em duas listas de
números diferentes, mas o código da lista 10-1 existe apenas
em um lugar:
maior = item;
maior
fn main() {
Nós podemos usar os mesmos passos usando tipos genéricos para reduzir a duplicação de
código de diferentes modos em diferentes cenários. Do mesmo modo
que o corpo da
função agora é operado em uma list abstrata ao invés de valores concretos, códigos
usando tipos genéricos operarão em tipos abstratos. Os conceitos empoderando tipos
genéricos são os mesmos conceitos que você já conhece que empodera funções, só que
aplicado de modos diferentes.
E se nós tivéssemos duas funções, uma que acha o maior item em um slice de valores i32 e
um que acha o maior item em um corte de valores char ? Como nos livraríamos dessa
duplicação? Vamos descobrir!
Tipos Genéricos de Dados
Usando tipos genéricos onde usualmente colocamos tipos, como em assinaturas de
funções ou estruturas, vamos criar definições que podemos usar muitos tipos diferentes de
tipos concretos de dados. Vamos dar uma olhada em como definir
funções, structs, enums
e métodos usando tipos genéricos, e ao final dessa
seção discutiremos a performance do
código usando tipos genéricos.
Nós podemos definir funções que usam tipos genéricos na assinatura da função
onde os
tipos de dados dos parâmetros e os retornos vão. Desse modo, o código
que escrevemos
pode ser mais flexível e pode fornecer mais funcionalidades para os chamadores da nossa
função, e ainda diminuir duplicação de código.
Continuando com nossa função maior , a Listagem 10-4 mostra duas funções que oferecem
a mesma funcionalidade de encontrar o maior valor dado um corte. A
primeira função é a
que extraímos na Listagem 10-3 que encontra o maior ì32 em um corte. A segunda função
encontra o maior char em um corte:
maior = item;
maior
maior = item;
maior
fn main() {
Aqui as funções maior_i32 e maior_char tem exatamente o mesmo corpo, então seria
bom se pudéssemos transformar essas duas funções em uma e nos livrar da duplicação.
Por sorte, nós podemos fazer isso introduzindo um parâmetro de tipo genérico!
Quando usamos um parâmetro no corpo de uma função, nós temos que declarar o
parâmetro na assinatura para que o compilador saiba o que aquele nome no corpo
significa. Similarmente, quando usamos um tipo de nome de parâmetro em uma assinatura
de função, temos que declarar o tipo de nome de parâmetro antes de usa-lo. Declarações
de tipos de nomes vão em colchetes entre o nome da função e a lista de paramêtros.
Nós leríamos isso como: a função maior é genérica sobre algum tipo T . Ela
tem um
parâmetro chamado lista , e o tipo de lista é um corte dos valores
do tipo T . A função
maior retornará um valor do mesmo tipo T .
A listagem 10-5 mostra a definição da função unificada maior usando um tipo genérico de
dado na sua assinatura, e mostra quando nós poderemos chamar a função maior com ou
um corte de valores de i32 ou de valores char . Note que esse código não compilará
ainda!
maior = item;
maior
fn main() {
Listagem 10-5: Uma definição para a função maior que usa um tipo genérico como
parâmetro mas não compila ainda
| ^^^^
Nós podemos definir structs para usar um parâmetro de tipo genérico em um ou mais
campos de um struct com a sintaxe <> também. A listagem 10-6 mostra a definição e faz
uso do struct Ponto que contém as coordenadas x e y com valores de qualquer tipo:
struct Ponto<T> {
x: T,
y: T,
fn main() {
A sintaxe é similar a que se usa em definições de funções usando tipos genéricos. Primeiro,
nós temos que declarar o nome do tipo de parâmetro dentro
de colchetes angulares logo
após o nome da struct. Então nós podemos usar tipos
genéricos na definição da struct onde
nós especificaríamos tipos concretos de
dados.
Note que porque só usamos um tipo genérico na definição de Ponto , o que estamos
dizendo é que o struct Ponto é genérico sobre algum tipo T , e os
campos x e y são
ambos do mesmo tipo, qualquer que seja. Se nós tentarmos
criar uma instância de um
Ponto que possui valores de tipos diferentes, como
na Listagem 10-7, nosso código não
compilará:
Nome do arquivo: src/main.rs
struct Ponto<T> {
x: T,
y: T,
fn main() {
-->
floating-point variable
Quando atribuímos o valor de 5 para x , o compilador sabe que para essa instância de
Ponto o tipo genérico T será um número inteiro. Então quando
especificamos 4.0 para y ,
o qual é definido para ter o mesmo tipo de x , nós
temos um tipo de erro de
incompatibilidade.
Se nós quisermos definir um struct de Ponto onde x e y têm tipos diferentes e quisermos
fazer com que esses tipos sejam genéricos, nós podemos usar parâmetros múltiplos de
tipos genéricos. Na listagem 10-8, nós mudamos a
definição do Ponto para os tipos
genéricos T e U . O campo x é do tipo
T , e o campo y do tipo U :
x: T,
y: U,
fn main() {
Agora todos as instâncias de Ponto são permitidas! Você pode usar quantos
parâmetros de
tipos genéricos em uma definição quanto quiser, mas usar mais que
alguns começa a
tornar o código difícil de ler e entender. Se você chegar em um
ponto que precisa usar
muitos tipos genéricos, é provavelmente um sinal que seu
código poderia ser reestruturado
e separado em partes menores.
Similar a structs, enums podem ser definidos para conter tipos genéricos de dados nas suas
variantes. Nós usamos o enum Option<T> concedido pela biblioteca padrão no capítulo 6, e
agora a definição deve fazer mais sentido. Vamos dar uma outra olhada:
enum Option<T> {
Some(T),
None,
Enum podem usar tipos múltiplos genéricos também. A definição do enum Resultado que
usamos no Capítulo 9 é um exemplo:
Ok(T),
Err(E),
struct Ponto<T> {
x: T,
y: T,
impl<T> Ponto<T> {
&self.x
fn main() {
let p = Ponto { x: 5, y: 10 };
Note que temos que declarar T logo após impl para usar T no tipo Ponto<T> . Declarar T
como um tipo genérico depois e impl é como o Rust
sabe se o tipo dentro das chaves
angulares em Ponto é um tipo genérico ou um
tipo concreto. Por exemplo, nós poderíamos
escolher implementar métodos nas
instâncias de Ponto<f32> ao invés nas de Ponto com
qualquer tipo genérico.
A listagem 10-10 mostra que não declaramos nada depois de impl
nesse caso, já
que estamos usanod um tipo concreto, f32 :
impl Ponto<f32> {
(self.x.powi(2) + self.y.powi(2)).sqrt()
Listagem 10-10: Construindo um bloco de impl que só se aplica a uma struct com o tipo
específico usado pelo parâmetro de tipo genérico
T
Parâmetros de tipos genéricos em uma definição de struct não são sempre os parâmetros
de tipos genéricos que você quer usar na assinatura de método daquela struct. A Listagem
10-11 define um método mistura na estrutura
Ponto<T, U> da Listagem 10-8. O método
recebe outro Ponto como parâmetro,
que pode ter tipos diferentes de self Ponto dos
quais usamos no mistura .
O método cria uma nova instância de Ponto que possui o valor
x de self
Ponto (que é um tipo de T ) e o valor de y passado de Ponto (que é do tipo
W ):
x: T,
y: U,
Ponto {
x: self.x,
y: other.y,
fn main() {
let p3 = p1.mistura(p2);
Listagem 10-11: Métodos que usam diferentes tipos genéricos das suas definições de struct
No main , nós definimos um Ponto que tem um i32 para o x (com o valor de
5 ) e um
f64 para y (com o valor de 10.4 ). p2 é um Ponto que tem um
pedaço de string x (com
o valor "Ola" ) e um char para y (com o valor
c ). Chamando mistura no p1 com o
argumento p2 nos dá p3 , que terá um
i32 para x , já que x veio de p1 . p3 terá um
char para y , já que y veio de p2 . O println! irá imprimir p3.x = 5, p3.y = c .
Note que os parâmetro genéricos T e U são declarados depois de impl , já
que eles vão
com a definição do struct. Os parâmetros genéricos V e Ẁ são
declarados depois de fn
mistura , já que elés só são relevantes para esse método.
Você pode estar lendo essa seção e imaginando se há um custo no tempo de execução para
usar parâmetros de tipos genéricos. Boas notícias: o modo como
Rust implementa tipos
genéricos significa que seu código não vai ser executado
mais devagar do que se você
tivesse especificado tipos concretos ao invés de tipos genéricos como parâmetros!
Rust consegue fazer isso realizando monomorfização de código usando tipos genéricos em
tempo de compilação. Monomorfização é o processo de transformar
código genérico em
código específico substituindo os tipos genéricos pelos tipos concretos que são realmente
utilizados.
O que o compilador faz é o oposto dos passos que fizemos para criar uma função
de tipo
genérico na Listagem 10-5. O compilador olhar para todos os lugares que
o código genérico
é chamado e gera o código para os tipos concretos que o
código genérico é chamado.
Vamos trabalhar sobre o exemplo que usa o padrão de enum Option da biblioteca:
Quando o Rust compilar esse código, ele vai fazer a monomorfização. O compilador lerá os
valores que foram passados para Option e ver que temos
dois tipos de Option<T> : um é
i32 , e o outro f64 . Assim sendo, ele expandirá a definição genérica de Option<T> para
Option_i32 e Option_64 ,
substituindo a definição genérica por definições específicas.
Some(i32),
None,
enum Option_f64 {
Some(f64),
None,
fn main() {
Nós podemos escrever códigos não duplicados usando tipos genéricos, e Rust vai
compila-
lo em código que especifica o tipo em cada instância. Isso significa que não pagamos
nenhum custo em tempo de processamento para usar tipos genéricos; quando o código
roda, ele executa do mesmo modo como executaria se
tivéssemos duplicado cada definição
particular a mão. O proccesso de monomorfização é o que faz os tipos genéricos de Rust
serem extremamente eficientes em tempo de processamento.
Definindo um Trait
O comportamento de um tipo consiste nos métodos que podemos chamar para aquele
tipo. Tipos diferentes dividem o mesmo comportamento se podemos chamar os mesmos
métodos em todos esses tipos. Definições de traits são um modo de agrupar métodos de
assinaturas juntos a fim de definir um conjunto de comportamentos para atingir algum
propósito.
Por exemplo, digamos que temos múltiplos structs que contém vários tipos e
quantidades
de texto: um struct ArtigoDeNoticias que contém uma notícia preenchida em um lugar do
mundo, e um Tweet que pode ter no máximo 140
caracteres em seu conteúdo além dos
metadados como se ele foi um retweet ou uma
resposta a outro tweet.
Nós queremos fazer uma biblioteca agregadora de mídia que pode mostrar resumos
de
dados que podem estar guardados em uma instância de ArtigoDeNoticia ou Tweet . O
comportamento que precisamos cada struct possua é que seja capaz de
ser resumido, e
que nós possamos pedir pelo resumo chamando um método resumo
em uma instância. A
Listagem 10-12 mostra a definição de um trait Resumir que
expressa esse conceito:
Um trait pode ter vários métodos no seu corpo, com os métodos das assinaturas
listados
um por linha e cada linha terminando com um ponto e vírgula.
Agora que deifnimos o trait Resumir , podemos implementa-lo nos tipos do nosso
agregador de mídias que queremos que tenham esse comportamento. A Listagem 10-13
mostra uma implementação do trait Resumir no struct ArtigoNotícia que
possui o título,
o autor e a localização para criar e retornar o valor de resumo . Para o struct Tweet , nós
escolhemos definir resumo como o nome de
usuário seguido por todo o texto do tweet,
assumindo que o conteúdo do tweet já
está limitado a 140 caracteres.
Uma vez que implementamos o trait, nós podemos chamar os métodos nas instâncias
de
ArtigoDeNoticia e Tweet da mesma maneira que nós chamamos métodos que não
são
parte de um trait:
nomeUsuario: String::from("horse_ebooks"),
pessoas"),
resposta: false,
retweet: false,
};
Isso irá imprimir 1 novo tweet: claro, como vocês provavelmente já sabem, pessoas
use aggregator::Resumir;
struct PrevisaoTempo {
alta_temp: f64,
baixa_temp: f64,
chance_de_chuva: f64,
Listagem 10-14: Trazendo o trait Resumir do nosso crate aggregator para o escopo de
outro crate
Esse código também assume que Resumir é um trait público, o que é verdade porque
colocamos a palavra-chave pub antes de trait na Listagem 10-12.
A Listagem 10-15 mostra como poderíamos ter escolhido especificar uma string padrão
para o método resumo do trait Resumir ao invés de escolher de apenas
definir a
assinatura do método como fizemos na Listagem 10-12:
String::from("(Leia mais...)")
Mesmo que não estejamos mais escolhendo definir o método resumo diretamente em
ArtigoDeNoticia , já que o método resumo tem uma implementação padrão e nós
especificamos que ArtigoDeNoticia implementa o trait Resumir , nós ainda
podemos
chamar o método resumo em uma instância de ArtigoDeNoticia :
autor: String::from("Iceburgh"),
};
Mudando o trait Resumir para ter uma implementação padrão para resumo não
requer
que nós mudemos nada na implementação de Resumir em Tweet na Listagem 10-13 ou
em PrevisaoTempo na Listagem 10-14: a sintaxe para sobrepor
uma implementação padrão
é exatamente a mesma de uma sintaxe para implementar
um método de trait que não tem
uma implementação padrão.
format!("@{}", self.nomeusuario)
Uma vez que definimos resumo_autor , nós podemos chamar resumo em instâncias
do
struct Tweet , e a implementação padrão de resumo chamará a definição de
resumo_autor
que fornecemos.
nomeusuario: String::from("horse_ebooks"),
pessoas"),
resposta: false,
retweet: false,
};
Limites de traits
Agora que definimos traits e os implementamos em tipos, podemos usar traits com
parâmetros de tipos genéricos. Podemos restringir tipos genéricos para que ao
invés de
serem qualquer tipo, o compilador tenha certeza que o tipo estará limitado a aqueles tipos
que implementam um trait em particular e por consequência tenham o comportamento
que precisamos que os tipos tenham. Isso é
chamado de especificar os limites dos traits em
um tipo genérico.
Por exemplo, na Listagem 10-13, nós implementamos o trait Resumir nos tipos
ArtigoDeNoticia e Tweet . Nós podemos definir uma função notificar que chama
o
método resumo no seu parâmetro item , que é do tipo genérico T . Para ser possível
chamar resumo em item sem receber um erro, podemos usar os limites de traits em T
para especificar que item precisa ser de um tipo que
implementa o trait Resumir :
Para funções que têm múltiplos parâmetros de tipos genéricos, cada tipo genérico tem seu
próprio limite de trait. Especificar muitas informações de limites de trait dentro de chaves
angulares entre o nome de uma função e sua
lista de parâmetros pode tornar o código
difícil de ler, então há uma sintaxe alternativa para especificar limites de traits que nos
permite movê-los para
uma cláusula depois da assinatura da função. Então ao invés de:
U: Clone + Debug
Isso é menos confuso e faz a assinatura da função ficar mais parecida à uma
função sem ter
vários limites de trait, nela o nome da função, a lista de
parâmetros, e o tipo de retorno
estão mais próximos.
Então qualquer hora que você queira usar um comportamento definido por um trait
em um
tipo genérico, você precisa especificar aquele trait nos limites dos
parâmetros dos tipos
genéricos. Agora podemos consertar a definição da função maior que usa um parâmetro
de tipo genérico da Listagem 10-5! Quando deixamos
esse código de lado, nós recebemos
esse erro:
| ^^^^
No corpo de maior nós queríamos ser capazes de comparar dois valores de tipo
T usando
o operador maior-que. Esse operador é definido com o método padrão na biblioteca
padrão de trait std::cmp::PartialOrd . Então para que possamos
usar o operador maior-
que, precisamos especificar PartialOrd nos limites do
trait para T para que a função
maior funcione em partes de qualquer tipo
que possa ser comparada. Não precisamos
trazer PartialOrd para o escopo porque está no prelúdio.
--> src/main.rs:4:23
| |
--> src/main.rs:6:9
| ^----
| ||
A chave para esse erro é cannot move out of type [T], a non-copy array . Com
nossas
versões não genéricas da função maior , nós estávamos apenas tentando
encontrar o
maior i32 ou char . Como discutimos no Capítulo 4, tipos como o
i32 e char que têm
um tamanho conhecido podem ser armazenados na pilha,
então eles implementam o trait
Copia . Quando mudamos a função maior para ser genérica, agora é possível que o
parâmetro list poderia ter tipos nele
que não implementam o trait Copia , o que significa
que não seríamos capazes de mover o valor para fora de list[0] para a variável maior .
Se quisermos ser capazes de chamar esse código com tipos que são Copia , nós
podemos
adicionar Copia para os limites de trait de T ! A Listagem 10-16 mostra o código completo
de uma função maior genérica que compilará desde que
os tipos dos valores nessa parte
que passamos para maior implementem ambos os
traits PartialOrd e Copia , como i32
e char :
maior = item;
maior
fn main() {
Se não quisermos restringir nossa função maior para apenas tipos que implementam o
trait Copia , podemos especificar que T tem o limite de trait
Clone ao invés de Copia e
clonar cada valor na parte quando quisermos que a
função maior tenha domínio. Usando a
função clone significa que potencialmente estamos fazendo mais alocações no heap,
porém, e alocações no heap podem ser vagarosas se estivermos trabalhando com grande
quantidade de dados. Outro jeito que podemos implementar maior é para a função
retornar uma
referência ao valor de T em uma parte. Se retornarmos o tipo de retorno
para
ser &T ao invés de T e mudar o corpo da função para retornar uma referência, não
precisaríamos usar os limites de traits Clone ou Copia e
nós não estaríamos fazendo
nenhuma alocação de heap.
Tente implementar essas soluções alternativas você mesmo!
Usando um limite de trait com um bloco impl que usa parâmetros de tipos genéricos
podemos implementar métodos condicionalmente apenas para tipos que
implementam os
traits específicos. Por exemplo, o tipo Par<T> na listagem 10-17 sempre implementa o
método novo , mas Par<T> implementa apenas o
cmp_display se seu tipo interno T
implementa o trait PartialOrd que permite a comparação e do trait Display que permite
a impressão:
use std::fmt::Display;
struct Par<T> {
x: T,
y: T,
impl<T> Par<T> {
Self {
x,
y,
fn cmp_display(&self) {
} else {
// --snip--
let s = 3.to_string();
Traits e limites de traits nos deixam escrever código que usam parâmetros de
tipos
genéricos para reduzir a duplicação, mas ainda sim especificam para o
compilador
exatamente qual o comportamento que nosso código precisa que o tipo
genérico tenha.
Porque demos a informação do limite de trait para o compilador,
ele pode checar que todos
os tipos concretos usados no nosso código proporcionam o comportamento correto. Em
linguagens dinamicamente tipadas, se
nós tentássemos chamar um método em um tipo
que não implementamos, nós receberíamos um erro em tempo de execução. O Rust move
esses erros para o temp
de compilação para que possamos ser forçados a resolver os
problemas antes que nosso código seja capaz de rodar. Além disso, nós não temos que
escrever código
que checa o comportamento em tempo de execução já que já checamos
em tempo de
compilação, o que melhora o desempenho comparado com outras linguagens
sem ter
que abrir mão da flexibilidade de tipos genéricos.
Há outro tipo de tipos genéricos que estamos usando sem nem ao menos perceber
chamados lifetimes. Em vez de nos ajudar a garantir que um tipo tenha o
comportamento
que precisamos, lifetimes nos ajudam a garantir que as referências são válidas tanto quanto
precisam ser. Vamos aprender como lifetimes fazem isso.
Tempos de vida são um tópico grande que não poderão ser cobertos inteiramente nesse
capítulo, então nós vamos cobrir algumas formas comuns que você pode encontrar a
sintaxe de tempo de vida nesse capítulo para que você se familiarize com os conceitos. O
Capítulo 19 conterá informações mais avançadas
sobre tudo que tempos de vida podem
fazer.
let r;
let x = 5;
r = &x;
Os próximos exemplos declaram vaŕiáveis sem darem a elas um valor inicial, então o
nome da variável existe no escopo exterior. Isso pode parecer um conflito com Rust
não ter null. No entanto, se tentarmos usar uma variável
antes de atribuir um valor a
ela, nós teremos um erro em tempo de compilação.
Tente!
6 | r = &x;
7 | }
...
10 | }
A variável x não "vive o suficiente". Por que não? Bem, x vai sair de escopo quando
passarmos pela chaves na linha 7, terminando o escopo interior.
Mas r é válida para o
escopo exterior; seu escopo é maior e dizemos que ela
"vive mais tempo". Se Rust
permitisse que esse código funcionasse, r estaria
fazendo uma referência à memória que
foi desalocada quando x saiu de escopo,
e qualquer coisa que tentássemos fazer com r
não funcionaria corretamente.
Então como o Rust determina que esse código não deve ser
permitido?
O Verificador de Empréstimos
// |
{ // |
r = &x; // | |
} // -+ |
// |
// |
// -------+
Vamos olhar para o exemplo na Listagem 10-20 que não tenta fazer uma referência
solta e
compila sem nenhum erro:
{
// |
// | |
// --+ |
} // -----+
Aqui, x tem o tempo de vida de 'b , que nesse caso tem um tempo de vida maior que o de
'a . Isso quer dizer que r pode referenciar x : o Rust sabe
que a referência em r será
sempre válida enquanto x for válido.
Vamos escrever uma função que retornará a mais longa de dois cortes de string. Nós
queremos ser capazes de chamar essa função passando para ela dois cortes de strings, e
queremos que retorne uma string. O código na Listagem 10-21
deve imprimir A string
mais longa é abcd uma vez que tivermos implementado a
função maior :
fn main() {
Note que queremos que a função pegue cortes de string (que são referências, como
falamos no Capítulo 4) já que não queremos que a função maior tome posse
de seus
argumentos. Nós queremos que uma função seja capaz de aceitar cortes de
uma String
(que é o tipo de variável string1 ) assim como literais de string
(que é o que a variável
strin2 contém).
Recorra à seção do Capítulo 4 "Cortes de Strings como Parâmetros" para mais discussões
sobre porque esses são os argumentos que queremos.
} else {
Ao invés disso recebemos o seguinte erro que fala sobre tempos de vida:
= help: this function's return type contains a borrowed value, but the
O texto de ajuda está nos dizendo que o tipo de retorno precisa de um parâmetro
de tempo
de vida genérico nele porque o Rust não pode dizer se a referência que
está sendo
retornada se refere a x ou y . Atualmente, nós também não sabemos, já que o bloco if
no corpo dessa função retorna uma referência para x e o bloco else retorna uma
referência para y !
Enquanto estamos definindo essa função, não sabemos os valores concretos que serão
passados para essa função, então não sabemos se o caso if ou o caso
else será
executado. Nós também não sabemos os tempos de vida concretos das
referências que
serão passadas, então não podemos olhar para esses escopos como
fizemos nas Listagem
10-19 e 10-20 afim de determinar que a referência que
retornaremos sempre será válida. O
verificador de empréstimos não consegue determinar isso também porque não sabe como
os tempos de vida de x e y se
relacionam com o tempo de vida do valor de retorno. Nós
vamos adicionar parâmetros genéricos de tempo de vida que definirão a relação entre as
referências para que o verificador de empréstimos possa fazer sua análise.
Anotações de tempo de vida tem uma sintaxe levemente incomum: os nomes dos
parâmetros de tempos de vida precisam começar com uma apóstrofe ' . Os nomes dos
parâmetros dos tempos de vida são usualmente todos em caixa baixa, e como tipos
genéricos, seu nome usualmente são bem curtos. 'a é o nome que a maior
parte das
pessoas usam por padrão. Parâmetros de anotações de tempos de vida vão depois do & de
uma referência, e um espaço separa a anotação de tempo de vida do tipo da referência.
Aqui vão alguns exemplos: nós temos uma referência para um i32 sem um parâmetro
tempo de vida, uma referência para um i32 que tem um parâmetro de
tempo de vida
chamado 'a :
&'a mut i32 // uma referência mutável com um tempo de vida explícito
Uma anotação de tempo de vida por si só não tem muito significado: anotações de
tempos
de vida dizem ao Rust como os parâmetros genéricos de tempos de vida de
múltiplas
referências se relacionam uns com os outros. Se tivermos uma função
com o parâmetro
primeiro que é uma referência para um i32 que tem um tempo
de vida de 'a , e a
função tem outro parâmetro chamado segundo que é outra
referência para um i32 que
também possui um tempo de vida 'a , essas duas
anotações de tempo de vida com o
mesmo nome indicam que as referências primeiro e segundo precisam ambas viver tanto
quanto o mesmo tempo de vida
genérico.
Vamos olhar para anotações de tempo de vida no contexto da função maior que
estamos
trabalhando. Assim como parâmetros de tipos genéricos, parâmetros de tempos de vida
genéricos precisam ser declarados dentro de colchetes angulares
entre o nome da função e
a lista de parâmetros. A limitanção que queremos dar ao Rust é que para as referências nos
parâmetros e o valor de retorno devem
ter o mesmo tempo de vida, o qual nomearemos
'a e adicionaremos para cada uma
das referências como mostrado na Listagem 10-23:
} else {
Isso compilará e produzirá o resultado que queremos quando usada com a função
main na
Listagem 10-21.
A assinatura de função agora diz que pra algum tempo de vida 'a , a função
receberá dois
parâmetros, ambos serão cortes de string que vivem pelo menos
tanto quanto o tempo de
vida 'a . A função retornará um corte de string que também vai durar tanto quanto o
tempo de vida 'a . Esse é o contrato que estamos dizendo ao Rust que queremos garantir.
Quando referências concretas são passadas para maior , o tempo de vida concreto que é
substituído por 'a é a parte do escopo de x que sobrepõe o
escopo de y . Já que escopos
sempre se aninham, outra maneira de dizer isso é
que o tempo de vida genérico 'a terá
um tempo de vida concreto igual ao menor
dos tempos de vida de x e y . Porque nós
anotamos a referência retornada com
o mesmo parâmetro 'a , a referência retornada será
portanto garantida de ser
válida tanto quanto for o tempo de vida mais curto de x e y .
Vamos ver como isso restringe o uso da função maior passando referências que
tem
diferentes tempos de vida concretos. A Listagem 10-25 é um exemplo direto
que deve
corresponder suas intuições de qualquer linguagem: string1 é válida
até o final do escopo
exterior, strin2 é válida até o final do escopo, a
string2 é válida até o final do escopo
interior. Com o verificador de empréstimos aprovando esse código; ele vai compilar e
imprimir A string mais longa é :
fn main() {
Em seguida, vamos tentar um exemplo que vai mostrar que o tempo de vida da referência
em resultado precisa ser o menor dos tempos de vida dos dois argumentos. Nós vamos
mover a declaração da variável resultado para fora do
escopo interior, mas deixar a
atribuição do valor para a variável resultado
dentro do escopo com string2 . Em seguida,
vamos mover o println! que usa o
resultado fora do escopo interior, depois que ele
terminou. O código na Listagem 10-25 não compilará:
fn main() {
let resultado;
Listagem 10-25: A tentativa de usar resultado depois que string2 saiu de escopo não
compilará
7 | }
9 | }
O erro está dizendo que para resultado ser válido para println! , a string2 teria que ser
válida até o final do escopo exterior. Rust sabe disso
porque nós anotamos os tempos de
vida dos parâmetros da função e retornamos
valores com o mesmo parâmetro do tempo
de vida, 'a .
Nós podemos olhar para esse código como humanos e ver que a string1 é mais
longa, e
portanto resultado conterá a referência para a string1 . Porque a
string1 não saiu de
escopo ainda, a referência para string1 ainda será válida para o println! . No entanto, o
que dissemos ao Rust com os parâmetros
de tempo de vida é que o tempo de vida da
referência retornado pela função maior é o mesmo que o menor dos tempos de vida das
referências passadas. Portanto, o verificador de empréstimos não permite o código da
Listagem 10-25
como possível já que tem um referência inválida.
Tente fazer mais alguns experimentos que variam os valores e os tempos de vidas
das
referências passadas para a função maior e como a referência retornada é
usada. Crie
hipóteses sobre seus experimentos se eles vão passar pelo verificador de empréstimos ou
não antes de você compilar, e então cheque para
ver se você está certo!
O modo exato de especificar parâmetros de tempos de vida depende do que sua função
está fazendo. Por exemplo, se mudaramos a implementação da função maior para sempre
retornar o primeiro argumento ao invés do corte de string
mais longo, não precisaríamos
especificar um tempo de vida no parâmetro y .
Este código compila:
resultado.as_str()
3 | resultado.as_str()
4 | }
note: borrowed value must be valid for the lifetime 'a as defined on the block
at 1:44...
| ^
O problema é que resultado sairá de escopo e será limpo no final da função maior , e
estamos tentando retornar uma referência para resultado da função.
Não há nenhum
modo que possamos especificar parâmetros de tempo de vida que
mudariam uma
referência solta, e o Rust não nos deixará criar uma referência solta. Nesse caso, a melhor
solução seria retornar um tipo de dado com posse
ao invés de uma referência de modo que
a função chamadora é então responsável
por limpar o valor.
Até agora, nós só definimos structs para conter tipos com posse. É possível para structs
manter referências, mas precisamos adicionar anotações de tempo de
vida em todas as
referências na definição do struct. A Listagem 10-26 tem a struct chamada
ExcertoImportante que contém um corte de string:
fn main() {
.next()
Esse struct tem um campo, parte , que contém um corte de string, que é uma
referência.
Assim como tipos genéricos de dados, temos que declarar o nome do
parâmetro genérico
de tempo de vida dentro de colchetes angulares depois do
nome do struct para que
possamos usar o parâmetro de tempo de vida no corpo da
definição do struct.
A função main cria uma instância da struct ExcertoImportante que contém uma
referência
pra a primeira sentença da String com posse da variável romance .
Nessa seção, nós aprendemos que toda referência tem um tempo de vida, e nós
precisamos especificar os parâmetros dos tempos de vida para funções ou estruturas que
usam referências. No entanto, no Capítulo 4 nós tínhamos a função na seção "Cortes de
Strings", mostradas novamente na Listagem 10-27, que
compilam sem anotações de tempo
de vida:
return &s[0..i];
&s[..]
Depois de escrever muito código em Rust, o time de Rust descobriu que os programadores
de Rust estavam digitando as mesmas anotações de tempo de vida
de novo e de novo.
Essas situações eram previsíveis e seguiam alguns padrões
determinísticos. O time de Rust
programou esses padrões no compilador de código de Rust para que o verificador de
empréstimos pode inferir os tempos de vida
dessas situações sem forçar o programador
adicionar essas anotações explicitamente.
As regras de elisão não fornecem total inferência: se o Rust aplicar as regras de forma
determinística ainda podem haver ambiguidades como quais tempos de vida as referências
restantes deveriam ter. Nesse caso, o compilador dará um
erro que pode ser solucionado
adicionando anotações de tempo de vida que correspondem com as suas intenções para
como as referências se relacionam umas
com as outras.
Agora, as regras que o compilador usa para descobrir quais referências de tempos de vidas
têm quando não há anotações explícitas. A primeira regra se aplica a tempos de vida de
entrada, e a segunda regra se aplica a tempos de vida de saída. Se o compilador chega no
fim das três regras e ainda há referências que ele não consegue descobrir tempos de vida, o
compilador irá
parar com um erro.
1. Cada parâmetro que é uma referência tem seu próprio parâmetro de tempo de
vida.
Em outras palavras, uma função com um parâmetro tem um parâmetro de tempo de
vida: fn foo<'a>(x: &'a i32) , uma função com dois argumentos recebe dois
parâmetros de tempo de vida separados: fn foo<'a, 'b>(x: &'a i32, y: &'b i32) ,
e assim por diante.
Vamos fingir que somos o compilador e aplicamos essas regras para descobrir quais os
tempos de vida das referências na assinatura da função primeira_palavra na Listagem 10-
27. A assinatura começa sem nenhum tempo de vida associado com as referências:
Então nós (como o compilador) aplicamos a primeira regra, que diz que cada parâmetro
tem sem próprio tempo de vida. Nós vamos chama-lo de 'a como é usual, então agora a
assinatura é:
Vamos fazer outro exemplo, dessa vez com a função maior que não tinha parâmetros de
tempo de vida quando começamos a trabalhar com ela na Listagem 10-22:
Fingindo que somos o compilador novamente, vamos aplicar a primeira regra: cada
parâmetro tem seu próprio tempo de vida. Dessa vez temos dois parâmetros, então
temos
dois tempos de vida:
Olhando para a segunda regra, ela não se aplica já que há mais de uma entrada de tempo
de vida. Olhando para a terceira regra, ela também não se aplica porque isso é uma função
e não um método, então nenhum dos parâmetros são self . Então, acabaram as regras,
mas não descobrimos qual é o tempo de vida do tipo de retorno. É por isso que recebemos
um erro quando tentamos
compilar o código da Listagem 10-22: o compilador usou as
regras de elisão de
tempo de vida que sabia, mas ainda sim não conseguiu descobrir todos
os tempos
de vida das referências na assinatura.
Porque a terceira regra só se aplica em assinaturas de métodos, vamos olhar
tempos de
vida nesse contexto agora, e ver porque a terceira regra significa que não temos que anotar
tempos de vida em assinaturas de métodos muito frequentemente.
Nomes de tempos de vida para campos de estruturas sempre precisam ser declarados após
a palavra-chave impl e então usadas após o nome da struct,
já que esses tempos de vida
são partes do tipo da struct.
impl<'a> ExcertoImportante<'a> {
A declaração do parâmetro de tempo de vida depois de impl e uso depois do tipo de nome
é obrigatório, mas nós não necessariamente precisamos de anotar o
tempo de vida da
referência self por causa da primeira regra da elisão.
Aqui vai um exemplo onde a terceira regra da elisão de tempo de vida se aplica:
impl<'a> ExcertoImportante<'a> {
self.part
Há dois tempos de vida de entrada, então o Rust aplica a primeira regra de elisão de
tempos de vida e dá ambos ao &self e ao anuncio seus próprios
tempos de vida. Então,
porque um dos parâmetros é self , o tipo de retorno
tem o tempo de vida de &self e
todos os tempos de vida foram contabilizados.
Você pode ver sugestões de usar o tempo de vida 'static em uma mensagem de ajuda de
erro, mas antes de especificar 'static como o tempo de vida para uma
referência, pense
sobre se a referência que você tem é uma que vive todo o tempo de vida do seu programa
ou não (ou mesmo se você quer que ele viva tanto,
se poderia). Na maior parte do tempo, o
probléma no código é uma tentativa de criar uma referência solta ou uma
incompatibilidade dos tempos de vida disponíveis, e a solução é consertar esses problemas,
não especificar um tempo
de vida 'static .
use std::fmt::Display;
fn maior_com_um_anuncio<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display
} else {
Essa é a função maior da Listagem 10-23 que retorna a maior de dois cortes de
string, mas
com um argumento extra chamado ann . O tipo de ann é o tipo genérico T , que pode ser
preenchido por qualquer tipo que implemente o trait
Display como está especificado na
cláusula where . Esse argumento extra será
impresso antes da função comparar os
comprimentos dos cortes de string, que é porque o trait de Display possui um limite.
Porque tempos de vida são um tipo
genérico, a declaração de ambos os parâmetros de
tempo de vida 'a e o tipo
genérico T vão na mesma lista com chaves angulares depois do
nome da função.
Sumário
Nós cobrimos várias coisas nesse capítulo! Agora que você sabe sobre parâmetros
de tipos
genéricos, traits e limites de traits, e parâmetros genéricos de tempo
de vida, você está
pronto para escrever código que não é duplicado mas pode ser
usado em muitas situações.
Parâmetros de tipos genéricos significam que o código pode ser aplicado a diferentes tipos.
Traits e limites de traits garantem que mesmo que os tipos sejam genéricos, esses tipos
terão o comportamento que o código precisa. Relações entre tempos de vida de referências
especificadas por anotações de tempo de vida garantem que esse código flexível não terá
referências soltas. E tudo isso acontece em tempo de
compilação para que a performace
em tempo de execução não seja afetada!
Acredite ou não, há ainda mais para aprender nessas áreas: Capítulo 17 discutirá objetos de
trait, que são outro modo de usar traits. O Capútulo 19
vai cobrir cenários mais complexos
envolvendo anotações de tempo de vida. O
Capítulo 20 vai tratar de alguns tipos avançados
de características do sistema.
Em seguida, porém, vamos falar sobre como escrever testes
em Rust para que
possamos ter certeza que nosso código usando todas essas
características está
funcionando do jeito que queremos!
Testing
Writing tests
Running tests
Test Organization
Um projeto de E/S: Criando um Programa
de Linha de Comando
Este capítulo é um recapitulação de muitas habilidades que você aprendeu até agora e uma
exploração de mais alguns recursos da biblioteca padrão. Vamos construir uma
ferramenta
que interage com arquivo de entrada/saída em linha de comando para praticar alguns dos
conceitos de Rust que você tem a disposição.
Ao longo do caminho, mostraremos como fazer com que nossa ferramenta de linha de
comando use recursos do
terminal que muitas ferramentas de linha de comando usam.
Leremos o valor de uma
variável de ambiente para permitir ao usuário configurar o
comportamento de nossa ferramenta.
Também imprimiremos na saída de console de erro
padrão ( stderr ) em vez da
saída padrão ( stdout ), por exemplo, o usuário pode
redirecionar saída de sucesso
para um arquivo enquanto ainda está vendo mensagens de
erro na tela.
Um membro da comunidade One Rust, Andrew Gallant, já criou uma versão completa
, e
muito rápida do grep , chamada ripgrep . Em comparação, nossa
versão do grep será
bastante simples, mas este capítulo lhe dará alguns dos
conhecimento básicos que você
precisa para entender um projeto real como
ripgrep .
Nosso projeto grep combinará uma série de conceitos que você aprendeu até agora:
$ cd minigrep
A primeira tarefa é fazer que minigrep aceite seus dois argumentos de linha de comando:
o
nome de arquivo e uma string para procurar. Ou seja, queremos ser capazes de
administrar o nosso
programa com cargo run , uma string para procurar e um caminho
para um arquivo onde será feira a
procura, dessa forma:
Neste momento, o programa gerado por cargo new não pode processar os argumentos
que nós
passamos. No entanto, algumas bibliotecas existentes no Crates.io
que podem nos
ajudar a escrever um programa que aceite argumentos na linha de comando, mas
como
você está aprendendo esses conceitos, vamos implementar essa capacidade
nós mesmos.
Para garantir que minigrep seja capaz de ler os valores dos argumentos da linha de
comando, nós
precisamos de uma função fornecida na biblioteca padrão do Rust, que é
Use o código na Listagem 12-1 para permitir que seu programa minigrep leia qualquer
argumento da linha de comando passados para ele e depois colete os valores em um vetor:
Arquivo: src/main.rs
use std::env;
fn main() {
println!("{:?}", args);
Primeiro, trazemos o módulo std::env para o escopo com uma declaração use , então nós
podemos usar a função args . Observe que a função std::env::args é
aninhada em dois
níveis de módulos. Como discutimos no Capítulo 7, nos casos em que
a função desejada
está aninhada em mais de um módulo, é convenção
trazer o módulo pai para o escopo em
vez da função. Como resultado, nós
podemos facilmente usar outras funções de std::env .
Também é menos ambíguo que
adicionar use std::env::args e depois chamando a
função com apenas args
porque args pode ser facilmente confundido com uma função
definida no
módulo atual.
$ cargo run
--snip--
["target/debug/minigrep"]
--snip--
Arquivo: src/main.rs
use std::env;
fn main() {
In file sample.txt
Ótimo, o programa está funcionando! Os valores dos argumentos que precisamos estão
sendo
salvos nas variáveis certas. Mais tarde, adicionaremos algum tratamento de erro para
lidar
com certas situações errôneas potenciais, como quando o usuário não fornece
argumentos; por enquanto, ignoraremos essa situação, e trabalharemos na adição das
funcinalidades de leitura dos arquivos.
Lendo um Arquivo
Agora vamos adicionar funcionalidades para ler o arquivo que é especificado no
argumento
filename da linha de comando. Primeiro, precisamos de um arquivo de amostra para
testá-lo:
o melhor tipo de arquivo a ser usado para garantir que o minigrep esteja
funcionando é um ,com uma
pequena quantidade de texto, em várias linhas com algumas
palavras repetidas. Listagem 12-3
tem um poema de Emily Dickinson que funcionará bem!
Crie um arquivo chamado
poem.txt no diretório raiz do seu projeto e entre com o poema
“I’m Nobody!
Who are you?”
Arquivo: poem.txt
To an admiring bog!
Com o texto no lugar, edite src/main.rs e adicione o código para abrir o arquivo, como
mostrado na Listagem 12-4:
Arquivo: src/main.rs
use std::env;
use std::fs::File;
use std::io::prelude::*;
fn main() {
// --snip--
f.read_to_string(&mut contents)
Após essas linhas, adicionamos novamente uma declaração temporária println! que
imprime o valor do contents depois que o arquivo é lido, para que possamos verificar que
o
o programa está funcionando até o momento.
Vamos executar este código com qualquer string como o primeiro argumento da linha de
comando (porque
ainda não implementamos a parte de pesquisa) e o arquivo poem.txt
como o
segundo argumento:
In file poem.txt
With text:
To an admiring bog!
Primeiro, a nossa função main agora executa duas tarefas: analisa argumentos e
abre
arquivos. Para uma função tão pequena, este não é um grande problema. No entanto, se
continuamos a desenvolver o nosso programa dentro de main , o número de tarefas
separadas que
a função main manipula aumentarão. Com uma função ganhando
responsabilidades,
torna-se mais difícil de raciocinar, mais difícil de testar e mais difícil de
mudar
sem quebrar uma das suas partes. É melhor separar a funcionalidade para que cada
função seja responsável por uma tarefa.
O terceiro problema é que usamos expect para imprimir uma mensagem de erro, ao
abrir
um arquivo, falha, mas a mensagem de erro apenas imprime file not found .
Abrir um
arquivo pode falhar de várias maneiras, além do arquivo faltando: como
exemplo, o arquivo
pode existir, mas talvez não possamos ter permissão para abri-lo.
Agora, se estivermos
nessa situação, imprimiríamos a mensagem de erro file not found
que daria ao usuário a
informação errada!
O quarto problema, usamos expect repetidamente para lidar com diferentes erros, e se o
usuário
executa o nosso programa sem especificar argumentos suficientes, eles terão erros
index out of bounds do Rust, que não explica claramente o problema. Seria
melhor se
todo o código de tratamento de erros estiver em um só lugar para futuros mantenedores
terem apenas um lugar para consultar, no código, se a lógica de tratamento de erros
precisar de
mudança. Ter todo o código de tratamento de erros em um só lugar também
assegurará que
estamos imprimindo mensagens que serão significativas para nossos
usuários finais.
Enquanto sua lógica de análise de linha de comando é pequena, ela pode permanecer
em main.rs.
Arquivo: src/main.rs
fn main() {
// --snip--
(query, filename)
Essa retrabalho pode parecer um exagero para o nosso pequeno programa, mas estamos
refatorando
em pequenos passos incrementais. Depois de fazer essa alteração, execute o
programa novamente para
verificar se a análise do argumento ainda funciona. É bom
verificar seu progresso
constantemente, porque isso irá ajudá-lo a identificar a causa dos
problemas quando eles
ocorrerem.
Podemos dar outro pequeno passo para melhorar ainda mais a função parse_config .
No
momento, estamos retornando uma tupla, mas depois quebramos imediatamente a tupla
em partes individuais novamente. Este é um sinal de que talvez não tenhamos
a abstração
certa ainda.
Outro indicador que mostra que há espaço para melhoria é a parte config
de
parse_config , o que implica que os dois valores que retornamos estão relacionados e
ambos são parte de um valor de configuração. Atualmente, não estamos transmitindo esse
significado na estrutura dos dados, que não sejam o agrupamento dos dois valores em um
tupla: podemos colocar os dois valores em uma estrutura e dar a cada uma das estruturas
um nome significativo. Isso facilitará os futuros mantenedores
deste código para entender
como os diferentes valores se relacionam entre si e
qual é o propósito deles.
Nota: algumas pessoas chamam este anti-padrão de usar valores primitivos quando
um
tipo complexo seria mais apropriado primitive obsession (obsessão primitiva).
A Listagem 12-6 mostra a adição de uma estrutura chamada Config definida para ter
campos chamados query e filename . Também mudamos a função parse_config
para
retornar uma instância da estrutura Config e atualizamos main para usar
os campos
struct em vez de ter variáveis separadas:
Arquivo: src/main.rs
fn main() {
// --snip--
struct Config {
query: String,
filename: String,
Existe uma tendência entre muitos Rustaceos para evitar o uso de clone para
consertar
problemas de ownership devido ao seu custo de tempo de execução. No
Capítulo 13, você aprenderá
como usar métodos mais eficientes neste tipo de
situação. Mas por agora,
é bom copiar algumas strings para continuar a fazer
progresso porque iremos
fazer essas cópias apenas uma vez, e nosso nome de
arquivo e seqüência de consulta são muito
pequenos. É melhor ter um programa de
trabalho que seja um pouco ineficiente do que
tentar hiper-optimizar o código na sua
primeira passagem. À medida que você se torna mais experiente
com Rust, será mais
fácil começar com a solução mais eficiente, mas para
agora, é perfeitamente aceitável
chamar clone .
Atualizamos main para que ele coloque a instância de Config retornada por
Agora, nosso código transmite mais claramente que query e filename estão relacionados,
e
seu objetivo é configurar como o programa funcionará. Qualquer código que use
esses
valores sabem encontrá-los na instância config nos campos nomeados
para esse
propósito.
Até agora, nós extraímos a lógica responsável por analisar os argumentos da linha de
comando de main e colocá-los na função parse_config , o que nos ajudou a ver que os
valores query e filename estavam relacionados e essa relação deve ser transmitida em
nosso código. Nós então adicionamos uma estrutura Config para nomear o propósito
relacionado de query e filename , e para poder retornar os nomes dos valores como
nomes de campos struct a partir da função parse_config .
Então, agora que a finalidade da função parse_config é criar uma instância Config
,
podemos alterar parse_config de ser uma função simples para um
função denominada
new que está associada à estrutura Config . Fazendo essa
mudança tornará o código mais
idiomático: podemos criar instâncias de tipos na biblioteca padrão, como String ,
chamando String::new , e
mudando parse_config para uma função new associada a
Config , iremos
ser capazes de criar instâncias de Config chamando Config::new .
Listagem 12-7
mostra as mudanças que precisamos fazer:
Arquivo: src/main.rs
fn main() {
// --snip--
// --snip--
impl Config {
Atualizamos main onde estávamos chamando parse_config para, em vez disso, chamar
Agora vamos trabalhar em consertar o nosso tratamento de erros. Lembre-se de que tentar
acessar
os valores no vetor args no índice 1 ou no índice 2 causará pânico no programa
se o vetor contiver menos de três itens. Tente executar o programa sem argumentos; Isso
parecerá assim:
$ cargo run
Running `target/debug/minigrep`
A linha index out of bounds: the len is 1 but the index is 1 é uma mensagem de
erro
destinada aos programadores. Isso não ajudará os usuários finais a entender o que
aconteceu e o que eles deveriam fazer a respeito disso. Vamos consertar isso agora.
Na Listagem 12-8, adicionamos uma verificação na função new que verificará que o
pedaço
é longo o suficiente antes de acessar os índices 1 e 2 . Se o pedaço não for
suficientemente
longo, o programa gera um pânico e exibe uma mensagem de erro melhor do que a
mensagem index out of bounds :
Arquivo: src/main.rs
// --snip--
if args.len() < 3 {
// --snip--
Este código é semelhante à função Guess::new que escrevemos na Listagem 9-9 onde
chamamos panic! quando o argumento value estava fora do alcance válido de
valores.
Em vez de verificar uma variedade de valores aqui, estamos checando que o
comprimento
de args é pelo menos 3 e o resto da função pode operar sob
o pressuposto de que essa
condição foi cumprida. Se args tiver menos de três
itens, essa condição será verdadeira, e
chamamos a macro panic! para terminar o
programa imediatamente.
Com estas poucas linhas de código adicionais em new , vamos executar o programa sem
nenhum
argumento novamente para ver como o erro parece agora:
$ cargo run
Running `target/debug/minigrep`
Este resultado é melhor: agora temos uma mensagem de erro razoável. No entanto, nós
também
temos informações estranhas que não queremos dar aos nossos usuários. Talvez
usando
a técnica que usamos na Lista 9-9 não é a melhor para usar aqui: uma chamada
para
panic! é mais apropriado para um problema de programação e não um problema de
uso
, conforme discutido no Capítulo 9. Em vez disso, podemos usar outra técnica que você
aprendeu no Capítulo 9 - retornando um Result que indica sucesso
ou um erro.
Em vez disso, podemos retornar um valor Result que conterá uma instância Config em
caso bem-sucedido e descreverá o problema no caso de erro. Quando
Config::new está se
comunicando com main , podemos usar o tipo Result para
sinalizar que não houve
problema. Então podemos mudar main para converter uma variante Err
em um erro mais
prático para os nossos usuários sem os demais textos
sobre thread 'main' e
RUST_BACKTRACE que uma chamada para panic! causa.
A Listagem 12-9 mostra as mudanças que precisamos fazer para o valor de retorno de
Arquivo: src/main.rs
impl Config {
if args.len() < 3 {
Nossa função new agora retorna um Result com uma instância Config no caso de
sucesso e um &'static str no caso de erro. Lembre-se da seção “The Static Lifetime” no
capítulo 10 que & 'static str é o tipo de string literal, que é o nosso tipo de mensagem
de erro por enquanto.
Para lidar com o caso de erro e imprimir uma mensagem amigável, precisamos atualizar
main para lidar com o Result sendo retornado por Config::new , conforme mostrado na
Listagem 12-10. Também assumiremos a responsabilidade de sair da linha de comando
com um código de erro diferente de zero do panic! e implementá-lo manualmente.
O
status de saída diferente de zero, é uma convenção para sinalizar o processo que chamou
nosso
programa que, o programa saiu com um estado de erro.
Arquivo: src/main.rs
use std::process;
fn main() {
process::exit(1);
});
// --snip--
Adicionamos uma nova linha de use para importar process da biblioteca padrão.
O código
na closure que será executado no caso de erro são apenas duas linhas: nós
imprimos o
valor de err e depois chamamos process::exit . A função process::exit
interromperá o
programa imediatamente e retornará o número que foi
passado como o código de status
de saída. Isso é semelhante ao manuseio baseado no panic!
que usamos na Listagem 12-
8, mas já não obtemos todos os resultados extras. Vamos tentar
isto:
$ cargo run
Running `target/debug/minigrep`
Listagem 12-11 mostra a função extraída run . Por enquanto, estamos apenas fazendo
a
pequena melhoria incremental da extração da função. Ainda estamos
definindo a função
em src/main.rs:
Arquivo: src/main.rs
fn main() {
// --snip--
run(config);
fn run(config: Config) {
f.read_to_string(&mut contents)
// --snip--
Arquivo: src/main.rs
use std::error::Error;
// --snip--
f.read_to_string(&mut contents)?;
Ok(())
Nós fizemos três mudanças significativas aqui. Primeiro, mudamos o tipo de retorno
da
função run para Result<(), Box<Error>> . Esta função anteriormente
devolveu o tipo de
unidade, () , e nós mantemos isso como o valor retornado Ok no
caso.
Em terceiro lugar, a função run agora retorna um valor Ok no caso de sucesso. Nós
declaramos o tipo de sucesso da função run como () na assinatura, que
significa que
precisamos wrap (envolver) o valor do tipo de unidade no valor Ok . Esta sintaxe Ok(())
pode parecer um pouco estranha no início, mas usar () como este é o
maneira idiomática
de indicar que chamamos run para seus efeitos colaterais somente;
ele não retorna o valor
que precisamos.
Quando você executa este código, ele compilará, mas exibirá um aviso:
--> src/main.rs:18:5
18 | run(config);
| ^^^^^^^^^^^^
Rust nos diz que nosso código ignorou o valor Result e o valor de Result
pode indicar
que ocorreu um erro. Mas não estamos checando para ver se ocorreu ou
não o erro, e o
compilador nos lembra que provavelmente queríamos
tratar algum código de erros aqui!
Vamos corrigir esse problema agora.
Verificamos erros e lidaremos com eles usando uma técnica semelhante à nossa
manipulação de erros com Config::new na Listagem 12-10, mas com umas
diferenças:
Arquivo: src/main.rs
fn main() {
// --snip--
process::exit(1);
O nosso projeto minigrep parece estar bem até agora! Agora vamos dividir o
src/main.rs e
colocar algum código no arquivo src/lib.rs para que possamos testá-lo
em um arquivo
src/main.rs com menos responsabilidades.
Vamos mover todo o código que não é da função main de src/main.rs para
src/lib.rs:
Arquivo: src/lib.rs
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
impl Config {
// --snip--
// --snip--
Nós fizemos um uso liberal do pub aqui: no Config , seus campos e seu método new
, e na
função run . Agora temos uma crate de biblioteca que tem uma
API pública que podemos
testar!
Agora, precisamos trazer o código que nós movemos para src/lib.rs no escopo da
crate
binária em src/main.rs, conforme mostrado na Listagem 12-14:
Arquivo: src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
// --snip--
// --snip--
Para colocar a crate de biblioteca na crate binária, usamos extern crate minigrep . Em
seguida, adicionaremos uma linha use minigrep::Config para trazer para o escopo o tipo
Config , e iremos prefixar a funão run com o nome da nossa crate. Agora
todas as
funcionalidades devem estar conectadas e devem funcionar. Execute o programa com
Ufa! Isso foi trabalhoso, mas nós nos preparamos para o sucesso no
futuro. Agora é muito
mais fácil lidar com erros, e nós fizemos o código mais
modular. Quase todo o nosso
trabalho será feito em src/lib.rs a partir daqui.
Vamos aproveitar desta nova recém-descoberta modularidade para fazer algo que seria
difícil com o código antigo, mas é fácil com o novo código: nós iremos
escreva alguns testes!
1. Escreva um teste que falha e execute-o, para certificar-se de que ele falha pelo motivo
esperado por você.
2. Escreva ou modifique o código apenas o suficiente para fazer passar no teste.
3. Refatore o código que você acabou de adicionar ou alterou e certifique-se de que os
testes
continuam a passar.
4. Repita a partir do passo 1!
Este processo é apenas uma das muitas maneiras de escrever software, mas o TDD pode
ajudar a conduzir
design de código também. Escrevendo o teste antes de escrever o código
que faz o
teste passar, ajuda a manter uma alta cobertura de teste ao longo do processo.
Arquivo: src/lib.rs
#[cfg(test)]
mod test {
use super::*;
#[test]
fn one_result() {
Rust:
Pick three.";
assert_eq!(
search(query, contents)
);
Este teste procura a string “duct”. O texto que estamos procurando contém três
linhas,
apenas uma das quais contém “duct.” Afirmamos que o valor retornado
a partir da função
search contém apenas a linha que esperamos.
Não somos capazes de executar este teste e vê-lo falhar porque o teste nem mesmo
compila: a função search ainda não existe! Então, agora vamos adicionar código apenas o
suficiente
para obter a compilação do teste, e executar, adicionando uma definição da
função search
que sempre retorna um vetor vazio, como mostrado na Listagem 12-16.
Então
o teste deve compilar e falhar porque um vetor vazio não corresponde a um vetor
contendo a linha "safe, fast, productive." .
Arquivo: src/lib.rs
vec![]
Em outras palavras, dizemos ao Rust que os dados retornados pela função search
irá viver
enquanto os dados passarem para a função search no
argumento de contents . Isso é
importante! Os dados referenciados por um pedaço precisa
ser válido para que a referência
seja válida; se o compilador assume que estamos fazendo
pedaços de string de query em
vez de contents , ele fará sua verificação de segurança
incorretamente.
--> src/lib.rs:5:51
| ^ expected lifetime
parameter
= help: this function's return type contains a borrowed value, but the
Rust não consegue saber qual dos dois argumentos que precisamos, então precisamos
informar
isto. Porque contents é o argumento que contém todo o nosso texto e nós
queremos retornar as partes desse texto que combinam, sabemos que o contents é o
argumento que deve ser conectado ao valor de retorno usando a sintaxe de lifetime.
Outras linguagens de programação não exigem que você conecte argumentos para retornar
valores na assinatura, por isso, embora isso possa parecer estranho, ele ficará
mais fácil ao
longo do tempo. Você pode querer comparar este exemplo com a seção “Validando
Referências com Lifetimes” no Capítulo 10.
--warnings--
Running target/debug/deps/minigrep-abcabcabc
running 1 test
failures:
right)`
failures:
test::one_result
Ótimo, o teste falha, exatamente como esperávamos. Vamos fazer o teste passar!
Atualmente, nosso teste está falhando porque sempre devolvemos um vetor vazio. Para
consertar isso é preciso implementar search , nosso programa precisa seguir essas etapas:
Rust tem um método útil para lidar com a iteração linha-a-linha de strings,
convenientemente chamado lines , que funciona como mostrado na Listagem 12-17.
Observe que isso
ainda não compilará:
Arquivo: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
Arquivo: src/lib.rs
if line.contains(query) {
Nós também precisamos de uma maneira de armazenar as linhas que contêm nossa string
de consulta. Por isso,
podemos fazer um vetor mutável antes do loop for e chamar o
método push
para armazenar uma line no vetor. Após o loop for , devolvemos o vetor,
como
mostrado na Listagem 12-19:
Arquivo: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
if line.contains(query) {
results.push(line);
results
Agora, a função search deve retornar apenas as linhas que contêm query ,
e nosso teste
deve passar. Vamos executar o teste:
$ cargo test
--snip--
running 1 test
Agora que a função search está funcionando e testada, precisamos chamar search
da
nossa função run . Precisamos passar o valor config.query e o contents que run lê do
arquivo para a função search . Então, run
irá imprimir cada linha retornada de search :
Arquivo: src/lib.rs
pub fn run(config: Config) -> Result<(), Box<Error>> {
f.read_to_string(&mut contents)?;
println!("{}", line);
Ok(())
Ainda estamos usando um loop for para retornar cada linha de search e imprimi-lo.
Agora, todo o programa deve funcionar! Vamos tentar, primeiro, com uma palavra que
deve
retornar exatamente uma linha do poema de Emily Dickinson, “frog”:
Legal! Agora vamos tentar uma palavra que combine várias linhas, como “body”:
E, finalmente, vamos nos certificar de que não recebemos nenhuma linha quando
buscamos uma
palavra que não está em qualquer lugar no poema, como
“monomorphization”:
Excelente! Nós construímos nossa própria mini versão de uma ferramenta clássica e
aprendemos muito
sobre como estruturar aplicativos. Também aprendemos um pouco
sobre a entrada de arquivos
e saída, lifetimes, teste e análise de linha de comando.
Arquivo: src/lib.rs
#[cfg(test)]
mod test {
use super::*;
#[test]
fn case_sensitive() {
Rust:
Pick three.
Duct tape.";
assert_eq!(
search(query, contents)
);
#[test]
fn case_insensitive() {
Rust:
Pick three.
Trust me.";
assert_eq!(
search_case_insensitive(query, contents)
);
Note que também editamos o contents do antigo teste. Adicionamos uma nova linha
com
o texto “Duct tape” usando um D maiúsculo que não deve corresponder à consulta
“duct”
quando procuramos de forma sensível à maiúsculas e minúsculas. Alterando o teste antigo
desta forma, ajuda a garantir que não quebramos acidentalmente a diferenciação de
maiúsculas e minúsculas
na funcionalidade de pesquisa que já implementamos. Este teste
deve passar agora
e deve continuar a passar enquanto trabalhamos na pesquisa insensível
à maiúsculas e minúsculas.
O novo teste para a pesquisa insensível usa “rUsT” para sua consulta. Na função
Arquivo: src/lib.rs
if line.to_lowercase().contains(&query) {
results.push(line);
results
Primeiro, caixa baixa na string query e a armazenamos em uma variável sombreada com
o
mesmo nome. Chamar to_lowercase na consulta é necessário, portanto, não importa
se a
consulta do usuário é “rust”, “RUST”, “Rust”, ou “rUsT”, trataremos a
consulta como se fosse
“rust” sendo insensível ao caso.
Note que query é agora uma String ao invés de um fatia de string, porque chamar
to_lowercase cria novos dados em vez de referenciar dados existentes. Suponha que
a
consulta é “rUsT”, por exemplo: essa fatia de string não contém minúsculas
“u” ou “t” para
nós usarmos, então temos que alocar uma nova String contendo
“rust”. Quando
passamos query como um argumento para o método contains agora, nós
precisamos
adicionar um ampersand (&) porque a assinatura de contains é definida para
uma fatia de
string.
running 2 tests
Arquivo: src/lib.rs
Arquivo: src/lib.rs
f.read_to_string(&mut contents)?;
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
println!("{}", line);
Ok(())
Arquivo: src/lib.rs
use std::env;
// --snip--
impl Config {
if args.len() < 3 {
Aqui, criamos uma nova variável case_sensitive . Para definir seu valor, chamamos a
função env::var e passamos o nome da variável de ambiente CASE_INSENSITIVE
. O
método env::var retorna um Result que será o sucesso
variante Ok que contém o valor
da variável de ambiente se a
variável de ambiente está definida. Ele retornará a variante
Err se a
variável de ambiente não está definida.
Parece que isso ainda funciona! Agora, vamos executar o programa com CASE_INSENSITIVE
definido como 1 mas com a mesma consulta “to”; devemos pegar linhas que contenham
“to”
que possam ter letras maiúsculas:
To an admiring bog!
$ $env.CASE_INSENSITIVE=1
Excelente, também temos linhas contendo “To”! Nosso programa minigrep agora pode
fazer
busca insensível a maiúsculas e minúsculas controlada por uma variável de ambiente.
Agora você sabe como gerenciar as opções definidas usando argumentos de linha de
comando ou variáveis de ambiente!
O módulo std::env contém muitos mais recursos úteis para lidar com
variáveis de
ambiente: confira sua documentação para ver o que está disponível.
A função println! só é capaz de imprimir na saída padrão, então temos que usar outra
coisa para imprimir em erro padrão.
Primeiro, vamos observar como o conteúdo impresso por minigrep está sendo gravado na
saída padrão, incluindo as mensagens de erro que desejamos gravar no erro padrão.
Faremos isso redirecionando o fluxo de saída padrão para um arquivo e, ao mesmo tempo,
causando um erro intencionalmente. Não redirecionamos o fluxo de erros padrão,
portanto, qualquer conteúdo enviado ao erro padrão continuará sendo exibido na tela.
Espera-se que os programas de linha de comando enviem mensagens de erro para o fluxo
erro padrão
, para que ainda possamos ver mensagens de erro na tela, mesmo se
redirecionarmos o fluxo de saída padrão para um arquivo. Nosso programa não está bem
comportado: estamos prestes a ver que ele salva a saída da mensagem de erro em um
arquivo!
A sintaxe > diz ao shell para gravar o conteúdo da saída padrão para
output.txt em vez da
tela. Nós não vimos a mensagem de erro que estávamos
esperando impresso na tela, o que
significa que deve ter acabado no
arquivo. Isto é o que o output.txt contém:
Sim, nossa mensagem de erro está sendo impressa na saída padrão. É muito mais útil que
mensagens de erro como essa sejam impressas no erro padrão e que somente os dados de
uma execução bem-sucedida acabem no arquivo quando redirecionamos a saída padrão
dessa maneira. Nós vamos mudar isso.
Usaremos o código da Listagem 12-24 para alterar a forma como as mensagens de erro são
impressas.
Por causa da refatoração que fizemos anteriormente neste capítulo, todo o
código que imprime mensagens de erro está em uma função, main . A biblioteca padrão
fornece a macro eprintln! que imprime no fluxo de erro padrão, então vamos alterar os
dois locais que estávamos chamando println! para imprimir erros para usar eprintln! :
Arquivo: src/main.rs
fn main() {
process::exit(1);
});
process::exit(1);
Agora vemos o erro na tela e o output.txt não contém nada, que é o comportamento
esperado dos programas de linha de comando.
Vamos executar o programa novamente com argumentos que não causam erro, mas ainda
redirecionamos a saída padrão para um arquivo, da seguinte forma:
Não veremos nenhuma saída para o terminal e output.txt conterá nossos resultados:
Arquivo: output.txt
Isso demonstra que agora estamos usando a saída padrão para saída bem-sucedida e erro
padrão para saída de erro, apropriadamente.
Resumo
Neste capítulo, recapitulamos alguns dos principais conceitos que você aprendeu até agora
e abordamos como realizar operações de E/S comuns em um contexto Rust. Usando
argumentos de linha de comando, arquivos, variáveis de ambiente e a macro eprintln!
para erros de impressão, você está preparado para escrever aplicativos de linha de
comando. Usando os conceitos dos capítulos anteriores, seu código será bem organizado,
armazenará dados de forma eficaz nas estruturas de dados apropriadas, tratará erros com
precisão e será bem testado.
Em seguida, exploraremos alguns recursos do Rust que foram influenciados por linguagens
funcionais: closures e iteradores.
Closures
Iterators
Performance
More about Cargo and Crates.io
Release Profiles
Publishing a Crate to Crates.io
Cargo Workspaces
Mergulhemos!
Boxes não têm custo adicional de desempenho além de armazenar dados no heap em
vez
de na pilha. Mas eles também não têm muitas habilidades a mais. Você irá
usá-los mais
comumente nestas situações:
Quando você tem um tipo cujo tamanho não é possível saber em tempo de
compilação, e você quer usar um valor desse tipo em um contexto que precisa
saber
um tamanho exato;
Quando você tem uma quantidade grande de dados e você quer transferir a posse
mas garantir que os dados não serão copiados quando você o fizer;
Quando você quer possuir um valor e só se importa se é um tipo que implementa
uma
trait específica, em vez de saber o tipo concreto.
Vamos demonstrar a primeira situação nesta seção. Mas antes disso, vamos falar
um pouco
mais sobre as outras duas situações: no segundo caso, transferir posse
de uma quantidade
grande de dados pode levar muito tempo porque os dados são
copiados de um lado para o
outro na pilha. Para melhorar o desempenho nessa
situação, podemos armazenar essa
quantidade grande de dados no heap em um box.
Assim, apenas uma quantidade pequena
de dados referentes ao ponteiro é copiada
na pilha, e os dados em si ficam em um lugar só
no heap. O terceiro caso é
conhecido como um objeto de trait (trait object), e o Capítulo 17
dedica uma
seção inteira somente a esse tópico. Então o que você aprender aqui você irá
aplicar de novo no Capítulo 17!
A Listagem 15-1 mostra como usar um box para armazenar um valor i32 no heap:
Arquivo: src/main.rs
fn main() {
let b = Box::new(5);
Nós definimos a variável b como tendo o valor de um Box que aponta para o
valor 5 , que
está alocado no heap. Esse programa irá imprimir b = 5 ; nesse
caso, podemos acessar o
dado no box de um jeito similar ao que usaríamos se esse
dado estivesse na pilha. Da
mesma forma que com qualquer valor possuído, quando
um box sai de escopo, como o b
no fim da main , ele é desalocado. A
desalocação acontece para o box (armazenado na
pilha) e para os dados aos quais
ele aponta (armazenados no heap).
Colocar um único valor no heap não é muito útil, então você normalmente não vai
usar
boxes sozinhos desse jeito. Ter valores como um único i32 na pilha, onde
são
armazenados por padrão, é mais apropriado para a maioria das situações.
Vamos dar uma
olhada em um caso onde o box nos possibilita definir tipos que não
poderíamos definir sem
ele.
Vamos explorar a lista ligada (cons list), que é um tipo de dados comum em
linguagens de
programação funcional, como um exemplo de tipo recursivo. O tipo
para lista ligada que
vamos definir é bem básico exceto pela recursão; portanto,
os conceitos no exemplo que
vamos trabalhar vão ser úteis sempre que você se
encontrar em situações mais complexas
envolvendo tipos recursivos.
Cada item em uma cons list contém dois elementos: o valor do item atual e o
próximo item.
O último item na lista contém apenas um valor chamado de Nil ,
sem um próximo item.
Uma cons list é produzida chamando-se recursivamente a
função cons . O nome canônico
que denota o caso base da recursão é Nil . Note
que isso não é o mesmo que o conceito de
"null" ou "nil" visto no Capítulo 6,
que é um valor inválido ou ausente.
A Listagem 15-2 contém uma definição de um enum para a cons list. Note que este
código
não compila ainda porque o tipo List não tem um tamanho conhecido, como
demonstraremos:
Arquivo: src/main.rs
enum List {
Cons(i32, List),
Nil,
Nota: estamos implementando uma cons list que guarda apenas valores i32 para
os
propósitos deste exemplo. Poderíamos tê-la implementado usando tipos
genéricos,
conforme discutimos no Capítulo 10, para definir um tipo cons list
que poderia
armazenar valores de qualquer tipo.
A listagem 15-3 mostra como fica o uso do tipo List para armazenar a lista
1, 2, 3 .
Arquivo: src/main.rs
use List::{Cons, Nil};
fn main() {
O primeiro valor Cons contém 1 e outro valor List . Esse valor List é
outro Cons que
contém 2 e outro valor List . Esse valor List é mais um
Cons que contém 3 e um valor
List , que finalmente é Nil , a variante não
recursiva que sinaliza o final da lista.
--> src/main.rs:1:1
1 | enum List {
2 | Cons(i32, List),
= ajuda: insira indireção (ex.: um `Box`, `Rc` ou `&`) em algum lugar para
O erro diz que esse tipo "tem tamanho infinito". A razão é que nós definimos
List com
uma variante que é recursiva: ela contém um outro valor de si mesma
diretamente. Como
resultado, o Rust não consegue determinar quanto espaço ele
precisa para armazenar um
valor List . Vamos analizar por partes por que
recebemos esse erro: primeiro, vamos ver
como o Rust decide quanto espaço
precisa para armazenar o valor de um tipo não
recursivo.
enum Mensagem {
Sair,
Escrever(String),
Para determinar quanto espaço alocar para um valor Mensagem , o Rust percorre
cada
variante para ver qual precisa de mais espaço. O Rust vê que
Mensagem::Sair não precisa
de nenhum espaço, Mensagem::Mover precisa de
espaço suficiente para armazenar dois
valores i32 , e assim por diante. Como
apenas uma variante será usada, o máximo de
espaço de que um valor Mensagem
vai precisar é o espaço que levaria para armazenar a
maior de suas variantes.
Contraste isso com o que acontece quando o Rust tenta determinar quanto espaço é
necessário para um tipo recursivo como o enum List na Listagem 15-2. O
compilador
começa olhando a variante Cons , que contém um valor do tipo i32 e
um valor do tipo
List . Portanto, Cons precisa de uma quantidade de espaço
igual ao tamanho de um i32
mais o tamanho de um List . Para determinar de
quanta memória o tipo List precisa, o
compilador olha para suas variantes,
começando com a Cons . A variante Cons contém um
valor do tipo i32 e um
valor do tipo List , e esse processo continua infinitamente,
conforme mostra a
Figura 15-1:
Como o Rust não consegue descobrir quanto espaço alocar para tipos definidos
recursivamente, o compilador dá o erro na Listagem 15-4. Mas o erro inclui esta
útil
sugestão:
= ajuda: insira indireção (ex.: um `Box`, `Rc` ou `&`) em algum lugar para
Como um Box<T> é um ponteiro, o Rust sempre sabe de quanto espaço ele precisa:
o
tamanho de um ponteiro não muda dependendo da quantidade de dados para a qual
ele
aponta. Isso significa que podemos colocar um Box<T> dentro da variante
Cons em vez de
outro valor List diretamente. O Box<T> vai apontar para o
próximo valor List , que vai
estar no heap em vez de dentro da variante Cons .
Conceitualmente, ainda temos uma lista,
criada de listas "contendo" outras
listas, mas essa implementação agora é mais como os
itens estando um do lado do
outro do que um dentro do outro.
Arquivo: src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
fn main() {
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
Boxes apenas proveem a indireção e a alocação no heap; eles não têm nenhuma
outra
habilidade especial, como as que vamos ver nos outros tipos de ponteiros
inteligentes. Eles
também não têm nenhum dos custos adicionais de desempenho que
essas habilidades
demandam, então eles podem ser úteis em casos como o da cons
list onde a indireção é a
única funcionalidade de que precisamos. No Capítulo
17 também vamos ver mais casos de
uso para as boxes.
Primeiro vamos ver como o * funciona com referências normais, e então vamos
tentar
definir nosso próprio tipo a la Box<T> e ver por que o * não funciona
como uma referência
no nosso tipo recém-criado. Vamos explorar como a trait
Deref torna possível aos
ponteiros inteligentes funcionarem de um jeito
similar a referências. E então iremos dar
uma olhada na funcionalidade de
coerção de desreferência (deref coercion) e como ela nos
permite trabalhar
tanto com referências quanto com ponteiros inteligentes.
Arquivo: src/main.rs
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
satisfeita
--> src/main.rs:6:5
6 | assert_eq!(5, y);
`{integer}`
Comparar um número com uma referência a um número não é permitido porque eles
são
de tipos diferentes. Devemos usar * para seguir a referência até o valor
ao qual ela está
apontando.
Podemos reescrever o código na Listagem 15-6 para usar um Box<T> em vez de uma
referência, e o operador de desreferência vai funcionar do mesmo jeito que na
Listagem 15-
7:
Arquivo: src/main.rs
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
A única diferença entre a Listagem 15-7 e a Listagem 15-6 é que aqui nós setamos
y para
ser uma instância de um box apontando para o valor em x em vez de uma
referência
apontando para o valor de x . Na última asserção, podemos usar o
operador de
desreferência para seguir o ponteiro do box do mesmo jeito que
fizemos quando y era
uma referência. A seguir, vamos explorar o que tem de
especial no Box<T> que nos permite
usar o operador de desreferência, criando
nosso próprio tipo box.
Vamos construir um smart pointer parecido com o tipo Box<T> fornecido pela
biblioteca
padrão para vermos como ponteiros inteligentes, por padrão, se
comportam diferente de
referências. Em seguida, veremos como adicionar a
habilidade de usar o operador de
desreferência.
O tipo Box<T> no fim das contas é definido como uma struct-tupla (tuple
struct) de um
elemento, então a Listagem 15-8 define um tipo MeuBox<T> da
mesma forma. Também
vamos definir uma função new como a definida no Box<T> :
Arquivo: src/main.rs
struct MeuBox<T>(T);
impl<T> MeuBox<T> {
MeuBox(x)
Arquivo: src/main.rs
fn main() {
let x = 5;
let y = MeuBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
--> src/main.rs:14:19
14 | assert_eq!(5, *y);
| ^^
Nosso tipo MeuBox<T> não pode ser desreferenciado porque não implementamos
essa
habilidade nele. Para habilitar desreferenciamento com o operador * ,
temos que
implementar a trait Deref .
Implementando a Trait Deref para Tratar um Tipo como uma Referência
Arquivo: src/main.rs
use std::ops::Deref;
type Target = T;
&self.0
A sintaxe type Target = T; define um tipo associado para a trait Deref usar.
Tipos
associados são um jeito ligeiramente diferente de declarar um parâmetro
genérico, mas
você não precisa se preocupar com eles por ora; iremos cobri-los
em mais detalhe no
Capítulo 19.
Nós preenchemos o corpo do método deref com &self.0 para que deref retorne
uma
referência ao valor que queremos acessar com o operador * . A função main
na Listagem
15-9 que chama * no valor MeuBox<T> agora compila e as asserções
passam!
Quando entramos *y na Listagem 15-9, por trás dos panos o Rust na verdade
rodou este
código:
*(y.deref())
Note que o * é substituído por uma chamada ao método deref e então uma
chamada ao
* apenas uma vez, cada vez que digitamos um * no nosso código.
Como a substituição do
* não entra em recursão infinita, nós terminamos com o
dado do tipo i32 , que
corresponde ao 5 em assert_eq! na Listagem 15-9.
Para ver a coerção de desreferência em ação, vamos usar o tipo MeuBox<T> que
definimos
na Listagem 15-8 e também a implementação de Deref que adicionamos
na Listagem 15-
10. A Listagem 15-11 mostra a definição de uma função que tem um
parâmetro do tipo
string slice:
Arquivo: src/main.rs
fn ola(nome: &str) {
Podemos chamar a função ola passando uma string slice como argumento, por
exemplo
ola("Rust"); . A coerção de desreferência torna possível chamar ola
com uma referência
a um valor do tipo MeuBox<String> , como mostra a Listagem
15-12:
Arquivo: src/main.rs
fn main() {
let m = MeuBox::new(String::from("Rust"));
ola(&m);
Aqui estamos chamando a função ola com o argumento &m , que é uma referência
a um
valor MeuBox<String> . Como implementamos a trait Deref em MeuBox<T>
na Listagem 15-
10, o Rust pode transformar &MeuBox<String> em &String
chamando deref . A biblioteca
padrão provê uma implementação de Deref para
String que retorna uma string slice,
documentada na API de Deref . O Rust
chama deref de novo para transformar o &String
em &str , que corresponde à
definição da função ola .
Arquivo: src/main.rs
fn main() {
let m = MeuBox::new(String::from("Rust"));
ola(&(*m)[..]);
Quando a trait Deref está definida para os tipos envolvidos, o Rust analisa os
tipos e usa
Deref::deref tantas vezes quanto necessário para chegar a uma
referência que
corresponda ao tipo do parâmetro. O número de vezes que
Deref::deref precisa ser
inserida é resolvido em tempo de compilação, então
não existe nenhuma penalidade em
tempo de execução para tomar vantagem da
coerção de desreferência.
Os primeiros dois casos são o mesmo exceto pela mutabilidade. O primeiro caso
afirma que
se você tem uma &T , e T implementa Deref para algum tipo U ,
você pode obter um &U
de maneira transparente. O segundo caso afirma que a
mesma coerção de desreferência
acontece para referências mutáveis.
O terceiro caso é mais complicado: o Rust também irá coagir uma referência
mutável a uma
imutável. Mas o contrário não é possível: referências imutáveis
nunca serão coagidas a
referências mutáveis. Por causa das regras de empréstimo,
se você tem uma referência
mutável, ela deve ser a única referência àqueles
dados (caso contrário, o programa não
compila). Converter uma referência mutável
a uma imutável nunca quebrará as regras de
empréstimo. Converter uma referência
imutável a uma mutável exigiria que houvesse
apenas uma referência imutável
àqueles dados, e as regras de empréstimo não garantem
isso. Portanto, o Rust não
pode assumir que converter uma referência imutável a uma
mutável seja possível.
Em algumas linguagens, a pessoa que está programando deve chamar código para
liberar
memória ou recursos toda vez que ela termina de usar uma instância de um
ponteiro
inteligente. Se ela esquece, o sistema pode ficar sobrecarregado e
falhar. No Rust, podemos
especificar que um pedaço específico de código deva ser
rodado sempre que um valor sair
de escopo, e o compilador irá inserir esse
código automaticamente. Assim, não precisamos
cuidadosamente colocar código de
limpeza em todos os lugares de um programa em que
uma instância de um tipo
específico deixa de ser usada, e ainda assim não vazaremos
recursos!
Para especificar o código que vai rodar quando um valor sair de escopo, nós
implementamos a trait Drop . A trait Drop requer que implementemos um método
chamado drop que recebe uma referência mutável de self . Para ver quando o
Rust
chama drop , vamos implementar drop com declarações de println! por
ora.
Arquivo: src/main.rs
struct CustomSmartPointer {
data: String,
fn drop(&mut self) {
fn main() {
println!("CustomSmartPointers criados.");
CustomSmartPointers criados.
O Rust chamou automaticamente drop para nós quando nossa instância saiu de
escopo,
chamando o código que especificamos. Variáveis são destruídas na ordem
contrária à de
criação, então d foi destruída antes de c . Esse exemplo serve
apenas para lhe dar um guia
visual de como o método drop funciona, mas
normalmente você especificaria o código de
limpeza que o seu tipo precisa rodar
em vez de imprimir uma mensagem.
Vamos ver o que acontece quando tentamos chamar o método drop da trait Drop
manualmente, modificando a função main da Listagem 15-14, conforme mostra a
Listagem
15-15:
Arquivo: src/main.rs
fn main() {
println!("CustomSmartPointer criado.");
c.drop();
Listagem 15-15: Tentando chamar o método drop da trait Drop manualmente para limpar
cedo
--> src/main.rs:14:7
14 | c.drop();
Essa mensagem de erro afirma que não nos é permitido chamar explicitamente
drop . A
mensagem de erro usa o termo destrutor, que é um termo geral de
programação para uma
função que limpa uma instância. Um destrutor é análogo a
um construtor, que cria uma
instância. A função drop em Rust é um destrutor
específico.
O Rust não nos deixa chamar drop explicitamente porque o drop ainda seria
chamado no
valor ao final da main . Isso seria um erro de liberação dupla
(double free) porque o Rust
estaria tentando limpar o mesmo valor duas vezes.
Nós não podemos desabilitar a inserção automática do drop quando um valor sai
de
escopo, e também não podemos chamar o método drop explicitamente. Então, se
precisamos forçar um valor a ser limpo antes, podemos usar a função
std::mem::drop .
Arquivo: src/main.rs
fn main() {
println!("CustomSmartPointer criado.");
drop(c);
CustomSmartPointer criado.
Também não temos que nos preocupar em acidentalmente limpar valores ainda em uso
porque isso causaria um erro de compilação: o sistema de posse que garante que
as
referências são sempre válidas também garante que o drop é chamado apenas
uma vez
quando o valor não está mais sendo usado.
Para permitir posse múltipla, o Rust tem um tipo chamado Rc<T> . Seu nome é uma
abreviação para reference counting (contagem de referências) que, como o
nome diz, mantém
registro do número de referências a um valor para saber se ele
ainda está em uso ou não.
Se há zero referências a um valor, ele pode ser
liberado sem que nenhuma referência se
torne inválida.
Imagine o Rc<T> como uma TV numa sala de família. Quando uma pessoa entra para
assistir à TV, ela a liga. Outros podem entrar na sala e assistir à TV. Quando a
última pessoa
sai da sala, ela desliga a TV porque essa não está mais em uso. Se
alguém desligasse a TV
enquanto outros ainda estão assistindo, haveria revolta
entre os telespectadores restantes!
Nós usamos o tipo Rc<T> quando queremos alocar algum dado no heap para que
múltiplas partes do nosso programa o leiam, e não conseguimos determinar em
tempo de
compilação qual parte irá terminar de usar o dado por último. Se
soubéssemos qual parte
terminaria por último, poderíamos simplesmente tornar
aquela parte a possuidora do dado
e as regras normais de posse aplicadas em
tempo de compilação teriam efeito.
Note que o Rc<T> serve apenas para cenários de thread única. Quando
discutirmos
concorrência no Capítulo 16, cobriremos como fazer contagem de
referências em
programas com múltiplas threads.
Vamos criar a lista a que contém 5 e depois 10. Então criaremos mais duas
listas: b , que
começa com 3 e c , que começa com 4. Ambas as listas b e c
irão então continuar na lista
a contendo 5 e 10. Em outras palavras, ambas as
listas irão compartilhar a primeira lista
contendo 5 e 10.
Tentar implementar esse cenário usando nossa definição de List com Box<T>
não irá
funcionar, como mostra a Listagem 15-17:
Arquivo: src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
fn main() {
let a = Cons(5,
Box::new(Cons(10,
Box::new(Nil))));
--> src/main.rs:13:30
= nota: o valor é movido porque `a` tem tipo `List`, que não implementa
a trait `Copy`
As variantes Cons têm posse dos dados que elas contêm, então quando criamos a
lista b ,
a é movida para dentro de b , e b toma posse de a . Então,
quando tentamos usar a de
novo na criação de c , não somos permitidos porque
a foi movida.
Em vez disso, vamos mudar nossa definição de List para usar o Rc<T> no lugar
do
Box<T> , como mostra a Listagem 15-18. Cada variante Cons agora vai conter
um valor e
um Rc<T> apontando para uma List . Quando criarmos b , em vez de
tomar posse de a ,
iremos clonar o Rc<List> que a está segurando, o que
aumenta o número de referências
de uma para duas e permite com que a e
b compartilhem posse dos dados naquele
Rc<List> . Também vamos clonar a
quando criarmos c , o que aumenta o número de
referências de duas para três.
Cada vez que chamarmos Rc::clone , a contagem de
referências ao valor dentro do
Rc<List> irá aumentar, e ele não será liberado até que haja
zero referências a
ele:
Arquivo: src/main.rs
enum List {
Cons(i32, Rc<List>),
Nil,
use std::rc::Rc;
fn main() {
Precisamos adicionar uma declaração use para trazer o Rc<T> ao escopo porque
ele não
está no prelúdio. Na main , criamos a lista contendo 5 e 10 e a
armazenamos em um novo
Rc<List> em a . Então quando criamos b e c ,
chamamos a função Rc::clone e
passamos uma referência ao Rc<List> em a
como argumento.
Vamos mudar nosso exemplo de trabalho na Listagem 15-18 para podermos ver a
contagem de referências mudando conforme criamos e destruímos referências ao
Rc<List> em a .
Na Listagem 15-19, vamos mudar a main para que tenha um escopo interno em
volta da
lista c ; assim poderemos ver como a contagem de referências muda
quando c sai de
escopo. Em cada ponto do programa onde a contagem de
referências muda, iremos
imprimir seu valor, que podemos obter chamando a função
Rc::strong_count . Essa
função se chama strong_count (contagem das
referências fortes) em vez de count
(contagem) porque o tipo Rc<T> também
tem uma weak_count (contagem das referências
fracas); veremos para que a
weak_count é usada na seção "Evitando Ciclos de Referências".
Arquivo: src/main.rs
fn main() {
O que não conseguimos ver nesse exemplo é que quando b e depois a saem de
escopo
no final da main , a contagem se torna 0, e o Rc<List> é
liberado por completo nesse
ponto. O uso do Rc<T> permite que um único valor
tenha múltiplos possuidores, e a
contagem garante que o valor permaneça válido
enquanto algum dos possuidores ainda
existir.
Para explorar este conceito, vamos ver o tipo RefCell<T> que segue a pattern
de
mutabilidade interior.
Em qualquer momento, você pode ter um dos mas não ambos os seguintes: uma
única
referência mutável ou qualquer número de referências imutáveis;
Referências devem sempre ser válidas.
Aqui está uma recapitulação das razões para escolher o Box<T> , o Rc<T> ou o
RefCell<T> :
Uma consequência das regras de empréstimo é que quando temos um valor imutável,
nós
não podemos pegá-lo emprestado mutavelmente. Por exemplo, este código não
compila:
fn main() {
let x = 5;
let y = &mut x;
erro[E0596]: não posso pegar emprestado a variável local imutável `x` como
mutável
--> src/main.rs:3:18
2 | let x = 5;
3 | let y = &mut x;
Vamos trabalhar com um exemplo prático onde podemos usar o RefCell<T> para
modificar um valor imutável e ver por que isto é útil.
Rust não tem objetos da mesma forma que outras linguagens, e não tem
funcionalidade de
objetos simulados embutida na biblioteca padrão como algumas
outras linguagens.
Contudo, certamente podemos criar uma struct que serve os
mesmos propósitos que um
objeto simulado.
Eis o cenário que vamos testar: vamos criar uma biblioteca que acompanha um
valor contra
um valor máximo e envia mensagens com base em quão próximo do valor
máximo o valor
atual está. Esta biblioteca pode ser usada para acompanhar a cota
de um usuário para o
número de chamadas de API que ele tem direito a fazer, por
exemplo.
mensageiro: &'a T,
valor: usize,
max: usize,
where T: Mensageiro {
AvisaLimite {
mensageiro,
valor: 0,
max,
self.valor = valor;
Uma parte importante deste código é que a trait Mensageiro tem um método
chamado
enviar que recebe uma referência imutável a self e o texto da
mensagem. Esta é a
interface que nosso objeto simulado precisa ter. A outra
parte importante é que queremos
testar o comportamento do método set_valor no
AvisaLimite . Podemos mudar o que
passamos para o parâmetro valor , mas o
set_valor não retorna nada sobre o qual
possamos fazer asserções. Queremos
poder dizer que se criarmos um AvisaLimite com
algo que implemente a trait
Mensageiro e um valor específico de max , quando passarmos
diferentes números
para o valor , o mensageiro receberá o comando para enviar as
mensagens
apropriadas.
Precisamos de um objeto simulado que, em vez de enviar um email ou mensagem de
texto
quando chamarmos enviar , irá apenas registrar as mensagens que recebeu
para enviar.
Podemos criar uma nova instância do objeto simulado, criar um
AvisaLimite que use o
objeto simulado, chamar o método set_valor no
AvisaLimite , e então verificar se o
objeto simulado tem as mensagens que
esperamos. A Listagem 15-21 mostra uma tentativa
de implementar um objeto
simulado para fazer exatamente isto, mas que o borrow checker
não permite:
Arquivo: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
struct MensageiroSimulado {
mensagens_enviadas: Vec<String>,
impl MensageiroSimulado {
self.mensagens_enviadas.push(String::from(mensagem));
#[test]
fn envia_uma_mensagem_de_aviso_de_acima_de_75_porcento() {
avisa_limite.set_valor(80);
assert_eq!(mensageiro_simulado.mensagens_enviadas.len(), 1);
Este código de teste define uma struct MensageiroSimulado que tem um campo
--> src/lib.rs:52:13
52 | self.sent_messages.push(String::from(message));
campo imutável
Esta é uma situação em que a mutabilidade interior pode ajudar! Vamos armazenas
as
mensagens_enviadas dentro de um RefCell<T> , e então o método enviar
poderá
modificar mensagens_enviadas para armazenar as mensagens que já vimos.
A Listagem 15-
22 mostra como fica isto:
Arquivo: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MensageiroSimulado {
mensagens_enviadas: RefCell<Vec<String>>,
impl MensageiroSimulado {
self.mensagens_enviadas.borrow_mut().push(String::from(mensagem));
#[test]
fn envia_uma_mensagem_de_aviso_de_acima_de_75_porcento() {
// --snip--
assert_eq!(mensageiro_simulado.mensagens_enviadas.borrow().len(), 1);
A última mudança que temos que fazer é na asserção: para ver quantos itens estão
no vetor
interno, chamamos borrow no RefCell<Vec<String>> para obter uma
referência imutável
ao vetor.
Agora que você viu como usar o RefCell<T> , vamos nos aprofundar em como ele
funciona!
Arquivo: src/lib.rs
emprestimo_um.push(String::from(mensagem));
emprestimo_dois.push(String::from(mensagem));
thread 'tests::envia_uma_mensagem_de_aviso_de_acima_de_75_porcento'
entrou
em pânico em
Por exemplo, lembre-se da cons list na Listagem 15-18 onde usamos o Rc<T> para
nos
permitir que múltiplas listas compartilhassem posse de outra lista. Como o
Rc<T> guarda
apenas valores imutáveis, nós não podemos modificar nenhum dos
valores na lista uma vez
que os criamos. Vamos adicionar o RefCell<T> para
ganhar a habilidade de mudar os
valores nas listas. A Listagem 15-24 mostra que,
usando um RefCell<T> na definição do
Cons , podemos modificar o valor
armazenado em todas as listas:
Arquivo: src/main.rs
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
*valor.borrow_mut() += 10;
A biblioteca padrão tem outros tipos que proveem mutabilidade interior, como o
Cell<T> ,
que é parecido, exceto que em vez de dar referências ao valor
interno, o valor é copiado
para dentro e para fora do Cell<T> . Tem também o
Mutex<T> , que oferece mutabilidade
interior que é segura de usar entre
threads; vamos discutir seu uso no Capítulo 16. Confira
a documentação da
biblioteca padrão para mais detalhes sobre as diferenças entre estes
tipos.
Vamos dar uma olhada em como um ciclo de referências poderia acontecer e como
preveni-
lo, começando com a definição do enum List e um método tail
(cauda) na Listagem 15-
25:
Arquivo: src/main.rs
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
impl List {
match *self {
Na Listagem 15-26, estamos adicionando uma função main que usa as definições
da
Listagem 15-25. Este código cria uma lista em a e uma lista em b que
aponta para a lista
em a , e depois modifica a lista em a para apontar para
b , o que cria um ciclo de
referências. Temos declarações de println! ao
longo do caminho para mostrar quais são
as contagens de referências em vários
pontos do processo:
Arquivo: src/main.rs
fn main() {
*link.borrow_mut() = Rc::clone(&b);
// Descomente a próxima linha para ver que temos um ciclo; ela irá
// estourar a pilha
Nós modificamos a para que aponte para b em vez de Nil , o que cria um
ciclo. Fazemos
isso usando o método tail para obter uma referência ao
RefCell<Rc<List>> em a , a
qual colocamos na variável link . Então usamos o
método borrow_mut no
RefCell<Rc<List>> para modificar o valor interno: de um
Rc<List> que guarda um valor
Nil para o Rc<List> em b .
Quando rodamos esse código, mantendo o último println! comentado por ora,
obtemos
esta saída:
Criar ciclos de referências não é fácil de fazer, mas também não é impossível.
Se você tem
valores RefCell<T> que contêm valores Rc<T> ou combinações
aninhadas de tipos
parecidas, com mutabilidade interior e contagem de
referências, você deve se assegurar de
que não está criando ciclos; você não
pode contar com o Rust para pegá-los. Criar ciclos de
referências seria um erro
de lógica no seu programa, e você deve usar testes
automatizados, revisões de
código e outras práticas de desenvolvimento de software para
minimizá-los.
Referências fortes são o modo como podemos compartilhar posse de uma instância
Rc<T> .
Referências fracas não expressam uma relação de posse. Elas não irão
causar um ciclo de
referências porque qualquer ciclo envolvendo algumas
referências fracas será quebrado
uma vez que a contagem de referências fortes
dos valores envolvidos for 0.
Como o valor ao qual o Weak<T> faz referência pode ter sido destruído, para
fazer qualquer
coisa com ele, precisamos nos assegurar de que ele ainda exista.
Fazemos isso chamando o
método upgrade na instância Weak<T> , o que nos
retornará uma Option<Rc<T>> . Iremos
obter um resultado de Some se o valor do
Rc<T> ainda não tiver sido destruído e um
resultado de None caso ele já
tenha sido destruído. Como o upgrade retorna uma
Option<T> , o Rust irá
garantir que lidemos com ambos os casos Some e None , e não
haverá um
ponteiro inválido.
Como exemplo, em vez de usarmos uma lista cujos itens sabem apenas a respeito do
próximo item, iremos criar uma árvore cujos itens sabem sobre seus itens filhos
e sobre
seus itens pais.
Para começar, vamos construir uma árvore com vértices que saibam apenas sobre
seus
vértices filhos. Iremos criar uma estrutura chamada Vertice que contenha
seu próprio
valor i32 , além de referências para seus valores filhos do tipo
Vertice :
Arquivo: src/main.rs
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Vertice {
valor: i32,
filhos: RefCell<Vec<Rc<Vertice>>>,
Arquivo: src/main.rs
fn main() {
valor: 3,
filhos: RefCell::new(vec![]),
});
valor: 5,
filhos: RefCell::new(vec![Rc::clone(&folha)]),
});
Para tornar o vértice filho ciente de seu pai, precisamos adicionar um campo
pai a nossa
definição da struct Vertice . O problema é decidir qual deveria
ser o tipo de pai . Sabemos
que ele não pode conter um Rc<T> porque isso
criaria um ciclo de referências com
folha.pai apontando para galho e
galho.filhos apontando para folha , o que faria
com que seus valores de
strong_count nunca chegassem a 0.
Pensando sobre as relações de outra forma, um vértice pai deveria ter posse de
seus filhos:
se um vértice pai é destruído, seus vértices filhos também deveriam
ser. Entretanto, um
filho não deveria ter posse de seu pai: se destruirmos um
vértice filho, o pai ainda deveria
existir. Esse é um caso para referências
fracas!
Então em vez de Rc<T> , faremos com que o tipo de pai use Weak<T> , mais
especificamente um RefCell<Weak<Vertice>> . Agora nossa definição da struct
Vertice
fica assim:
Arquivo: src/main.rs
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Vertice {
valor: i32,
pai: RefCell<Weak<Vertice>>,
filhos: RefCell<Vec<Rc<Vertice>>>,
Agora um vértice pode se referir a seu vértice pai, mas não tem posse dele. Na
Listagem 15-
28, atualizamos a main com essa nova definição para que o vértice
folha tenha um jeito
de se referir a seu pai, galho :
Arquivo: src/main.rs
fn main() {
valor: 3,
pai: RefCell::new(Weak::new()),
filhos: RefCell::new(vec![]),
});
valor: 5,
pai: RefCell::new(Weak::new()),
filhos: RefCell::new(vec![Rc::clone(&folha)]),
});
*folha.pai.borrow_mut() = Rc::downgrade(&galho);
Nesse ponto, quando tentamos obter uma referência ao pai de folha usando o
método
upgrade , recebemos um valor None . Vemos isso na saída do primeiro
comando println! :
Quando criamos o vértice galho , ele também tem uma nova referência
Weak<Vertice> no
campo pai , porque galho não tem um vértice pai. Nós ainda
temos folha como um dos
filhos de galho . Uma vez que temos a instância de
Vertice em galho , podemos
modificar folha para lhe dar uma referência
Weak<Vertice> a seu pai. Usamos o método
borrow_mut do
RefCell<Weak<Vertice>> no campo pai de folha , e então usamos a
função
Rc::downgrade para criar uma referência Weak<Vertice> a galho a partir do
Rc<Vertice> em galho .
Quando imprimimos o pai de folha de novo, dessa vez recebemos uma variante
Some
contendo galho : agora folha tem acesso a seu pai! Quando imprimimos
folha , nós
também evitamos o ciclo que eventualmente terminou em um estouro de
pilha como o que
tivemos na Listagem 15-26: as referências Weak<Vertice> são
impressas como (Weak) :
A falta de saída infinita indica que esse código não criou um ciclo de
referências. Também
podemos perceber isso olhando para os valores que obtemos ao
chamar
Rc::strong_count e Rc::weak_count .
Arquivo: src/main.rs
fn main() {
valor: 3,
pai: RefCell::new(Weak::new()),
filhos: RefCell::new(vec![]),
});
println!(
Rc::strong_count(&folha),
Rc::weak_count(&folha),
);
valor: 5,
pai: RefCell::new(Weak::new()),
filhos: RefCell::new(vec![Rc::clone(&folha)]),
});
*folha.pai.borrow_mut() = Rc::downgrade(&galho);
println!(
Rc::strong_count(&galho),
Rc::weak_count(&galho),
);
println!(
Rc::strong_count(&folha),
Rc::weak_count(&folha),
);
println!(
Rc::strong_count(&folha),
Rc::weak_count(&folha),
);
Depois que folha é criada, seu Rc<Vertice> tem uma strong count de 1 e uma
weak count
de 0. Dentro do escopo interno, criamos galho e o associamos a
folha . Nesse ponto,
quando imprimimos as contagens, o Rc<Vertice> em galho
tem uma strong count de 1 e
uma weak count de 1 (porque folha.pai aponta para
galho com uma Weak<Vertice> ).
Quando imprimirmos as contagens de folha ,
veremos que ela terá uma strong count de 2,
porque galho agora tem um clone do
Rc<Vertice> de folha armazenado em
galho.filhos , mas ainda terá uma weak
count de 0.
Quando o escopo interno termina, galho sai de escopo e a strong count do
Rc<Vertice>
diminui para 0, e então seu Vertice é destruído. A weak count de
1 por causa de
folha.pai não tem nenhuma influência sobre se Vertice é
destruído ou não, então não
temos nenhum vazamento de memória!
Resumo
Esse capítulo cobriu como usar ponteiros inteligentes para fazer garantias e
trade-offs
diferentes daqueles que o Rust faz por padrão com referências
normais. O tipo Box<T> tem
um tamanho conhecido e aponta para dados alocados
no heap. O tipo Rc<T> mantém
registro do número de referências a dados no
heap, para que eles possam ter múltiplos
possuidores. O tipo RefCell<T> com
sua mutabilidade interior nos dá um tipo que
podemos usar quando precisamos de
um tipo imutável mas precisamos mudar um valor
interno ao tipo; ele também
aplica as regras de empréstimo em tempo de execução em vez
de em tempo de
compilação.
Também foram discutidas as traits Deref e Drop que tornam possível muito da
funcionalidade dos ponteiros inteligentes. Exploramos ciclos de referências que
podem
causar vazamentos de memória e como preveni-los usando Weak<T> .
Se esse capítulo tiver aguçado seu interesse e você quiser implementar seus
próprios
ponteiros inteligentes, dê uma olhada no "Rustnomicon" em
https://doc.rust-
lang.org/stable/nomicon/ para mais informação útil.
Concurrency
Usando essa definição, Rust é orientada a objetos: structs e enums têm dados
e os blocos
impl fornecem métodos em structs e enums. Embora structs e
enums com métodos não
sejam chamados de objetos, eles fornecem a mesma
funcionalidade, de acordo com a
definição de objetos de Gangue dos Quatro.
lista: Vec<i32>,
media: f64,
A estrutura é marcada como pub , portanto, outro código possa usá-la, mas os campos
dentro
da estrutura permanecem privados. Isso é importante nesse caso porque queremos
garantir que sempre que um valor seja adicionado ou removido da lista, a média também
seja
atualizada. Fazemos isso implementando os métodos adicionar remover e media
na
estrutura, conforme na Listagem 17-2:
self.lista.push(valor);
self.atualizar_media();
match resultado {
Some(valor) => {
self.atualizar_media();
Some(valor)
},
self.media
fn atualizar_media(&mut self) {
Se a linguagem precisa ter herança para ser uma linguagem orientada a objetos, então
Rust
nao é. Não há como definir uma estrutura que herde
os campos e implementações de
métodos da estrutura pai. No entanto, se você está acostumado a usar
herança nos seus
programas, pode usar uma outra solução em Rust,
dependendo da sua razão pra obter a
herança em primeiro lugar.
Você escolhe herança por dois motivos principais. Uma é para reuso de código: você pode
implementar comportamento específico para um tipo e herança permite que você
reutilize
essa implementação para um tipo diferente. Você pode compartilhar códigos em Rust
usando
implementações do método de característica padrão, que você viu na Listagem 10-
14,
quando adicionamos uma implementação padrão do método resumir no
trait Resumo .
Qualquer tipo de implementação de trait Resumo teria o
método resumir disponível sem
precisar de outro código. Isso é semelhante a
uma classe pai tendo uma imeplementação
de um método e uma classe filha
herdando a implementação do método. Também
podemos sobrescrever a
implementação padrão do método resumir quando
implementamos o
trait Resumo , que é similar a uma classe filha que sobrescreve a
implementação do método herdado da classe pai.
A outra razão para usar herança diz respeito ao sistema de tipos: permitir que um
tipo filho
seja usado nos mesmos lugares que o tipo pai. Isso também é
chamado de polimorfismo, o
que significa que você pode subistituir vários objetos um pelo
outro em tempo de execução
se eles compartilham certas características.
Polimorfismo
Alternativamente, Rust isa genéricos para abstrair sobre diferentes tipos possíveis e
trait bounds para impor restrições no que esses tipos devem fornecer. As vezes,
isso é
chamado de polimorfismo paramétrico limitado
Recentemente, herança caiu em desuso como uma solução de design de programação
em
muitas linguagens de programação, porque muitas vezes corre o risco de compartilhar mais
código
que o necessário. As subclasses nem sempre devem compartilhar todas as
características de sua
classe pai, mas o farão com herança. Isso pode fazer o design do
programa
menos flexível e introduzir a possibilidade de chamar métodos nas subclasses
que não fazem sentido ou que causam erros porque os métodos não se aplicam
à
subclasse. Algumas linguagens também só permitem que uma subclasse herde de
uma
classe, restringindo a flexibilidade do design do programa.
Por esses motivos, Rust usa abordagens diferentes, usando objetos trait em vez
de herança.
Vamos ver como objetos trait possibilitam o polimorfismo em Rust.
No entanto, algumas vezes queremos que nosso usuário de biblioteca seja capaz de
estender o conjunto de
tipos que são válidos em uma situação específica. Para mostrar
como podemos alcançar
isso, criaremos um exemplo de ferramenta de interface gráfica
(GUI) que interage
através de uma lista de itens, chamando um método desenhar em cada
um para desenhá-lo
na tela - uma técnica comum para ferramentas GUI. Criaremos uma
crate chamada gui que contém a estrutura da biblioteca GUI. Essa crate pode incluir
alguns tipos para as pessoas usarem, como um Button ou TextField . Além disso,
usuários de gui vão querer criar seus próprios tipos que podem ser desenhados: por
exemplo, um programados pode adicionar uma Image e outro pode adicionar um
SelectBox .
Não implementamos uma biblioteca gráfica completa para esse exemplo, mas
mostraremos
como as peças se encaixariam. No momento de escrever a biblioteca, não
podemos
saber e definir todos os tipos que outros programadores podem querer criar. Mas
sabemos
que gui precisa manter o controle de diferentes valores de diferentes tipos e ele
precisa chamar o método desenhar em cada um desses diferentes tipos de valores. Não
é
necessário saber exatamente o que acontecerá quando chamarmos o método desenhar ,
apenas que o valor tera este método disponível para executarmos.
Para fazer isso em uma linguagem com herança, podemos definir uma classe chamada
Para implementar o comportamento que queremos que gui tenha, definiremos um trait
chamado
Draw que terá um método chamado desenhar . Então podemos definir um vetor
que tenha um objeto trait. Um objeto trait aponta para uma instância de um tipo que
implmenta o trait que especificamos. Criamos um objeto trait especificando alguns
tipos de
ponteiros, como uma referência & ou um ponteiro Box<T> e
especificando um trait
relevante (falaremos sobre o motimo pelo qual os objetos trait
devem ser usados no
Capítulo 19, na seção "Tipos e tamanhos dimensionados dinamicamente").
Podemos usar
objetos trait no lugar de um tipo genérico ou concreto. Onde quer que usemos
um objeto
trait, o sistema de tipos do Rust irá garantir em tempo de compilação que qualquer
valor
usado nesse contexto implementará o trait de um objeto trait.
Consequentemente, não
precisamos saber todos os possíveis tipos em tempo de compilação.
Listagem 17-3 mostra como definir um trait chamado Draw com um método chamado
desenhar :
Arquivo: src/lib.rs
fn desenhar(&self);
Arquivo: src/lib.rs
Arquivo: src/lib.rs
impl Janela {
pub fn executar(&self) {
component.desenhar();
Isso funciona de forma diferente do que definir uma estrutura que usa um parâmetro de
tipo
genérico com trait bounds. Um parâmetro de tipo genérico pode
apenas ser
substituido por um tipo concreto de cada vez, enquanto objetos trait permitem vários tipos
concretos para preencher o objeto trait em tempo de execução. Por exemplo, poderíamos
ter definido a estrutura Janela usando um tipo genérico e um trait bounds
como na
Listagem 17-6:
Arquivo: src/lib.rs
pub struct Janela<T: Draw> {
impl<T> Janela<T>
where T: Draw {
pub fn executar(&self) {
component.desenhar();
Isso nos restringe a uma instância de Janela que tem uma lista de componentes, todos
do
tipo Button ou do tipo TextField . Se você tiver somente coleções do mesmo tipo,
usar
genéricos e trait bounds é preferível, porque as
definições serão monomorfizadas em
tempo de compilação para os tipos concretos.
Por outro lado, com o método usando objetos trait, uma instância de Janela
pode conter
um Vec que contém um Box<Button> assim como um Box<TextField> .
Vamos ver como
isso funciona e falaremos sobre as impliciações do desempenho
em tempo de compilação.
Implementando o Trait
Arquivo: src/lib.rs
fn desenhar(&self) {
Se alguém estiver usando nossa biblioteca para implementar a estrutura SelectBox que
tem
os campos largura , altura e opcoes , eles implementam o trait Draw no tipo
SelectBox , como mostra a Listagem 17-8:
Arquivo: src/main.rs
use gui::Draw;
struct SelectBox {
largura: u32,
altura: u32,
opcoes: Vec<String>,
fn desenhar(&self) {
Os usuários da nosso biblioteca agoora podem escrever suas funções main para criar uma
instância de Janela . Para a instância de Janela , eles podem adicionar um SelectBox e
um Button
colocando cada um em um Box<T> para se tornar um objeto trait. Eles podem
chamar o
método executar na instância de Janela , que irá chamar o desenhar para cada
um dos
componentes. A Listagem 17-9 mostra essa implementação:
Arquivo: src/main.rs
use gui::{Janela, Button};
fn main() {
componentes: vec![
Box::new(SelectBox {
largura: 75,
altura: 10,
opcoes: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No")
],
}),
Box::new(Button {
largura: 50,
altura: 10,
label: String::from("OK"),
}),
],
};
screen.executar();
Quando escrevemos uma biblioteca, não sabemos o que alguém pode adicionar ao
tipo
SelectBox , mas nossa implementação de Janela foi capaz de operar no
novo tipo e
desenhá-lo, porque SelectBox implementa o tipo Draw , o que
significa que ele
implementa o método desenhar .
A vantagem de usar objetos trait e o sistema de tipos do Rust para escrever códigos
semelhante ao código usando duck typing é que nunca precisamos verificar se um valor
implementa umm método em particular no tempo de execução ou se preocupar com erros
se
um valor não implementa um método, mas nós o chamamos mesmo assim. Rust não irá
compilar nosso
código se os valores não implementarem os traits que o objeto trait precisa.
Por exemplo, a Listagem 17-10 mostra o que acontece se tentarmos criar uma Janela
com
uma String como um componente:
Arquivo: src/main.rs
use gui::Janela;
fn main() {
componentes: vec![
Box::new(String::from("Hi")),
],
};
screen.executar();
--> src/main.rs:7:13
7 | Box::new(String::from("Hi")),
Esse erro nos permite saber se estamos passando algo para Janela que não
pretenderíamos passar e que deveríamos passar um tipo diferente ou devemos
implementar
Draw na String , para que Janela possa chamar desenhar nela.
Quando usamos objetos trait, o Rust deve usar despacho dinâmico. O compilador não
sabe
todos os tipos que podem ser usados com código que está usando os objetos trait,
por isso
não sabe qual método implementado em que tipo chamar.
Em vez disso, em tempo de
execução, Rust usa os ponteiros dentro de objeto trait para saber
que método, específico,
deve chamar. Há um custo de tempo de execução quando essa pesquisa ocorre,
que não
ocorre com despacho estático. Dispacho dinâmico também impede que o
compilador
escolha inline o código de um método, o que, por vezes, impede
algumas otimizações. No
entanto, conseguimos uma maior flexibilidade no código que escrevemos
na Listagem 17-5
e foram capazes de suportar na Listagem 17-9, é uma desvantagem
a se considerar.
Você apenas pode fazer objetos traits seguros em objetos traits. Algumas regras complexas
determinam todas as propriedades que fazem um objeto trait seguro, mas em prática,
apenas
duas regras são relevantes. Um trait é um objeto seguro se todos os métodos
definidos no
trait tem as seguintes propriedades:
O compilador indicará quando você estiver tentando fazer algo que viole as
regras de
segurança de objetos em relação a objetos trait. Por exemplo, digamos
que tentamos
implementar a estrutuda da Listagem 17-4 para manter os tipos que
implementam o trait
Clone em vez do trait Draw , desta forma:
--> src/lib.rs:2:5
Esse erro significa que você não pode usar esse trait como um objeto trait dessa maneira.
Se
estiver interessado em mais detalhes sobre segurança de objetos, veja Rust RFC 255.
Quaisquer outras tentativas de mudança em uma postagem não deve ter efeito. Por
exemplo, se
tentarmos aprovar um rascunho de postagem no blog antes de solicitarmos
uma revisão, a postagem
a postagem deve permanecer em rascunho não publicado.
Listagem 17-11 mostra esse fluxo de trabalho em forma de código: este é um exemplo de
uso de
API que implementaremos em um biblioteca crate chamada blog . Isso ainda não foi
compilado,
porque não tempos implementado o crate blog :
Arquivo: src/main.rs
use blog::Postagem;
fn main() {
assert_eq!("", post.conteudo());
post.solicitar_revisao();
assert_eq!("", post.conteudo());
post.aprovar();
Queremos permitir que o usuário crie uma nova postagem de blog com Postagem :: new .
Então, queremos permitir que o texto seja adicionado à postagem do blog enquanto ela
estiver no estado de
rascunho. Se tentarmos obter o conteúdo da postagem
imediatamente, antes da aprovação,
nada deve acontecer porque a postagem ainda é um
rascunho. Adicionamos
assert_eq! no código para fins de demonstração. Um excelente
teste unitário para
isso seria afirmar que uma postagem do blog em rascunho retorna uma
string vazia do método
conteudo , mas não vamos escrever testes para este exemplo.
Arquivo: src/lib.rs
estado: Option<Box<Estado>>,
conteudo: String,
impl Postagem {
Postagem {
conteudo: String::new(),
trait Estado {}
struct Rascunho {}
Quando criamos um novo Postagem , definimos seu campo estado como um valor Some ,
que
conterá um Box . Este Box aponta para uma nova instância da estrutura Rascunho .
Isso
garante que sempre criamos uma nova instância de Postagem , ela começará como um
rascunho. Como o campo estado do Postagem é privado, não há como
criar um Postagem
em qualquer outro estado!
Arquivo: src/lib.rs
impl Postagem {
// --recorte--
self.conteudo.push_str(text);
Mesmo depois que chamamos add_texto e adicionamos algum conteúdo para nossa
postagem, ainda
queremos que o método conteudo retorne um pedaço de string vazia,
porque a postagem ainda
está no está de rascunho, como mostrado na linha 8 da Listagem
17-11. Por hora, vamos
implementar o método conteudo com a coisa mais simples que
atenderá a esse
requisito: sempre retornando um pedaço de string vazia. Mudaremos isso
mais tarde,
quando implementaremos a possibilidade de mudar o estado de uma postagem
para que ela possa ser publicada.
Até agora, postagens apenas podem estar no estado de
rascunho, portanto, o conteúdo da publicação deve estar
vazio. Listagem 17-14 mostra essa
implementação substituta:
Arquivo: src/lib.rs
impl Postagem {
// --recorte--
""
Arquivo: src/lib.rs
impl Postagem {
// --recorte--
self.estado = Some(s.solicitar_revisao())
trait Estado {
struct Rascunho {}
Box::new(RevisaoPendente {})
struct RevisaoPendente {}
self
Arquivo: src/lib.rs
impl Postagem {
// --recorte--
self.estado = Some(s.aprovar())
trait Estado {
struct Rascunho {}
// --recorte--
self
struct RevisaoPendente {}
// --recorte--
Box::new(Publicado {})
struct Publicado {}
self
self
Adicionamos o método aprovar para o trait Estado e adicionamos uma nova estrutura
que
implementa Estado , o estado Publicado .
Arquivo: src/lib.rs
impl Postagem {
// --recorte--
self.estado.as_ref().unwrap().conteudo(&self)
// --recorte--
Porque o objetivo é manter todos essas regras dentro das estruturas que implementam
Nós chamamos o método as__ref do Option porque queremos uma referência ao valor
do Option em vez da propriedade do valor. Como estado
é um Option<Box<Estado>> ,
quando chamamos as_ref , um Option<Box<Estado>> é
retornado. Se não chamarmos
as__ref , receberíamos um erro,
porque não podemos obter estado emprestado do
&self do parâmetro da função.
Então chamamos o método unwrap , que sabemos que nunca vai entrar em pânico, porque
sabemos
que os métodos em Postagem garantem que o estado sempre conterá um valor
Some
quando esses métodos forem realizados. Esse é um dos casos sobre os quais falamos
na
seção "Casos em que Você Tem Mais Informação Que o Compilador" do Capítulo
9,
quando sabemos que um valor None nunca é possível, mesmo que o compilador não
consiga ententer isso.
Arquivo: src/lib.rs
trait Estado {
// --recorte--
""
// --recorte--
struct Publicado {}
// --recorte--
&post.conteudo
Adicionamos uma implementação padrão para o método conteudo , que retorna uma
string
vazia. Isso significa que não preciamos implementar conteudo nas estruturas Rascunho e
Observe que precisamos anotações de vida útil nesse método, como discutimos no
Capítulo
10. Estamos fazendo uma referência a um post como argumento e retornando uma
referência a parte desse post , então o tempo de vida útil da referência retornada é
relacionada ao tempo de vida útil do argumento post .
Com o padrão de de estados, os métodos de Postagem e os locais que usam Postagem não
precisam da instrução match e para adicionar um novo estado, apenas precisamos
adicionar uma nova estrutura e
implementar os métodos trait nessa estrutura.
Outra desvantagem é que nós duplicamos algumas lógicas. Para eleminar parte da
duplicação, podemos tentar fazer a implementação padrão dos métodos
Arquivo: src/main.rs
fn main() {
assert_eq!("", post.conteudo());
Arquivo: src/lib.rs
pub struct Postagem {
conteudo: String,
conteudo: String,
impl Postagem {
RascunhoPostagem {
conteudo: String::new(),
&self.conteudo
impl RascunhoPostagem {
self.conteudo.push_str(text);
Nós ainta temos uma função Postagem::new , mas ao invés de retornar uma instância de
Arquivo: src/lib.rs
impl RascunhoPostagem {
// --recorte--
RevisaoPendentePostagem {
conteudo: self.conteudo,
conteudo: String,
impl RevisaoPendentePostagem {
Postagem {
conteudo: self.conteudo,
Arquivo: src/main.rs
use blog::Postagem;
fn main() {
As mudanças que precisamos fazer na main reatribuir post , o que significa que essa
implementação não segue mais o padrão de estados orientado a objetos:
as
transformações entre os estados não são mais encapsuladas inteiramente
dentro da
implementação do Postagem . No entanto, nosso ganho é que estados inválidos agora
são
impossíveis por causa do sistema de tipos e a verificação de tipos que acontecem em
tempo
de compilação! Isso garante que certos bugs, como o conteúdo de uma postagem
não
publicada sendo exibida, será descoberta antes de chegar em
produção.
Vimos que, embora o Rust seja capas de implementar o padrão de projetos orientado a
objetos,
outros padrões, como codificar estados em sistema de tipos,
também estão
disponíveis. Esses padrões têm diferentes vantagens e desvantagens. Apesar
de você poder
estar bastante familiarizado com o padrão orientado a objetos, repensar
o problema para
aproveitar os recursos do Rust pode fornecer benefícios, como evitar
alguns bugs em
tempo de compilação. Padrões orientados a objetos nem sempre serão a
melhor solução
em Rust devido certos recursos, como propriedade, que
as linguagens orientadas a objetos
não têm.
Resumo
Não importa se você acha que Rust é uma linguagem orientada a objetos depois
de ler este
capítulo, você agora sabe que pode usar objetos trait para obter alguns
recursos orientado
a objetos em Rust. O despacho dinâmico pode dar ao seu código alguma
flexibilidade em
troca de um pouco de desempenho em tempo de execução. Você pode usar essa
flexibilidade para implementar padrão orientado a objetos que podem ajudar na
manutenção
de seu código. Rust também tem outros recursos, como propriedade, que
linguagens orientadas aobjetos não têm. Um padrão orientado a objetos nem sempre
é a
melhor maneira de aproveitar os pontos fortes do Rust, mas é uma opção disponível.
Em seguida, veremos os padrões, que são outros dos recursos que permitem
muita
flexibilidade. Veremos brevemente eles ao longo do livro, mas ainda não vimos a
capacidade total deles. Vamos lá!
Patterns
More Lifetimes
Appendix
Keywords
Operators
Derivable Traits
Nightly Rust
Macros
Nightly
Beta
Stable (Estável)
nightly: * - - * - - *
nightly: * - - * - - *
beta: *
A maioria dos usuários do Rust não usa ativamente as versões beta, mas faz
testes com
versões beta no sistema de IC (integração contínua) para ajudar o
Rust a descobrir possíveis
regressões. Enquanto isso, ainda há uma release
todas as noites:
nightly: * - - * - - * - - * - - *
beta: *
Agora digamos que uma regressão seja encontrada. Ainda bem que tivemos algum
tempo
para testar a versão beta antes da regressão se tornar uma versão estável!
A correção é
aplicada à branch master , de modo que todas as noites é
corrigida e, em seguida, a
correção é portada para a branch beta , e uma nova
versão beta é produzida:
nightly: * - - * - - * - - * - - * - - *
beta: * - - - - - - - - *
Seis semanas depois da criação da primeira versão beta, é hora de uma versão
estável! A
branch stable é produzida a partir da branch beta :
nightly: * - - * - - * - - * - - * - - * - * - *
beta: * - - - - - - - - *
stable: *
Viva! Rust 1.5 está feito! No entanto, esquecemos uma coisa: como as seis
semanas se
passaram, também precisamos de uma nova versão beta da próxima
versão do Rust, 1.6.
Então, depois que a branch stable é criada a partir da beta ,
a próxima versão da beta é
criada a partir da nightly novamente:
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
stable: *
Isso é chamado de "train model" (modelo de trem) porque a cada seis semanas,
uma release
"sai da estação", mas ainda precisa percorrer o canal beta antes
de chegar como uma
release estável.
O Rust é lançando a cada seis semanas, como um relógio.
Se você souber a data de um
lançamento do Rust, poderá saber a data do próximo:
seis semanas depois. Um aspecto
interessante de ter lançamentos agendados a cada
seis semanas é que o próximo trem
estará chegando em breve. Se um recurso falhar
em uma versão específica, não há
necessidade de se preocupar: outra está
acontecendo em pouco tempo! Isso ajuda a
reduzir a pressão para ocultar recursos
possivelmente "não polidos" perto do prazo de
lançamento.
Graças a esse processo, você sempre pode verificar a próxima versão do Rust e
verificar por
si mesmo que é fácil fazer uma atualização para a mesma: se uma
versão beta não
funcionar conforme o esperado, você pode reportar à equipe e ter
isso corrigido antes do
próximo lançamento estável!
A quebra de uma versão beta é relativamente rara, mas o
rustc ainda é um
software, e bugs existem.
Recursos instáveis
Se você estiver usando uma versçao beta ou estável do Rust, você não pode usar
qualquer
sinalizador de recurso. Essa é a chave que nos permite usar de forma
prática os novos
recursos antes de declará-los estáveis para sempre. Aqueles que
desejam optar pelo que há
de mais moderno podem fazê-lo, e aqueles que desejam
uma experiência sólida podem se
manter estáveis sabendo que seu código não será
quebrado. Estabilidade sem estagnação.
Este livro contém informações apenas sobre recursos estáveis, pois os recursos
em
desenvolvimento ainda estão sendo alterados e certamente serão diferentes
entre quando
este livro foi escrito e quando eles forem ativados em compilações
estáveis. Você pode
encontrar documentação on-line para recursos do exclusivos
do Nightly (nightly-only).
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc
$ cd ~/projects/needs-nightly
Então, como você aprende sobre esses novos recursos? O modelo de desenvolvimento
da
Rust segue um processo de solicitação de comentários (RFC). Se você deseja
uma melhoria
no Rust, pode escrever uma proposta, chamada RFC (Request For
Comments).
Se o recurso for aceito, uma Issue será aberta no repositório do Rust e alguém
poderá
implementá-lo. A pessoa que a implementa pode muito bem não ser a pessoa
que propôs o
recurso em primeiro lugar! Quando a implementação está pronta, ela
chega à branch
master atrás de um sinalizador de recurso, conforme
discutimos na seção "Recursos
instáveis".
Depois de algum tempo, assim que os desenvolvedores do Rust que usam versões
Nightly
puderem experimentar o novo recurso, os membros da equipe discutirão o
recurso, como
ele funciona no Nightly e decidirão se ele deve se tornar parte
do Rust estável ou não. Se a
decisão for sim, o portão do recurso será removido
e o recurso agora será considerado
estável! Ele entra no próximo trem para uma
nova versão estável do Rust.