10 avantages évidents de l'utilisation de Rust

Rust est un langage de programmation système jeune et ambitieux. Il implémente la gestion automatique de la mémoire sans un garbage collector et autres frais généraux de temps d'exécution. De plus, la langue par défaut est utilisée dans la langue Rust, il existe des règles sans précédent pour accéder aux données mutables et les durées de vie des liens sont également prises en compte. Cela lui permet de garantir la sécurité de la mémoire et facilite la programmation multi-thread, en raison du manque de course aux données.



Tout cela est déjà bien connu de tous ceux qui suivent au moins un peu le développement des technologies de programmation modernes. Mais que se passe-t-il si vous n'êtes pas programmeur système et qu'il n'y a pas beaucoup de code multithread dans vos projets, mais vous êtes toujours attiré par les performances de Rust. Tirerez-vous des avantages supplémentaires de son utilisation dans les applications? Ou tout ce qu'il vous donnera en plus est un combat acharné avec le compilateur, qui vous forcera à écrire le programme afin qu'il suive constamment les règles du langage sur l'emprunt et la propriété?


Cet article a rassemblé une douzaine d'avantages non évidents et pas particulièrement annoncés de l'utilisation de Rust, ce qui, je l'espère, vous aidera à décider du choix de cette langue pour vos projets.


1. L'universalité de la langue


Malgré le fait que Rust soit positionné comme un langage pour la programmation système, il convient également pour résoudre des problèmes appliqués de haut niveau. Vous n'avez pas à travailler avec des pointeurs bruts, sauf si vous en avez besoin pour votre tâche. La bibliothèque de langues standard a déjà implémenté la plupart des types et fonctions qui peuvent être nécessaires au développement d'applications. Vous pouvez également connecter facilement des bibliothèques externes et les utiliser. Le système de type et la programmation généralisée dans Rust permettent d'utiliser des abstractions d'un niveau assez élevé, bien qu'il n'y ait pas de support direct pour la POO dans le langage.


Regardons quelques exemples simples d'utilisation de Rust.


Un exemple de combinaison de deux itérateurs en un seul itérateur sur des paires d'éléments:


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]); 

Exécuter


Remarque: un appel au name!(...) format name!(...) est un appel à une macro fonctionnelle. Les noms de ces macros dans Rust se terminent toujours par un symbole ! afin qu'ils puissent être distingués des noms de fonction et autres identifiants. Les avantages de l'utilisation des macros seront discutés ci-dessous.

Un exemple d'utilisation de la bibliothèque d'expressions régulières pour travailler avec des expressions régulières:


 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")); 

Exécuter


Un exemple de l'implémentation du Add pour la propre structure Point pour surcharger l'opérateur d'addition:


 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; 

Exécuter


Un exemple d'utilisation d'un type générique dans une structure:


 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 }; 

Exécuter


Sur Rust, vous pouvez écrire des utilitaires système efficaces, de grandes applications de bureau, des microservices, des applications Web (y compris la partie client, car Rust peut être compilé dans Wasm), des applications mobiles (bien que l'écosystème linguistique soit encore mal développé dans cette direction). Une telle polyvalence peut être un avantage pour les équipes multi-projets, car elle vous permet d'utiliser les mêmes approches et les mêmes modules dans de nombreux projets différents. Si vous êtes habitué au fait que chaque outil est conçu pour son champ d'application étroit, essayez de considérer Rust comme une boîte à outils avec la même fiabilité et la même commodité. C'est peut-être exactement ce qui vous manquait.


2. Outils de construction et de gestion des dépendances pratiques


Ce n'est clairement pas annoncé, mais beaucoup remarquent que Rust possède l'un des meilleurs systèmes de gestion de la construction et des dépendances disponibles aujourd'hui. Si vous avez programmé en C ou C ++, et que la question de l'utilisation indolore des bibliothèques externes était assez aiguë pour vous, alors utiliser Rust avec son outil de construction et son gestionnaire de dépendance Cargo sera un bon choix pour vos nouveaux projets.


En plus du fait que Cargo télécharge des dépendances pour vous et gère leurs versions, crée et exécute vos applications, exécute des tests et génère de la documentation, il peut également être étendu avec des plugins pour d'autres fonctions utiles. Par exemple, il existe des extensions qui permettent à Cargo de déterminer les dépendances obsolètes de votre projet, d'effectuer une analyse statique du code source, de créer et de redéployer des parties clientes d'applications Web, et bien plus encore.


Le fichier de configuration Cargo utilise le langage de balisage toml convivial et minimal pour décrire les paramètres du projet. Voici un exemple de Cargo.toml configuration typique de 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 = "*" 

Et voici trois commandes typiques pour utiliser Cargo:


 $ cargo check $ cargo test $ cargo run 

Avec leur aide, le code source sera vérifié pour les erreurs de compilation, l'assemblage du projet et le lancement des tests, l'assemblage et le lancement du programme pour l'exécution, respectivement.


3. Tests intégrés


Écrire des tests unitaires dans Rust est si facile et simple que vous voulez le faire encore et encore. :) Il sera souvent plus facile d'écrire un test unitaire que d'essayer de tester la fonctionnalité d'une autre manière. Voici un exemple de fonctions et de tests pour eux:


 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)); } } 

