Como a linguagem Rust resolve o desafio de desalocação de objetos (com ownership, sem memory-leaks e sem GC)

Escrever programas que lidam bem com a memória não é tarefa fácil. Em C++, é fácil escrever programas que não desalocam objetos (os famosos memory-leaks) ou que tentam usar objetos que já foram desalocados. Em C# (e Java), é fácil sobrecarregar o GC e sofrer penalidades sérias de performance.

Rust tenta resolver esse problema através de uma abordagem original. Por sinal, a promessa da linguagem é detectar mau uso da memória durante o processo de compilação.

Como funciona a desalocação de objetos em Rust

Em Rust, todo objeto no heap é de propriedade de uma, e somente uma, variável. Quando esta variável sai de contexto, Rust automaticamente remove o objeto da memória.

fn foo() {
  let a = String::from("Foo");
  println!("The value of A is {}", a);
}

No código acima, a string está alocada na heap e é de propriedade da variável a. Quando essa variável sai de contexto, Rust tem segurança para tirar a string da memória.

Em C++, teríamos de usar um smart-pointer para ter comportamento semelhante. Em C#, o objeto permaneceria em memória até que o GC fizesse a coleta (não é possível determinar claramente quando isso acontecerá).

Transferência de ownership entre variáveis

Em Rust, quando atribuímos o valor de uma proprieade, para a outra, transferimos a “propriedade” do valor. Assim, a variável que continha o valor perde direito de acesso.

Por exemplo, o código que segue é inválido:

fn main() {
  let a = String::from("Foo");
  let b = a;
  println!("A is {} and B is {}", a, b); // essa linha causa um erro de compilação.
}

O problema aqui foi que estamos tentando acessar o valor da variável a que já não possui permissão de acesso ao valor pois transferiu a ownership para b.

Transferência de ownership ao chamar uma função

Em Rust, quando chamamos uma função passando um valor como parâmetro, transferimos o ownership do valor para a variável que representa o parâmetro na função.

O código a seguir é inválido em Rust:

fn main() {
  let a = String::from("Foo");
  print_string(a);
  println!("A is {}", a); // essa linha causa um erro de compilação.
}

fn print_string(s: String) {
  println!("{}", s);
}

O código acima é inválido porque, na chamada da função, ocorre a transferência de ownership da variável a para s no momento em que ocorre a chamada da função print_string. Por sinal, o valor é desalocado quando s sai de contexto (retorno da função).

Transferência de ownership para uma estrutura de dados mais complexa

Em Rust, quando atribuímos um valor a um elemento de uma tupla, transferímos o onwership desse valor para tupla.

O código que segue também é inválido:

fn main() {
  let a = String::from("Foo");
  let (_, length) = get_string_length(a);
  println!("Length of A is {}.", length);
}

fn get_string_length(s: String) -> (String, usize) {
  (s, s.len()) // esse código gera um erro de compilação
}

Aqui, o problema ocorre porque, ao atribuir o valor de s para o primeiro elemento da tupla, perdemos a permissão de acessar o valor na atribuição do segundo elemento.

Aliás, esse código se resolveria facilmente assim:

fn main() {
  let a = String::from("Foo");
  let (_, length) = get_string_length(a);
  println!("Length of A is {}.", length);
}

fn get_string_length(s: String) -> (String, usize) {
  let l = s.len(); // <-- usamos o valor de "s" antes de atribuir para a tupla
  (s, l) 
}

Mas afinal, por que o conceito de ownership é tão interessante?

Pelo pouco que podemos ver até aqui, o conceito de ownership parece impor uma série de dificuldades ao programador. Entretanto, ela se compensa!

Sabendo que apenas uma variável, em um dado momento, tem a propriedade de um valor, o compilador de Rust consegue gerar código nativo inteligente de desalocação, com segurança assegurada durante a compilação, sem os memory-leaks das linguagens não gerenciadas e sem os overheads de processamento causados pelos GCs.

Rust não tem um runtime. Programas em Rust são muito rápidos!

Tomando e devolvendo ownership

Considere o código VÁLIDO abaixo:

fn main() {
  let a = String::from("Foo");
  let a = print_string(a);
  println!("A is {}", a);
}
fn print_string(s: String) -> String {
  println!("{}", s);
  s
}

Nesse código, ocorre a transferência de ownership da variável a para a função para a função print_string, mas essa devolve o ownership retornando s que é transferido para um shadow de a.

É uma solução, mas, concordemos, “feia pra caramba”!

