Conteúdo


Por que você deve aprender a linguagem de programação Rust

Descubra a história, os principais conceitos e as ferramentas para usar o Rust

Uma recente pesquisa de opinião da Stack Overflow descobriu que quase 80% dos participantes ficaram satisfeitos ao usá-lo ou desejavam desenvolver com a linguagem Rust. Esse é um número incrível! Então, o que há de tão bom na Rust? Este artigo explora os pontos altos dessa linguagem semelhante à C e ilustra por que ela deve ser a próxima em sua lista de linguagens para aprender.

Rust e sua genealogia

Primeiro, vamos começar com uma rápida aula de história. Rust é uma nova linguagem com relação a suas antecessoras (sobretudo C, que a precederam 38 anos), mas sua genealogia cria sua abordagem multiparadigma. Rust é considerado uma linguagem semelhante à C, mas os outros recursos que ele inclui cria vantagens sobre seus antecessores (consulte Figura 1).

Primeiro, Rust é fortemente influenciado por Cyclone (um dialeto seguro de C e uma linguagem imperativa), com alguns aspectos de recursos orientados a objetos de C++. Mas, ele também inclui recursos funcionais de linguagens como Haskell e OCaml. O resultado é uma linguagem semelhante à C que suporta programação multiparadigma (imperativa, funcional e orientada a objeto).

Figura 1. Rust e sua árvore genealógica
Rust e sua árvore genealógica
Rust e sua árvore genealógica

Principais conceitos em Rust

Rust possui muitos recursos que o tornam útil, mas os desenvolvedores e suas necessidades diferem. Eu cubro cinco dos principais conceitos que tornam o aprendizado do Rust valioso e mostro essas ideias na origem do Rust.

Primeiro, para ter uma ideia do código, vamos analisar o programa canônico "Hello World", que simplesmente emite aquela mensagem ao usuário (consulte Listagem 1).

Lista 1. "Hello World" em Rust
fn main()
{
   println!( "Hello World.");
}

Esse programa simples, semelhante a C, define a função principal que é o ponto de entrada designado para o programa (e cada programa possui uma). A função é definida com a palavra-chave fn seguida por um conjunto opcional de parâmetros entre parênteses (()). As chaves ({}) delineiam a função; essa função consiste em uma chamada para a macro println! , que emite texto formatado para o console (stdout), conforme definido pelo parâmetro string.

Rust inclui uma variedade de recursos que o tornam interessante e valem o investimento no aprendizado. Você encontrará conceitos como módulos para reusabilidade, segurança e garantias de memória (operações seguras vs. inseguras), recursos de manipulação de erro irrecuperável e recuperável, suporte para simultaneidade e tipos de dados complexos (chamados coleções).

Código reutilizável via módulos

O Rust permite organizar o código de uma maneira que promove sua reutilização. Você alcança este nível de organização usando módulos, que contêm funções, estruturas e até mesmo outros módulos que você pode tornar públicos (ou seja, expor aos usuários do módulo) ou privados (ou seja, usar somente dentro do módulo e não pelos usuários do módulo, pelo menos não diretamente). O módulo organiza o código como um pacote que outros usuários podem usar.

Você usa três palavras-chave para criar módulos, usar módulos e modificar a visibilidade dos elementos nos módulos.

  • A palavra-chave mod cria um novo módulo
  • A palavra-chave use permite usar o módulo (expor as definições no escopo para usá-las)
  • A palavra-chave pub torna elementos do módulo públicos (caso contrário, eles são privados).

A Listagem 2 fornece um exemplo simples. Ela inicia com a criação de um novo módulo chamado bits que contém três funções. A primeira função, chamada pos, é uma função privada que utiliza um argumento u32 e retorna um u32 (conforme indicado pela seta -> ), que é um valor 1 deslocado para a esquerda bit vezes. Observe que uma palavra-chave return não é necessária aqui. Esse valor é chamado por duas funções públicas (observe a palavra-chave pub ): decimal e hex. Essas funções chamam a função privada pos e imprimem o valor da posição de bit no formato decimal ou hexadecimal (observe o uso de :x para indicar o formato hexadecimal). Por fim, ele declara uma função main que chama as duas funções públicas do módulo bits com a saída mostrada no final da Listagem 2 como comentários.

