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 b 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.
O compilador não sabe interpretar qual lifetime dos parâmetros da função.
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!!
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.
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.
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? 😛 😀