10 benefícios não óbvios do uso do Rust

Rust é uma linguagem de programação de sistema jovem e ambiciosa. Ele implementa o gerenciamento automático de memória sem um coletor de lixo e outras sobrecargas no tempo de execução. Além disso, o idioma padrão é usado no idioma Rust, existem regras sem precedentes para acessar dados mutáveis ​​e a vida útil do link também é levada em consideração. Isso lhe permite garantir a segurança da memória e facilita a programação multithread, devido à falta de corridas de dados.



Tudo isso já é bem conhecido por todos que acompanham pelo menos um pouco o desenvolvimento de modernas tecnologias de programação. Mas e se você não for um programador de sistemas e não houver muitos códigos multithread em seus projetos, mas você ainda estiver atraído pelo desempenho do Rust. Você obterá benefícios adicionais com o uso em aplicativos? Ou tudo o que ele lhe dará adicionalmente é uma luta dura com o compilador, o que forçará você a escrever o programa para que ele siga constantemente as regras da linguagem sobre empréstimos e propriedade?


Este artigo reuniu uma dúzia de vantagens não óbvias e não particularmente anunciadas do uso do Rust, o que, espero, ajudará você a decidir sobre a escolha desse idioma para seus projetos.


1. A universalidade da língua


Apesar do Rust estar posicionado como uma linguagem para a programação do sistema, ele também é adequado para resolver problemas aplicados de alto nível. Você não precisa trabalhar com ponteiros brutos, a menos que precise deles para sua tarefa. A biblioteca de idiomas padrão já implementou a maioria dos tipos e funções que podem ser necessárias no desenvolvimento de aplicativos. Você também pode conectar facilmente bibliotecas externas e usá-las. O sistema de tipos e a programação generalizada no Rust permitem o uso de abstrações de um nível bastante alto, embora não haja suporte direto para OOP na linguagem.


Vejamos alguns exemplos simples de uso do Rust.


Um exemplo de combinação de dois iteradores em um iterador sobre pares de elementos:


let zipper: Vec<_> = (1..).zip("foo".chars()).collect(); assert_eq!((1, 'f'), zipper[0]); assert_eq!((2, 'o'), zipper[1]); assert_eq!((3, 'o'), zipper[2]); 

Executar


Nota: uma chamada para o name!(...) do formato name!(...) é uma chamada para uma macro funcional. Os nomes dessas macros no Rust sempre terminam com um símbolo ! para que eles possam ser distinguidos dos nomes das funções e outros identificadores. Os benefícios do uso de macros serão discutidos abaixo.

Um exemplo de uso da biblioteca regex externa para trabalhar com expressões regulares:


 extern crate regex; use regex::Regex; let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); assert!(re.is_match("2018-12-06")); 

Executar


Um exemplo da implementação da Add para a própria estrutura Point para sobrecarregar o operador de adição:


 use std::ops::Add; struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } } } let p1 = Point { x: 1, y: 0 }; let p2 = Point { x: 2, y: 3 }; let p3 = p1 + p2; 

Executar


Um exemplo de uso de um tipo genérico em uma estrutura:


 struct Point<T> { x: T, y: T, } let int_origin = Point { x: 0, y: 0 }; let float_origin = Point { x: 0.0, y: 0.0 }; 

Executar


No Rust, você pode escrever utilitários de sistema eficientes, grandes aplicativos de desktop, microsserviços, aplicativos da Web (incluindo a parte do cliente, pois o Rust pode ser compilado no Wasm), aplicativos móveis (embora o ecossistema de idiomas ainda seja pouco desenvolvido nessa direção). Essa versatilidade pode ser uma vantagem para equipes de multiprojetos, pois permite usar as mesmas abordagens e os mesmos módulos em muitos projetos diferentes. Se você está acostumado com o fato de que cada ferramenta é projetada para seu campo de aplicação restrito, tente ver o Rust como uma caixa de ferramentas com a mesma confiabilidade e conveniência. Talvez seja exatamente isso que você estava perdendo.


2. Ferramentas práticas de gerenciamento de compilação e dependência