Exécuter


Les fonctions du module de test , marquées de l'attribut #[test] , sont des tests unitaires. Ils seront exécutés en parallèle lors de l'appel de la commande de cargo test . L'attribut de compilation conditionnelle #[cfg(test)] , qui marque l'ensemble du module avec des tests, conduira au fait que le module sera compilé uniquement lorsque les tests seront exécutés et n'entrera pas dans l'assembly normal.


Il est très pratique de placer les tests dans le même module que la fonctionnalité sous test, simplement en y ajoutant le sous-module de test . Et si vous avez besoin de tests d'intégration, placez simplement vos tests dans le répertoire tests à la racine du projet et utilisez-y votre application comme package externe. Dans ce cas, il n'est pas nécessaire d'ajouter un module de test séparé et des directives de compilation conditionnelle.


Des exemples spéciaux de documentation exécutée en tant que tests méritent une attention particulière, mais cela sera discuté ci-dessous.


Des tests de performances intégrés (benchmarks) sont également disponibles, mais ils ne sont pas encore stables, ils ne sont donc disponibles que dans les assemblys de nuit du compilateur. Dans Rust stable, vous devrez utiliser des bibliothèques externes pour ce type de test.


4. Bonne documentation avec des exemples actuels


La bibliothèque Rust standard est très bien documentée. La documentation HTML est automatiquement générée à partir du code source avec des descriptions de démarques dans les commentaires du dock. De plus, les commentaires doc dans le code Rust contiennent un exemple de code qui s'exécute lorsque les tests sont exécutés. Cela garantit la pertinence des exemples:


 /// 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 } 

La documentation


Voici un exemple d'utilisation de la méthode as_bytes de type String


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

sera exécuté comme test lors du lancement des tests.


De plus, la pratique de créer des exemples de leur utilisation sous la forme de petits programmes indépendants situés dans le répertoire d' examples à la racine du projet est courante pour les bibliothèques Rust. Ces exemples sont également une partie importante de la documentation et ils sont également compilés et exécutés pendant l'exécution du test, mais ils peuvent être exécutés indépendamment des tests.


5. Auto-déduction intelligente des types


Dans un programme Rust, vous ne pouvez pas spécifier explicitement le type d'expression si le compilateur est capable de le générer automatiquement en fonction du contexte d'utilisation. Et cela ne s'applique pas seulement aux endroits où les variables sont déclarées. Regardons un exemple:


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

Exécuter


Si nous organisons les annotations de type, cet exemple ressemblera à ceci:


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