Lista 2. Exemplo de módulo simples em Rust
mod bits {
   fn pos(bit: u32) -> u32 {
      1 << bit
   }

   pub fn decimal(bit: u32) {
      println!("Bits decimal {}", pos(bit));
   }

   pub fn hex(bit: u32) {
      println!("Bits decimal 0x{:x}", pos(bit));
   }
}

fn main( ) {
   bits::decimal(8);
   bits::hex(8);
}

// Bits decimal 256
// Bits decimal 0x100

Os módulos permitem coletar a funcionalidade de maneira pública ou privada, mas também é possível associar métodos a objetos usando a palavra-chave impl .

Verificações de segurança para código mais limpo

O compilador Rust aplica garantias de segurança da memória e outras verificações que tornam a linguagem de programação segura (diferente de C, que pode ser inseguro). Portanto, em Rust, você nunca precisará se preocupar com ponteiros pendentes ou com o uso de um objeto após ele ter sido liberado. Essas coisas fazem parte da linguagem Rust central. Mas, em áreas como desenvolvimento integrado, é importante fazer coisas como colocar uma estrutura em um endereço que represente um conjunto de registros de hardware.

O Rust inclui uma palavra-chave unsafe com a qual é possível desativar verificações que geralmente resultariam em um erro de compilação. Conforme mostrado na Listagem 3, a palavra-chave unsafe permite declarar um bloco inseguro. Nesse exemplo, eu declaro uma variável imutável x e, em seguida, um ponteiro para essa variável chamado raw. Então, para remover a referência de raw (que nesse caso imprimiria 1 no console), eu uso a palavra-chave unsafe para permitir essa operação, que de outra forma seria sinalizada na compilação.

Lista 3. Operações inseguras em Rust
fn main() {
   let a = 1;
   let rawp = &a as *const i32;

   unsafe {
      println!("rawp is {}", *rawp);
   }
}

É possível aplicar a palavra-chave unsafe em funções e em blocos de código dentro de uma função do Rust. A palavra-chave é mais comum na gravação de ligações em funções não Rust. Esse recurso torna o Rust útil para coisas como desenvolvimento de sistema operacional ou programação integrada (bare-metal).

Melhor manipulação de erros

Erros acontecem, independentemente da linguagem de programação que você usa. No Rust, os erros encontram-se em dois campos: erros irrecuperáveis (o pior tipo) e erros recuperáveis (o tipo não tão ruim).

Erros irrecuperáveis

A função panic! do Rust é semelhante à macro assert do C . Ela gera saída para ajudar o usuário a depurar um problema (bem como parar a execução antes que eventos mais catastróficos ocorram). A função panic! é mostrada na Listagem 4, com sua saída executável nos comentários.

Lista 4. Manipulando erros irrecuperáveis no Rust com panic!
fn main() {
   panic!("Bad things happening.");
}

// thread 'main' panicked at 'Bad things happening.', panic.rs:2:4
// note: Run with `RUST_BACKTRACE=1` for a backtrace.

Na saída, é possível ver que o tempo de execução do Rust indica exatamente onde o problema ocorreu (linha 2) e emitiu a mensagem fornecida (que poderia emitir informações mais descritivas). Conforme indicado na mensagem de saída, você poderia gerar um rastreio de pilha executando com uma variável de ambiente especial chamada RUST_BACKTRACE. Também é possível chamar panic! internamente com base nos erros detectáveis (como acessar um índice inválido de um vetor).

Erros recuperáveis

A manipulação de erros recuperáveis é uma parte padrão da programação e o Rust inclui um ótimo recurso para verificação de erro (consulte Listagem 5). Analise esse recurso no contexto de uma operação de arquivo. A função File::open retorna um tipo de Result<T, E>, em que T e E representam parâmetros de tipo genérico (nesse contexto, eles representam std::fs::File e std::io::Error). Portanto, quando você chama File::open e nenhum erro ocorreu (E é Ok), T representaria o tipo de retorno (std::fs::File). Se um erro ocorreu, E representaria o tipo de erro que ocorreu (usando o tipo std::io::Error). (Observe que minha variável de arquivo _f usa um sublinhado [_] para omitir o aviso de variável não usada que o compilador gerou.)

