
Olá pessoal. Recentemente eu me encontrei com uma nova linguagem de programação Rust. Percebi que ele era diferente dos outros que eu havia encontrado antes. Portanto, eu decidi ir mais fundo. Quero compartilhar os resultados e minhas impressões:
- Vou começar com as principais características, na minha opinião, do Rust
- Vou descrever detalhes interessantes de sintaxe
- Vou explicar por que não é provável que Rust domine o mundo
Explicarei imediatamente que escrevo em Java há cerca de dez anos, então argumentarei da minha torre sineira.
Recurso assassino
O Rust está tentando tomar uma posição intermediária entre linguagens de baixo nível, como C / C ++ e Java / C # / Python / Ruby de alto nível ... Quanto mais a linguagem estiver do hardware, mais controle, mais fácil será prever como o código será executado. Mas ter acesso total à memória é muito mais fácil de fotografar sua perna. Ao contrário do C / C ++, o Python / Java e todo o resto apareceram. Eles não precisam pensar em limpar a memória. A pior coisa é NPE, vazamentos não são tão comuns. Mas, para que isso funcione, você precisa, no mínimo, de um coletor de lixo, que, por sua vez, começa a viver sua própria vida, em paralelo com o código do usuário, reduzindo sua previsibilidade. A máquina virtual ainda oferece independência de plataforma, mas quanto é necessário é um ponto discutível, não o levarei agora.
Rust é uma linguagem de baixo nível, o compilador gera um binário, que não requer truques adicionais para funcionar. Toda a lógica para remover objetos desnecessários é integrada ao código no momento da compilação, ou seja, também não há coletor de lixo em tempo de execução. O Rust também não possui referências nulas e os tipos são seguros, o que o torna ainda mais confiável que o Java.
No centro do gerenciamento de memória está a idéia de possuir uma referência a um objeto e pedir empréstimos. Se apenas uma variável possuir cada objeto, assim que expirar no final do bloco, tudo o que apontar poderá ser recursivamente limpo. Os links também podem ser emprestados para leitura ou escrita. Aqui funciona o princípio de um escritor e de muitos leitores.
Esse conceito pode ser demonstrado no seguinte trecho de código. Test () é chamado do método main () , que cria uma estrutura de dados recursiva MyStruct que implementa a interface destruidora. Drop permite que você defina a lógica a ser executada antes que o objeto seja destruído. Algo semelhante ao finalizador em Java, apenas ao contrário de Java, o momento da chamada do método drop () é bastante certo.
fn main() { test(); println!("End of main") } fn test() { let a = MyStruct { v: 1, s: Box::new( Some(MyStruct { v: 2, s: Box::new(None), }) ), }; println!("End of test") } struct MyStruct { v: i32, s: Box<Option<MyStruct>>, } impl Drop for MyStruct { fn drop(&mut self) { println!("Cleaning {}", self.v) } }
A conclusão será a seguinte:
End of test Cleaning 1 Cleaning 2 End of main
I.e. antes de sair do teste (), a memória foi limpa recursivamente. O compilador cuidou disso, inserindo o código necessário. O que é Box e Option descreverá um pouco mais tarde.
Dessa maneira, o Rust obtém segurança de linguagens de alto nível e previsibilidade de linguagens de programação de baixo nível.
O que mais interessante
A seguir, listo os recursos do idioma em ordem decrescente de importância, na minha opinião.
Oop
Aqui, Rust geralmente está à frente do resto. Se a maioria das línguas chegou à conclusão de que a herança múltipla deve ser abandonada, em Rust não há herança alguma. I.e. uma classe só pode implementar interfaces em qualquer quantidade, mas não pode herdar de outras classes. Em termos de Java, isso significaria tornar todas as classes finais. Em geral, a variedade sintática para manter a POO não é tão grande. Talvez isso seja o melhor.
Para combinar dados, existem estruturas que podem conter implementação. As interfaces são chamadas de características e também podem conter implementações padrão. Eles não alcançam classes abstratas, porque não pode conter campos, muitos reclamam dessa restrição. A sintaxe é a seguinte, acho que os comentários não são necessários aqui:
fn main() { MyPrinter { value: 10 }.print(); } trait Printer { fn print(&self); } impl Printer { fn print(&self) { println!("hello!") } } struct MyPrinter { value: i32 } impl Printer for MyPrinter { fn print(&self) { println!("{}", self.value) } }
Dos recursos que notei, vale destacar o seguinte:
- As classes não têm construtores. Existem apenas inicializadores que especificam valores para campos através de chaves. Se você precisar de um construtor, isso será feito através de métodos estáticos.
- O método da instância difere do estático, tendo a & self reference como o primeiro argumento.
- Classes, interfaces e métodos também podem ser generalizados. Mas, diferentemente do Java, essas informações não são perdidas no momento da compilação.
Um pouco mais de segurança
Como eu disse, Rust presta muita atenção à confiabilidade do código e tenta evitar a maioria dos erros no estágio de compilação. Para isso, a capacidade de tornar os links vazios foi excluída. Isso me lembrou os tipos anuláveis da Kotlin. A opção é usada para criar links vazios. Assim como no Kotlin, ao tentar acessar essa variável, o compilador vai bater as mãos, forçando a inserir cheques. Tentar extrair o valor sem verificar pode levar a um erro. Mas isso certamente não pode ser feito por acidente, como, por exemplo, em Java.
Também gostei do fato de que todas as variáveis e campos de classe são imutáveis por padrão. Olá novamente Kotlin. Se o valor puder mudar, isso deve ser indicado explicitamente com a palavra-chave mut . Eu acho que o desejo de imutabilidade melhora muito a legibilidade e a previsibilidade do código. Embora a opção por algum motivo seja mutável, eu não entendi isso, aqui está o código da documentação:
let mut x = Some(2); let y = x.take(); assert_eq!(x, None); assert_eq!(y, Some(2));
Transferências
Ferrugem é chamada enum . Somente além de um número limitado de valores, eles ainda podem conter dados e métodos arbitrários. Portanto, é algo entre enumerações e classes em Java. A opção enum padrão no meu primeiro exemplo apenas pertence a este tipo:
pub enum Option<T> { None, Some(T), }
Existe uma construção especial para processar esses valores:
fn main() { let a = Some(1); match a { None => println!("empty"), Some(v) => println!("{}", v) } }
Tambem
Não pretendo escrever um livro sobre o Rust, mas simplesmente enfatizar suas características. Nesta seção, descreverei o que mais é útil, mas, na minha opinião, não é tão único:
- Os fãs de programação funcional não ficarão desapontados; existem lambdas para eles. O iterador possui métodos para processar a coleção, por exemplo, filter e for_each . Algo como fluxos Java.
- A construção de correspondência também pode ser usada para coisas mais complexas do que enum regular, por exemplo, para padrões de processamento.
- Há um grande número de classes internas, por exemplo, coleções: Vec, LinkedList, HashMap , etc.
- Você pode criar macros
- É possível adicionar métodos às classes existentes
- Inferência automática de tipo suportada
- Junto com o idioma, vem uma estrutura de teste padrão
- O utilitário de carga integrado é usado para criar e gerenciar dependências
Voar na pomada
Esta seção é necessária para completar a imagem.
Problema assassino
A principal desvantagem vem do recurso principal. Você tem que pagar por tudo. No Rust, é muito inconveniente trabalhar com estruturas de dados de gráficos mutáveis, porque qualquer objeto não deve ter mais de um link. Para contornar essa limitação, há várias classes internas:
- Box - um valor imutável na pilha, um análogo de wrappers para primitivos em Java
- Célula - valor variável
- RefCell - valor variável acessível por referência
- Rc - contador de referência, para várias referências a um objeto
E esta é uma lista incompleta. Para a primeira amostra do Rust, decidi imprudentemente escrever uma lista isolada com métodos básicos. Por fim, o link para o nó resultou na seguinte Opção <Rc <RefCell <ListNode> >> :
- Opção - para processar um link vazio
- Rc - para vários links, como o último nó é referenciado pelo nó anterior e pela própria planilha
- RefCell - para link mutável
- ListNode - o próximo elemento em si
Parece mais ou menos, um total de três invólucros em torno de um objeto. O código para simplesmente adicionar um item ao final da lista é muito complicado, e há coisas não óbvias, como clonagem e empréstimo:
struct ListNode { val: i32, next: Node, } pub struct LinkedList { root: Node, last: Node, } type Node = Option<Rc<RefCell<ListNode>>>; impl LinkedList { pub fn add(mut self, val: i32) -> LinkedList { let n = Rc::new(RefCell::new(ListNode { val: val, next: None })); if (self.root.is_none()){ self.root = Some(n.clone()); } self.last.map(|v| { v.borrow_mut().next = Some(n.clone()) }); self.last = Some(n); self } ...
No Kotlin, o mesmo parece muito mais simples:
public fun add(value: Int) { val newNode = ListNode(null, value); root = root ?: newNode; last?.next = newNode last = newNode; }
Como descobri mais tarde, essas estruturas não são típicas do Rust e meu código é completamente não-idiomático. As pessoas até escrevem artigos inteiros:
Aqui, a Rust sacrifica a legibilidade por segurança. Além disso, esses exercícios ainda podem levar a links em loop que ficam na memória, porque nenhum coletor de lixo os levará embora. Como não escrevi código de trabalho no Rust, é difícil dizer o quanto essas dificuldades complicam a vida. Seria interessante receber comentários de engenheiros em exercício.
Dificuldade de aprendizagem
O longo processo de aprendizado da ferrugem segue em grande parte a seção anterior. Antes de escrever qualquer coisa, você precisa dedicar algum tempo ao domínio do conceito-chave de propriedade da memória, como permeia todas as linhas. Por exemplo, a lista mais simples me levou algumas noites, enquanto no Kotlin a mesma coisa é escrita em 10 minutos, embora essa não seja a minha linguagem de trabalho. Além disso, muitas abordagens familiares para escrever algoritmos ou estruturas de dados no Rust parecerão diferentes ou não funcionarão. I.e. ao mudar para isso, será necessária uma reestruturação mais profunda do pensamento, apenas dominar a sintaxe não será suficiente. Isso está longe do JavaScript, que engole e suporta tudo. Eu acho que Rust nunca será a linguagem que as crianças aprendem em uma escola de programação. Mesmo C / C ++ tem mais chances nesse sentido.
No final
Achei muito interessante a idéia de gerenciar a memória na fase de compilação. No C / C ++, não tenho experiência, portanto não compararei com o ponteiro inteligente. A sintaxe é geralmente agradável e não há nada supérfluo. Critiquei o Rust pela complexidade da implementação de estruturas de dados de gráficos, mas suspeito que esse seja um recurso de todas as linguagens de programação que não sejam do GC. Talvez a comparação com Kotlin não tenha sido totalmente honesta.
Todo
Neste artigo, eu não toquei em multithreading, acho que esse é um grande tópico separado. Ainda há planos de escrever algum tipo de estrutura ou algoritmo de dados mais complicado que a lista; se você tiver idéias, compartilhe nos comentários. Seria interessante saber que tipos de aplicativos são geralmente escritos em Rust.
Ler
Se você está interessado em Rust, aqui estão alguns links:
UPD: Obrigado a todos por seus comentários. Eu aprendi muitas coisas úteis para mim. Imprecisões e erros de digitação corrigidos, links adicionados. Eu acho que essas discussões contribuem muito para o estudo de novas tecnologias.