Autrement dit, nous avons un vecteur de tranches de chaîne et une variable de type tranche de chaîne. Mais dans ce cas, la spécification des types est complètement redondante, car le compilateur peut les produire par lui-même (en utilisant la version étendue de l'algorithme Hindley-Milner ). Le fait que vec est un vecteur est déjà clair par le type de la valeur de retour de Vec::new() , mais il n'est pas encore clair quel type de ses éléments seront. Le fait que le type de text soit une tranche de chaîne est compréhensible par le fait qu'on lui affecte un littéral de ce type. Ainsi, après vec.push(text) , le type d'éléments vectoriels devient évident. Notez que le type de la variable vec été complètement déterminé par son utilisation dans le thread d'exécution, et non au stade de l'initialisation.


Un tel système d'inférence de type élimine le bruit du code et le rend aussi concis que le code dans un langage de programmation typé dynamiquement. Et cela tout en conservant une frappe statique stricte!


Bien sûr, nous ne pouvons pas nous débarrasser complètement de la saisie dans une langue typée statiquement. Le programme doit avoir des points auxquels les types d'objets sont garantis pour être connus, afin qu'à d'autres endroits ces types puissent être affichés. De tels points dans Rust sont des déclarations de types de données définis par l'utilisateur et de signatures de fonctions, dans lesquelles on ne peut que spécifier les types utilisés. Mais vous pouvez y entrer des "méta-variables de types", en utilisant une programmation généralisée.


6. Correspondance de modèle aux points de déclaration variables


let opération


 let p = Point::new(); 

pas vraiment limité à simplement déclarer de nouvelles variables. Ce qu'elle fait, c'est faire correspondre l'expression à droite du signe égal avec le motif à gauche. Et de nouvelles variables peuvent être introduites dans le cadre de l'échantillon (et seulement ainsi). Jetez un œil à l'exemple suivant, et il deviendra plus clair pour vous:


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

Exécuter


La déstructuration a été effectuée ici: une telle comparaison introduira les variables x et y , qui seront initialisées avec la valeur des champs x et y de l'objet de la structure Point , qui est retournée en appelant Point::new() . Dans le même temps, la comparaison est correcte, car le type de l'expression à droite correspond au modèle de Point de type Point à gauche. De la même manière, vous pouvez prendre, par exemple, les deux premiers éléments d'un tableau:


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

Et bien plus encore. La chose la plus remarquable est que de telles comparaisons sont effectuées dans tous les endroits où de nouveaux noms de variables peuvent être saisis dans Rust, à savoir: dans la match , les if let let , if let , while let , dans l'en-tête de la boucle for , dans les arguments des fonctions et des fermetures. Voici un exemple d'utilisation élégante de la correspondance de motifs dans une boucle for :


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

Exécuter


La méthode enumerate , appelée sur l'itérateur, construit un nouvel itérateur, qui itérera non pas sur les valeurs initiales, mais sur les tuples, les paires "index ordinal, valeur initiale". Chacun de ces tuples pendant l'itération du cycle sera mappé sur le modèle spécifié (i, ch) , à la suite de quoi la variable i recevra la première valeur du tuple - l'index et la variable ch - la seconde, c'est-à-dire le caractère de la chaîne. Plus loin dans le corps de la boucle, nous pouvons utiliser ces variables.


Un autre exemple populaire d'utilisation d'un motif dans une boucle for :


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

Ici, nous ignorons simplement la valeur de l'itérateur en utilisant le modèle _ . Parce que nous n'utilisons pas le numéro d'itération dans le corps de la boucle. La même chose peut être faite, par exemple, avec un argument de fonction:


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

Ou lors d'une correspondance dans une déclaration 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"), _ => (), //        } 

Exécuter


La correspondance de motifs rend le code très compact et expressif, et dans l'énoncé de match , il est généralement irremplaçable. L'opérateur de match est un opérateur d'analyse variée complète, vous ne pourrez donc pas accidentellement oublier de vérifier certaines des correspondances possibles pour l'expression analysée.


7. Extension de syntaxe et DSL personnalisé


La syntaxe de Rust est limitée, en grande partie à cause de la complexité du système de type utilisé dans le langage. Par exemple, Rust n'a pas d'arguments de fonction nommés ou de fonctions avec un nombre variable d'arguments. Mais vous pouvez contourner ces limitations et d'autres avec des macros. Rust a deux types de macros: déclarative et procédurale. Avec les macros déclaratives, vous n'aurez jamais les mêmes problèmes qu'avec les macros en C, car elles sont hygiéniques et ne fonctionnent pas au niveau du remplacement de texte, mais au niveau du remplacement dans l'arborescence de syntaxe abstraite. Les macros vous permettent de créer des abstractions au niveau de la syntaxe du langage. Par exemple:


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

Outre le fait que cette macro étend les capacités syntaxiques d'appeler la "fonction" d'impression d'une chaîne formatée, elle vérifiera également dans son implémentation que les arguments d'entrée correspondent à la chaîne de format spécifiée au moment de la compilation et non au moment de l'exécution. À l'aide de macros, vous pouvez entrer une syntaxe concise pour vos propres besoins de conception, créer et utiliser DSL. Voici un exemple d'utilisation de code JavaScript dans un programme Rust compilé dans 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); 

Macro js! défini dans le package stdweb et il vous permet d'incorporer du code JavaScript à part entière dans votre programme (à l'exception des chaînes entre guillemets simples et des opérateurs non complétés par un point-virgule) et d'utiliser des objets du code Rust en utilisant la syntaxe @{expr} .


Les macros offrent d'énormes possibilités pour adapter la syntaxe des programmes Rust aux tâches spécifiques d'un domaine particulier. Ils vous feront gagner du temps et de l'attention lors du développement d'applications complexes. Non pas en augmentant la surcharge d'exécution, mais en augmentant le temps de compilation. :)


8. Génération automatique de code dépendant