Em seguida, eu uso um recurso especial no Rust chamado match, que é semelhante à instrução switch no C, mas mais poderoso. Nesse contexto, eu correspondo _f com relação aos possíveis valores de erro (Ok e Err). Para Ok, eu retorno o arquivo para designação; para Err, eu uso panic!.

Lista 5. Manipulando erros recuperáveis no Rust com Result<T, E>
use std::fs::File;

fn main() {
   let _f = File::open("file.txt");

   let _f = match _f {
      Ok(file) => file,
      Err(why) => panic!("Error opening the file {:?}", why),
   };
}

// thread 'main' panicked at 'Error opening the file Error { repr: Os
// { code: 2, message: "No such file or directory" } }', recover.rs:8:23
// note: Run with `RUST_BACKTRACE=1` for a backtrace.

Os erros recuperáveis são simplificados no Rust quando você usa a enumeração Result; eles são ainda mais simplificados por meio do uso de match. Observe também nesse exemplo a falta de uma operação File::close: o arquivo é fechado automaticamente quando o escopo de _f termina.

Suporte para simultaneidade e encadeamentos

A simultaneidade comumente vem com problemas (disputas e conflitos de dados, para citar dois). O Rust fornece meios de gerar encadeamentos usando o sistema operacional nativo, mas também tenta minimizar os efeitos negativos do encadeamento. O Rust inclui transmissão de mensagem para permitir que os encadeamentos se comuniquem entre si (via send e recv, bem como bloqueio por meio de mutexes). O Rust também fornece a capacidade de permitir que um encadeamento empreste um valor, que lhe fornece propriedade e faz a transição efetiva do escopo do valor (e de sua propriedade) para um novo encadeamento. Assim, o Rust fornece segurança de memória juntamente com simultaneidade, sem disputas de dados.

Considere um exemplo simples de encadeamento no Rust que introduz alguns novos elementos (operações de vetor) e traz de volta alguns conceitos discutidos anteriormente (reconhecimento de padrões). Na Listagem 6, eu começo importando os namespaces thread e Duration para o meu programa. Em seguida, eu declaro uma nova função chamada my_thread, que representa o encadeamento que eu criarei mais tarde. Nesse encadeamento, eu simplesmente emito o identificador do encadeamento e, em seguida, o suspendo por um breve período, para que o planejador permita que outro encadeamento seja executado.

Minha função main é o ponto principal desse exemplo. Eu começo criando um vetor mutável vazio que posso usar para armazenar valores do mesmo tipo. Em seguida, eu crio 10 encadeamentos usando a função spawn e envio por push a manipulação de junção resultante para o vetor (falaremos mais sobre isso posteriormente). Esse exemplo de spawn é independente do encadeamento atual, o que permite que o encadeamento permaneça após o encadeamento pai ter sido encerrado. Após emitir uma mensagem curta do encadeamento pai, eu finalmente itero o vetor dos tipos JoinHandle e aguardo o encerramento de cada encadeamento filho. Para cada JoinHandle no vetor, eu chamo a função join, que aguarda o encerramento desse encadeamento antes de continuar. Se a função join retornar um erro, eu irei expor esse erro por meio da chamada match .

Lista 6. Encadeamentos no Rust
use std::thread;
use std::time::Duration;

fn my_thread() {
   println!("Thread {:?} is running", std::thread::current().id());
   thread::sleep(Duration::from_millis(1));
}

fn main() {
   let mut v = vec![];

   for _i in 1..10 {
      v.push( thread::spawn(|| { my_thread(); } ) );
   }

   println!("main() waiting.");

   for child in v {
      match child.join() {
         Ok(_) => (),
         Err(why) => println!("Join failure {:?}", why),
      };
   }
}

Na execução, eu vejo a saída fornecida na Listagem 7. Observe que o encadeamento principal continuou sendo executado até o início do processo de junção. Então, os encadeamentos são executados e encerrados em diferentes momentos, identificando a sua natureza assíncrona.

Lista 7. A saída do encadeamento do código de exemplo na Listagem 6
main() waiting.
Thread ThreadId(7) is running
Thread ThreadId(9) is running
Thread ThreadId(8) is running
Thread ThreadId(6) is running
Thread ThreadId(5) is running
Thread ThreadId(4) is running
Thread ThreadId(3) is running
Thread ThreadId(2) is running
Thread ThreadId(1) is running