Isso claramente não é anunciado, mas muitos percebem que o Rust possui um dos melhores sistemas de gerenciamento de compilação e dependência disponíveis atualmente. Se você programou em C ou C ++, e a questão do uso indolor de bibliotecas externas foi bastante aguda para você, usar o Rust com sua ferramenta de construção e o gerenciador de dependência de carga será uma boa opção para seus novos projetos.


Além do fato de o Cargo fazer download de dependências para você e gerenciar suas versões, criar e executar seus aplicativos, executar testes e gerar documentação, também pode ser expandido com plugins para outras funções úteis. Por exemplo, existem extensões que permitem ao Cargo determinar as dependências obsoletas do seu projeto, realizar análises estáticas do código-fonte, criar e reimplantar partes de clientes de aplicativos da Web e muito mais.


O arquivo de configuração do Cargo usa a linguagem de marcação amigável e mínima para descrever as configurações do projeto. Aqui está um exemplo de um Cargo.toml configuração típico do Cargo.toml :


 [package] name = "some_app" version = "0.1.0" authors = ["Your Name <you@example.com>"] [dependencies] regex = "1.0" chrono = "0.4" [dev-dependencies] rand = "*" 

E abaixo estão três comandos típicos para usar o Cargo:


 $ cargo check $ cargo test $ cargo run 

Com sua ajuda, o código-fonte será verificado quanto a erros de compilação, montagem do projeto e lançamento de testes, montagem e lançamento do programa para execução, respectivamente.


3. Testes internos


Escrever testes de unidade no Rust é tão fácil e simples que você deseja fazer isso de novo e de novo. :) Frequentemente, será mais fácil escrever um teste de unidade do que tentar testar a funcionalidade de outra maneira. Aqui está um exemplo de funções e testes para eles:


 pub fn is_false(a: bool) -> bool { !a } pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod test { use super::*; #[test] fn is_false_works() { assert!(is_false(false)); assert!(!is_false(true)); } #[test] fn add_two_works() { assert_eq!(1, add_two(-1)); assert_eq!(2, add_two(0)); assert_eq!(4, add_two(2)); } } 

Executar


As funções no módulo de test , marcadas com o atributo #[test] , são testes de unidade. Eles serão executados em paralelo quando o comando de cargo test for chamado. O atributo de compilação condicional #[cfg(test)] , que marca todo o módulo com testes, levará ao fato de que o módulo será compilado somente quando os testes forem executados, mas não entrará na montagem normal.


É muito conveniente colocar os testes no mesmo módulo que o funcional em teste, simplesmente adicionando o submódulo de test a ele. E se você precisar de testes de integração, basta colocá-los no diretório de tests na raiz do projeto e usar seu aplicativo neles como um pacote externo. Um módulo de test separado e as diretivas de compilação condicional nesse caso não precisam ser adicionadas.


Exemplos especiais de documentação executada como testes merecem atenção especial, mas isso será discutido abaixo.


Testes de desempenho internos (benchmarks) também estão disponíveis, mas ainda não são estáveis, portanto, estão disponíveis apenas em montagens noturnas do compilador. No Rust estável, você precisará usar bibliotecas externas para esse tipo de teste.


4. Boa documentação com exemplos atuais


A biblioteca Rust padrão está muito bem documentada. A documentação HTML é gerada automaticamente a partir do código-fonte com descrições de descontos nos comentários do dock. Além disso, os comentários do documento no código Rust contêm código de amostra que é executado quando os testes são executados. Isso garante a relevância dos exemplos:


 /// Returns a byte slice of this `String`'s contents. /// /// The inverse of this method is [`from_utf8`]. /// /// [`from_utf8`]: #method.from_utf8 /// /// # Examples /// /// Basic usage: /// /// ``` /// let s = String::from("hello"); /// /// assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes()); /// ``` #[inline] #[stable(feature = "rust1", since = "1.0.0")] pub fn as_bytes(&self) -> &[u8] { &self.vec } 

A documentação


Aqui está um exemplo do uso do método as_bytes do tipo String


 let s = String::from("hello"); assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes()); 

será executado como um teste durante o lançamento dos testes.