Les macros de dérivation procédurale de Rust sont largement utilisées pour implémenter automatiquement des traits et d'autres génération de code. Voici un exemple:


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

Étant donné que tous ces types ( Copy , Clone , Debug , Default , PartialEq et Eq ) de la bibliothèque standard sont implémentés pour le type de champs de la structure i32 , leur implémentation peut être affichée automatiquement pour l'ensemble de la structure dans son ensemble. Un autre exemple:


 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(); 

Exécuter


Ici, en utilisant les Deserialize et Deserialize de la bibliothèque serde pour la structure Point , les méthodes pour sa sérialisation et sa désérialisation sont générées automatiquement. Vous pouvez ensuite transmettre une instance de cette structure à diverses fonctions de sérialisation, par exemple, la convertir en chaîne JSON.


Vous pouvez créer vos propres macros procédurales qui généreront le code dont vous avez besoin. Ou utilisez les nombreuses macros déjà créées par d'autres développeurs. En plus d'économiser le programmeur de l'écriture de code passe-partout, les macros ont également l'avantage que vous n'avez pas besoin de maintenir différentes sections de code dans un état cohérent. Par exemple, si un troisième champ z est ajouté à la structure Point , pour effectuer correctement sa sérialisation, si vous utilisez dériver, vous n'avez rien d'autre à faire. Si nous mettons nous-mêmes en œuvre les traits nécessaires à la sérialisation de Point , alors nous devrons nous assurer que cette mise en œuvre est toujours cohérente avec les derniers changements dans la structure de Point .


9. Type de données algébrique


Pour le dire simplement, un type de données algébrique est un type de données composite qui est une union de structures. Plus formellement, il s'agit d'une somme de types de produits. Dans Rust, ce type est défini à l'aide du mot clé enum :


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

Le type d'une valeur particulière d'une variable de type Message ne peut être que l'un des types de structure répertoriés dans Message . Il s'agit soit d'une structure Quit sans champ de type unité, de l'une des structures de tuple ChangeColor ou Write avec des champs sans nom, ou de la structure Move habituelle. Un type énuméré traditionnel peut être représenté comme un cas particulier d'un type de données algébrique:


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

Il est possible de savoir quel type a réellement pris de la valeur dans un cas particulier en utilisant la correspondance de modèles:


 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), }; } 

Exécuter


Sous la forme de types de données algébriques, Rust implémente des types importants tels que Option et Result , qui sont utilisés pour représenter la valeur manquante et le résultat correct / erroné, respectivement. Voici comment l' Option est définie dans la bibliothèque standard:


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

Rust n'a pas de valeur nulle, tout comme les erreurs gênantes d'un appel inattendu. Au lieu de cela, lorsqu'il est vraiment nécessaire d'indiquer la possibilité d'une valeur manquante, Option utilisée:


 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"), } 

Exécuter


Le type de données algébrique est un outil puissant et expressif qui ouvre la porte au développement piloté par type. Un programme écrit avec compétence dans ce paradigme attribue la plupart des vérifications de l'exactitude de son travail au système de type. Par conséquent, si vous manquez un peu de Haskell dans la programmation industrielle quotidienne, Rust peut être votre point de vente. :)


10. Refactoring facile


Le système de type statique strict développé dans Rust et la tentative d'effectuer autant de vérifications que possible pendant la compilation, conduit au fait que la modification et la refactorisation du code deviennent assez simples et sûres. Si, après les modifications, le programme a été compilé, cela signifie qu'il n'a laissé que des erreurs logiques qui n'étaient pas liées à la fonctionnalité dont la vérification a été affectée au compilateur. Combiné à la facilité d'ajouter des tests unitaires à la logique de test, cela conduit à de sérieuses garanties de fiabilité des programmes et à une augmentation de la confiance du programmeur dans le bon fonctionnement de son code après avoir effectué des modifications.




C'est peut-être tout ce dont je voulais parler dans cet article. Bien sûr, Rust présente de nombreux autres avantages, ainsi qu'un certain nombre d'inconvénients (une certaine humidité du langage, un manque d'idiomes de programmation familiers et une syntaxe «non littéraire»), qui ne sont pas mentionnés ici. Si vous avez quelque chose à dire à leur sujet, écrivez dans les commentaires. En général, essayez Rust dans la pratique. Et peut-être que ses avantages pour vous l'emporteront sur toutes ses lacunes, comme cela s'est produit dans mon cas. Et enfin, vous obtiendrez exactement l'ensemble d'outils dont vous aviez besoin pendant longtemps.

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


All Articles