Suporte para tipos de dados complexos (coleções)

A biblioteca padrão do Rust inclui várias estruturas de dados populares e úteis que podem ser usadas em seu desenvolvimento, inclusive quatro tipos de estruturas de dados: sequências, mapas, conjuntos e um tipo diverso.

Para sequências, é possível usar o tipo vetor (Vec), que eu usei no exemplo de encadeamento. Esse tipo fornece uma matriz dinamicamente redimensionável e é útil para coletar dados para processamento posterior. A estrutura VecDeque é semelhante ao Vec, mas é possível inseri-la em ambas as extremidades da sequência. A estrutura LinkedList também é semelhante ao Vec, mas com ela é possível dividir e anexar listas.

Para mapas, você tem as estruturas HashMap e BTreeMap. Use a estrutura HashMap para criar pares chave-valor e você poderá referenciar elementos por suas chaves (para recuperar o valor). A BTreeMap é semelhante à HashMap, mas ela pode classificar as chaves e você pode iterar facilmente todas as entradas.

Para conjuntos, você tem as estruturas HashSet e BTreeSet (que você observará após as estruturas de mapas). Essas estruturas são úteis quando você não possui valores (apenas chaves) e chama novamente as chaves que foram inseridas.

Por fim, a estrutura diversa é atualmente a BinaryHeap. Essa estrutura implementa uma fila de prioridade com um heap binário.

Instalando o Rust e suas ferramentas

Uma das maneiras mais simples de instalar o Rust é usando curl por meio do script de instalação. Basta executar a seguinte sequência da linha de comandos do Linux®:

curl -sSf https://static.rust-lang.org/rustup.sh | sh

Essa sequência transfere o shell script rustup de rust-lang.org e, em seguida, transmite o script para o shell para execução. Ao concluir, é possível executar rustc -v para mostrar a versão de Rust que você instalou. Com o Rust instalado, é possível mantê-lo usando o utilitário rustup, que também pode ser usado para atualizar sua instalação do Rust.

O compilador do Rust chama-se rustc. Nos exemplos mostrados aqui, o processo de construção é simplesmente definido como:

rustc threads.rs

... em que o compilador do Rust produz um arquivo executável nativo chamado threads. É possível depurar simbolicamente os programas Rust usando rust-lldb ou rust-gdb.

Você provavelmente percebeu que os programas Rust que eu demonstrei aqui têm um estilo exclusivo. É possível aprender esse estilo por meio da formatação de origem automática do Rust usando o utilitário rustfmt . Esse utilitário, executado com um nome do arquivo de origem, formatará automaticamente sua origem em um estilo consistente e padronizado.

Por fim, embora o Rust seja um pouco restrito pelo fato de que ele aceita por origem, é possível usar o programa rust-clippy para mergulhar ainda e mais em sua origem para identificar elementos de práticas ruins. Pense no rust-clippy como o utilitário C lint .

Considerações sobre o Windows

No Windows, o Rust requer adicionalmente as ferramentas de construção C++ para o Visual Studio 2013 ou mais recente. A maneira mais fácil de adquirir as ferramentas de construção é instalando o Microsoft Visual C++ Build Tools 2017 que fornece apenas as ferramentas de construção do Visual C++. Como alternativa, é possível instalar o Visual Studio 2017, o Visual Studio 2015 ou o Visual Studio 2013 e, durante a instalação, selecionar ferramentas C++.

Para obter informações adicionais sobre como configurar o Rust no Windows, consulte a Documentação do rustup específica do Windows.

Indo além

Em meados de fevereiro de 2018, a equipe de Rust liberou a versão 1.24. Essa versão inclui compilação incremental, formatação de origem automática com rustfmt, novas otimizações e estabilizações de biblioteca. É possível saber mais sobre Rust e sua evolução no blog do Rust e fazer download do Rust no website da Linguagem Rust. Lá, é possível ler sobre vários outros recursos que Rust oferece, inclusive reconhecimento de padrões, iteradores, fechamentos e ponteiros inteligentes.


Recursos para download

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Software livre
ArticleID=1063029
ArticleTitle=Por que você deve aprender a linguagem de programação Rust
publish-date=09192018