Além disso, a prática de criar exemplos de seu uso na forma de pequenos programas independentes localizados no diretório de examples na raiz do projeto é comum para as bibliotecas Rust. Esses exemplos também são uma parte importante da documentação e também são compilados e executados durante a execução do teste, mas podem ser executados independentemente dos testes.


5. Dedução automática inteligente de tipos


Em um programa Rust, você não pode especificar explicitamente o tipo de expressão se o compilador puder produzi-la automaticamente com base no contexto de uso. E isso se aplica não apenas aos locais onde as variáveis ​​são declaradas. Vejamos um exemplo:


 let mut vec = Vec::new(); let text = "Message"; vec.push(text); 

Executar


Se organizarmos as anotações de tipo, este exemplo será semelhante a este:


 let mut vec: Vec<&str> = Vec::new(); let text: &str = "Message"; vec.push(text); 

Ou seja, temos um vetor de fatias de sequência e uma variável do tipo fatia de sequência. Mas, neste caso, a especificação de tipos é completamente redundante, pois o compilador pode produzi-los por si só (usando a versão estendida do algoritmo Hindley-Milner ). O fato de vec ser um vetor já é claro pelo tipo de valor de retorno de Vec::new() , mas ainda não está claro qual será o tipo de seus elementos. O fato de o tipo de text ser uma fatia de sequência é compreensível pelo fato de ser atribuído um literal desse tipo. Assim, após vec.push(text) , o tipo de elementos do vetor se torna óbvio. Observe que o tipo da variável vec foi completamente determinado por seu uso no encadeamento de execução, e não no estágio de inicialização.


Esse sistema de inferência de tipo elimina o ruído do código e o torna tão conciso quanto o código em alguma linguagem de programação de tipo dinâmico. E isso mantendo a digitação estática estrita!


Obviamente, não podemos nos livrar completamente da digitação em um idioma estaticamente digitado. O programa deve ter pontos nos quais os tipos de objetos têm garantia de serem conhecidos, para que em outros locais esses tipos possam ser exibidos. Tais pontos no Rust são declarações de tipos de dados definidos pelo usuário e assinaturas de funções, nas quais não se pode deixar de especificar os tipos utilizados. Mas você pode inserir "meta-variáveis ​​de tipos" nelas, usando programação generalizada.


6. Correspondência de padrões em pontos de declaração variáveis


let operação


 let p = Point::new(); 

não é realmente limitado a apenas declarar novas variáveis. O que ela realmente faz é igualar a expressão à direita do sinal de igual com o padrão à esquerda. E novas variáveis ​​podem ser introduzidas como parte da amostra (e somente isso). Dê uma olhada no exemplo a seguir e ele ficará mais claro:


 let Point { x, y } = Point::new(); 

Executar


A desestruturação é realizada aqui: essa comparação apresentará as variáveis x e y , que serão inicializadas com o valor dos campos x e y do objeto da estrutura Point , retornado pela chamada Point::new() . Ao mesmo tempo, a comparação está correta, pois o tipo de expressão à direita corresponde ao padrão de Point do tipo Point à esquerda. De maneira semelhante, você pode pegar, por exemplo, os dois primeiros elementos de uma matriz:


 let [a, b, _] = [1, 2, 3]; 

E muito mais a fazer. O mais notável é que essas comparações são realizadas em todos os locais em que novos nomes de variáveis ​​podem ser inseridos no Rust, a saber: na match , let , if let , while let if let , no cabeçalho do loop for , nos argumentos de funções e fechamentos. Aqui está um exemplo de como usar elegantemente a correspondência de padrões em um loop for :


 for (i, ch) in "foo".chars().enumerate() { println!("Index: {}, char: {}", i, ch); } 

Executar


O método enumerate , chamado no iterador, constrói um novo iterador, que iterará não sobre os valores iniciais, mas as tuplas, pares "índice ordinal, valor inicial". Cada uma dessas tuplas durante a iteração do ciclo será mapeada para o padrão especificado (i, ch) , como resultado da variável i receberá o primeiro valor da tupla - o índice e a variável ch - o segundo, ou seja, o caractere da string. Mais adiante, no corpo do loop, podemos usar essas variáveis.