Variáveis que referenciam um valor, mas sem ownership

Para tornar a vida do programador um pouco menos difícil, Rust possui o conceito de referências semelhante ao que temos em C++. Porém, diferente de C++, com garantias, em tempo de compilação, de que as variáveis de referência não irão “viver” mais tempo que a variável com ownership do valor referenciado.

O código a seguir é válido:

fn main() {
  let a = String::from("Foo");
  let b = &a;
  println!("A is {} and B is {}", a, b); 
}

Dessa vez, a variável possui uma referência para o valor em a. Não ouve transferência de ownership. Aliás, o compilador garante que não b não tenha “vida mais longa do que a.

O seguinte código não seria válido:

fn main() {
  let b;
  {
    let a = String::from("Foo");
    b = &a; // essa linha causa erro de compilação
  }
  println!("B is {}", b); 
}

Aqui, tentou-se atribuir a b uma referência para o valor a. O problema, aqui, foi que a variável a sai de contexto antes que b.

O seguinte código também é válido:

fn main() {
  let a = String::from("Foo");
  let length = get_string_length(&a);
  println!("Length of A is {}.", length);
}
fn get_string_length(s: &String) -> usize {
  s.len()
}

Nesse código, get_string_length recebe uma referência para o valor da variável a. Não há transferência de ownership.

Como saber mais sobre Rust

Quase tudo que aprendi sobre Rust foi escrevendo código e estudando o excelente The Rust Programming Language que está disponível gratuitamente online. Aliás, boa parte do código que está aqui foi inspirado em algum exemplo que encontrei nesse livro.

Se você precisa de performance extrema e não quer escrever C# feito, nem recorrer a C++! Rust pode ser a resposta.

Antes de ir embora

A seguinte função não compila em Rust.

fn longest(x: &str, y: &str) -> &str {
  if x.len() > y.len() {
    x
  } else {
    y
  }
}

Baseado no que aprendeu até aqui, saberia me dizer a causa? (Em tempo, esse código foi extraído do livro que indiquei acima).

Era isso… por enquanto

Esse foi o primeiro post com código aqui na Eximia! Espero que tenham gostado.

Continuarei escrevendo sobre aspectos interessantes que encontrei em Rust. Enquanto isso, , que tal conversarmos sobre o que mostrei aqui nos comentários?

Créditos para a imagem da capa para edunham.

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
5 Comentários
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Leandro Sousa
Leandro Sousa
5 anos atrás

O compilador não sabe interpretar qual lifetime dos parâmetros da função.

Antonio Maniero
Antonio Maniero
5 anos atrás

Farei algumas considerações:

Esta é provavelmente a melhor explicação do sistema de borrowing de Rust que eu já vi, talvez por se concentrar no que importa mais. Quero ver falar tão fácil do tempo de vida explícito 🙂

Tecnicamente Rust usa um mecanismo de GC, só não é um tracing GC. Mas claro que quando se fala informalmente de GC todo mundo entende que é um tracing GC (mesmo que nem conheça esse termo) e não um GC mais simples e principalmente determinístico.

Em alguns casos o overhead do gerenciamento da memória desta forma, ou até mesmo manual sem um cuidado extremo, pode ser maior que o do tracing GC. Claro, ele não terá pausas (significativas, nem vou entrar nesse assunto 🙂 ), mas a alocação do tracing GC bem feito tende a ser muito mais eficiente, e o custo de coleta pode não ser tão significativo assim comparando com tanto “malloc e free” que pode ocorrer em certos cenários de Rust, ou C++. Claro que em Rust, e principalmente em C++ você pode tomar certos cuidados para evitar alocação efêmera em grandes volumes. Mas complica mais o código e não é comum a pessoa pensar nisso na maioria das vezes, só o faz quando realmente precisa.

Em alguns casos a complicação que o mecanismo impõe é pior que o demonstrando, só pra não parecer que é tudo tão fácil.

Faltou dizer que a compilação é bem mais lenta que em muitas linguagens ( não vou comparar com C++ complexo que tem seus próprios problemas).

Rust tem problemas de performance ainda (para o que ela se propõe). Mas isso tem a ver com a implementação mais que um problema da linguagem (que tem também).

Rust é um excelente meio termo entre C++ e C# (que está cada vez mais preocupada com performance e principalmente com menos geração de lixo, que é a melhor forma de solucionar tudo isto).

Não vou responder o desafio porque sei a resposta, melhor deixar para quem está vendo isso pela primeira vez ;P

Valeu!!

Elemar Júnior
Elemar Júnior
5 anos atrás

Muito obrigado pelos elogios ao POST.

Realmente, alocação é extremamente eficiente com .NET. Mas, a desalocação costuma não ser.

No fim do dia, dependendo do cenário, teremos uma estratégia ou outra se mostrando mais eficiente. Minha única ressalva, se me permite, é que já consigo colocar Rust ao lado de C++. Fiquei curioso para saber o que viu de problema.

Daniel Moreira Yokoyama
Daniel Moreira Yokoyama
5 anos atrás

Não sei se consigo pensar em um bom motivo pra considerar a lentidão na compilação um problema. O motivo pelo qual ela leva mais tempo é o que determina a diferença entre Rust e C (que é a linguagem que os criadores de Rust pretendem substituir). Pensando assim, eu estou totalmente inclinado a aceitar mais tempo durante a compilação.
Vale a pena assisitir o video da Carol Nichols explicando o projeto. Já tem uns 15 dias que estou apaixonado por esta linguagem e estudando-a comparando-a com Go (que apesar de ser bacana, em minha opinião, carece de recursos úteis para a modelagem, que Rust supera com generics, traits e outras coisinhas bacanas – eu sou apaixonado pelos enums).

Segue o video: https://www.youtube.com/watch?v=A3AdN7U24iU

Valeu.

Antonio Maniero
Antonio Maniero
5 anos atrás

Eu usei pouco de Rust, mais pra brincar mesmo, você tem bem mais experiência, mas ficou claro pra mim que algumas coisas ficam mais lentas (pode ser que no futuro resolvam), ou dá um pouco mais trabalho fazer, ainda não entendi bem como usar certo `Cell` e `RefCell` entre outros *workarounds* :), tem cenários que não ter um mecanismo de herança de implementação e não só de tipo dá uma atrapalhada (salvo eu não ter entendido algo, mas já vi outras pessoas reclamando). Não fiquei fã do uso das macros, ainda que seja uma evolução em relação ao que se tem em C++, algumas soluções pra mim ficaram bem esquisitas (pra quem está acostumado com C#, já que C++ também tem esquisitices de monte). Tem casos que para ficar rápido tem que jogar fora muito das vantagens que a linguagem trouxe. Claro que Rust é uma bela evolução, mas C++ vai um pouco além quando precisa de controle mais absoluto do que está fazendo e de como otimizar ao máximo. É certamente mais produtiva e elegante em quase tudo, só perde um pouco nisso pra C#, mas é o preço que se paga para ter mais performance e principalmente não ter pausas. Eu acho que Rust substitui bem C++ na imensa maioria dos cenários, só não em todos, ainda. O C# também, tem uns caras que fizeram nela um banco de dados rápido e poderoso, relativamente popular, quando normalmente as pessoas só pensariam fazer em C++, já ouviu falar disso? 😛 😀

AUTOR

Elemar Júnior
Fundador e CEO da EximiaCo atua como tech trusted advisor ajudando empresas e profissionais a gerar mais resultados através da tecnologia.

NOVOS HORIZONTES PARA O SEU NEGÓCIO

Nosso time está preparado para superar junto com você grandes desafios tecnológicos.

Entre em contato e vamos juntos utilizar a tecnologia do jeito certo para gerar mais resultados.

Insights EximiaCo

Confira os conteúdos de negócios e tecnologia desenvolvidos pelos nossos consultores:

Arquivo

Pós-pandemia, trabalho remoto e a retenção dos profissionais de TI

CTO Consulting e Especialista em Execução em TI
5
0
Queremos saber a sua opinião, deixe seu comentáriox

A sua inscrição foi realizada com sucesso!

O link de acesso à live foi enviado para o seu e-mail. Nos vemos no dia da live.

Muito obrigado!

Deu tudo certo com seu envio!
Logo entraremos em contato

Como a linguagem Rust resolve o desafio de desalocação de objetos (com ownership, sem memory-leaks e sem GC)

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Como a linguagem Rust resolve o desafio de desalocação de objetos (com ownership, sem memory-leaks e sem GC)

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Condição especial de pré-venda: R$ 14.000,00 - contratando a mentoria até até 31/01/2023 e R$ 15.000,00 - contratando a mentoria a partir de 01/02/2023, em até 12x com taxas.

Tenho interesse nessa capacitação

Para solicitar mais informações sobre essa capacitação para a sua empresa, preencha o formulário a seguir:

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse nessa solução

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse neste serviço

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

× Precisa de ajuda?