Outro exemplo popular de usar um padrão em um loop for :


 for _ in 0..5 { //   5  } 

Aqui, simplesmente ignoramos o valor do iterador usando o padrão _ . Porque não usamos o número da iteração no corpo do loop. O mesmo pode ser feito, por exemplo, com um argumento de função:


 fn foo(a: i32, _: bool) { //      } 

Ou ao fazer a correspondência em uma declaração de match :


 match p { Point { x: 1, .. } => println!("Point with x == 1 detected"), Point { y: 2, .. } => println!("Point with x != 1 and y == 2 detected"), _ => (), //        } 

Executar


A correspondência de padrões torna o código muito compacto e expressivo e, na declaração de match , geralmente é insubstituível. O operador de match é um operador de análise variada completa, portanto você não poderá esquecer acidentalmente de verificar algumas das correspondências possíveis para a expressão analisada nela.


7. Extensão de sintaxe e DSL personalizado


A sintaxe da ferrugem é limitada, em grande parte devido à complexidade do sistema de tipos usado no idioma. Por exemplo, Rust não possui argumentos de função nomeados ou funções com um número variável de argumentos. Mas você pode contornar essas e outras limitações com macros. O Rust possui dois tipos de macros: declarativo e processual. Com macros declarativas, você nunca terá os mesmos problemas que as macros em C, porque são higiênicas e não funcionam no nível de substituição de texto, mas no nível de substituição na árvore de sintaxe abstrata. As macros permitem criar abstrações no nível da sintaxe do idioma. Por exemplo:


 println!("Hello, {name}! Do you know about {}?", 42, name = "User"); 

Além do fato de que essa macro expande os recursos sintáticos de chamar a "função" de imprimir uma cadeia de caracteres formatada, também verificará em sua implementação que os argumentos de entrada correspondem à cadeia de formato especificada no tempo de compilação e não no tempo de execução. Usando macros, você pode inserir sintaxe concisa para suas próprias necessidades de design, criar e usar DSL. Aqui está um exemplo do uso de código JavaScript dentro de um programa Rust compilando no Wasm:


 let name = "Bob"; let result = js! { var msg = "Hello from JS, " + @{name} + "!"; console.log(msg); alert(msg); return 2 + 2; }; println!("2 + 2 = {:?}", result); 

js! macro js! definido no pacote stdweb e permite incorporar código JavaScript completo em seu programa (com exceção de cadeias e operadores de aspas simples não concluídas com ponto-e-vírgula) e usar objetos do código Rust usando a sintaxe @{expr} .


As macros oferecem enormes oportunidades para adaptar a sintaxe dos programas Rust às tarefas específicas de uma área específica. Eles economizarão seu tempo e atenção ao desenvolver aplicativos complexos. Não aumentando a sobrecarga do tempo de execução, mas aumentando o tempo de compilação. :)


8. Geração automática de código dependente


As macros de derivação processual da Rust são amplamente usadas para implementar automaticamente características e outra geração de código. Aqui está um exemplo:


 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] struct Point { x: i32, y: i32, } 

Como todos esses tipos ( Copy , Clone , Debug , Default , PartialEq e Eq ) da biblioteca padrão são implementados para o tipo de campo da estrutura i32 , sua implementação pode ser exibida automaticamente para toda a estrutura como um todo. Outro exemplo:


 extern crate serde_derive; extern crate serde_json; use serde_derive::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct Point { x: i32, y: i32, } let point = Point { x: 1, y: 2 }; //  Point  JSON . let serialized = serde_json::to_string(&point).unwrap(); assert_eq!("{\"x\":1,\"y\":2}", serialized); //  JSON   Point. let deserialized: Point = serde_json::from_str(&serialized).unwrap(); 

Executar


Aqui, usando as Deserialize e serde biblioteca serde para a estrutura Point , são gerados automaticamente métodos para sua serialização e desserialização. Em seguida, você pode passar uma instância dessa estrutura para várias funções de serialização, por exemplo, convertendo-a em uma sequência JSON.


Você pode criar suas próprias macros processuais que irão gerar o código necessário. Ou use as muitas macros já criadas por outros desenvolvedores. Além de salvar o programador de escrever código padrão, as macros também têm a vantagem de que você não precisa manter seções diferentes do código em um estado consistente. Por exemplo, se um terceiro campo z for adicionado à estrutura Point , para fazer sua serialização corretamente, se você usar derivar, não precisará fazer mais nada. Se nós mesmos implementarmos as características necessárias para a serialização do Point , teremos que garantir que essa implementação seja sempre consistente com as alterações mais recentes na estrutura do Point .


9. Tipo de dados algébrico


Simplificando, um tipo de dados algébrico é um tipo de dados composto que é uma união de estruturas. Mais formalmente, é uma soma de tipos de produtos. No Rust, esse tipo é definido usando a palavra-chave enum :


 enum Message { Quit, ChangeColor(i32, i32, i32), Move { x: i32, y: i32 }, Write(String), } 

O tipo de um valor específico de uma variável do tipo Message pode ser apenas um dos tipos de estrutura listados em Message . Essa é uma estrutura Quit sem campo, semelhante a uma unidade, uma das estruturas de tupla ChangeColor ou Write com campos sem nome ou a estrutura Move comum. Um tipo enumerado tradicional pode ser representado como um caso especial de um tipo de dados algébrico:


 enum Color { Red, Green, Blue, White, Black, Unknown, } 

É possível descobrir que tipo realmente recebeu valor em um caso específico usando a correspondência de padrões:


 let color: Color = get_color(); let text = match color { Color::Red => "Red", Color::Green => "Green", Color::Blue => "Blue", _ => "Other color", }; println!("{}", text); ... fn process_message(msg: Message) { match msg { Message::Quit => quit(), Message::ChangeColor(r, g, b) => change_color(r, g, b), Message::Move { x, y } => move_cursor(x, y), Message::Write(s) => println!("{}", s), }; } 

Executar


Na forma de tipos de dados algébricos, o Rust implementa tipos importantes como Option e Result , que são usados ​​para representar o valor ausente e o resultado correto / incorreto, respectivamente. Veja como o Option é definido na biblioteca padrão:


 pub enum Option<T> { None, Some(T), } 

Ferrugem não tem um valor nulo, assim como os erros irritantes de uma chamada inesperada. Em vez disso, onde é realmente necessário indicar a possibilidade de um valor ausente, Option usada:


 fn divide(numerator: f64, denominator: f64) -> Option<f64> { if denominator == 0.0 { None } else { Some(numerator / denominator) } } let result = divide(2.0, 3.0); match result { Some(x) => println!("Result: {}", x), None => println!("Cannot divide by 0"), } 

Executar


O tipo de dados algébrico é uma ferramenta poderosa e expressiva que abre as portas para o desenvolvimento orientado a tipos. Um programa escrito com competência nesse paradigma atribui a maioria das verificações da correção de seu trabalho ao sistema de tipos. Portanto, se você não tiver um pouco de Haskell na programação industrial cotidiana, o Rust pode ser sua saída. :)


10. Fácil refatoração


O sistema estrito de tipo estático desenvolvido no Rust e a tentativa de realizar o maior número possível de verificações durante a compilação levam ao fato de que modificar e refatorar o código se torna bastante simples e seguro. Se, após as alterações, o programa foi compilado, isso significa que ele deixou apenas erros lógicos que não estavam relacionados à funcionalidade cuja verificação foi atribuída ao compilador. Combinado com a facilidade de adicionar testes de unidade à lógica de teste, isso leva a sérias garantias da confiabilidade dos programas e a um aumento da confiança do programador na operação correta de seu código após fazer alterações.




Talvez seja sobre isso que eu queria falar neste artigo. Obviamente, o Rust tem muitas outras vantagens, além de várias desvantagens (alguma umidade da linguagem, falta de expressões familiares de programação e sintaxe “não literária”), que não são mencionadas aqui. Se você tem algo a dizer sobre eles, escreva nos comentários. Em geral, tente Rust na prática. E talvez as vantagens dele para você superem todas as deficiências dele, como aconteceu no meu caso. E, finalmente, você obterá exatamente o conjunto de ferramentas necessárias por um longo tempo.

Source: https://habr.com/ru/post/pt430294/


All Articles