Premiers pas pour la rouille

image


Bonjour à tous. Récemment, j'ai rencontré un nouveau langage de programmation Rust. J'ai remarqué qu'il était différent des autres que j'avais rencontrés auparavant. J'ai donc décidé de creuser plus profondément. Je souhaite partager les résultats et mes impressions:


  • Je vais commencer par les principales caractéristiques de Rust, à mon avis
  • Je vais décrire des détails de syntaxe intéressants
  • Je vais expliquer pourquoi Rust n'est pas susceptible de conquérir le monde

Je vais vous expliquer tout de suite que j'écris en Java depuis une dizaine d'années, donc je vais argumenter depuis mon clocher.


Fonction tueur


Rust essaie de prendre une position intermédiaire entre les langages de bas niveau tels que C / C ++ et Java / C # / Python / Ruby de haut niveau ... Plus le langage est proche du matériel, plus il y a de contrôle, plus il est facile de prédire comment le code s'exécutera. Mais avoir un accès complet à la mémoire est beaucoup plus facile de tirer sur votre jambe. Contrairement à C / C ++, Python / Java et tout le reste sont apparus. Ils n'ont pas besoin de penser à vider la mémoire. Le pire est le NPE, les fuites ne sont pas si courantes. Mais pour que cela fonctionne, vous avez besoin, au minimum, d'un garbage collector, qui, à son tour, commence à vivre sa propre vie, en parallèle avec le code utilisateur, ce qui réduit sa prévisibilité. La machine virtuelle offre toujours l'indépendance de la plate-forme, mais combien il en faut est un point discutable, je ne vais pas le soulever maintenant.


Rust est un langage de bas niveau, le compilateur génère un binaire, qui ne nécessite pas de trucs supplémentaires pour fonctionner. Toute la logique de suppression des objets inutiles est intégrée dans le code au moment de la compilation, c'est-à-dire il n'y a pas non plus de garbage collector lors de l'exécution. Rust n'a également aucune référence nulle et les types sont sûrs, ce qui le rend encore plus fiable que Java.


L'idée de posséder une référence d'objet et d'emprunter est au cœur de la gestion de la mémoire. Si une seule variable possède chaque objet, dès qu'il expire à la fin du bloc, tout ce qu'il pointe peut être effacé récursivement. Des liens peuvent également être empruntés pour la lecture ou l'écriture. Ici fonctionne le principe d'un écrivain et de nombreux lecteurs.


Ce concept peut être démontré dans le morceau de code suivant. Test () est appelé à partir de la méthode main () , qui crée une structure de données récursive MyStruct qui implémente l'interface du destructeur. Drop vous permet de définir la logique à exécuter avant que l'objet ne soit détruit. Quelque chose de similaire au finaliseur en Java, à la différence de Java, le moment de l'appel de la méthode drop () est tout à fait certain.


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

La conclusion sera la suivante:


 End of test Cleaning 1 Cleaning 2 End of main 

C'est-à-dire avant de quitter test (), la mémoire a été effacée récursivement. Le compilateur s'est occupé de cela en insérant le code nécessaire. Ce qui est Box et Option sera décrit un peu plus tard.


De cette façon, Rust prend la sécurité des langages de haut niveau et la prévisibilité des langages de programmation de bas niveau.


Quoi d'autre intéressant


Ensuite, je vais énumérer les caractéristiques de la langue par ordre décroissant d'importance, à mon avis.


Oop


Ici, Rust est généralement en avance sur les autres. Si la plupart des langues sont parvenues à la conclusion que l'héritage multiple doit être abandonné, alors dans Rust il n'y a aucun héritage du tout. C'est-à-dire une classe ne peut implémenter des interfaces qu'en quantité quelconque, mais ne peut pas hériter d'autres classes. En termes de Java, cela signifierait de rendre toutes les classes finales. En général, la variété syntaxique pour maintenir la POO n'est pas si grande. C'est peut-être pour le mieux.


Pour combiner des données, il existe des structures qui peuvent contenir une implémentation. Les interfaces sont appelées trait et peuvent également contenir des implémentations par défaut. Ils n'atteignent pas les classes abstraites, car ne peut pas contenir de champs, beaucoup se plaignent de cette restriction. La syntaxe est la suivante, je pense que les commentaires ne sont pas nécessaires ici:


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

Parmi les fonctionnalités que j'ai remarquées, il convient de noter les éléments suivants:


  • Les classes n'ont pas de constructeurs. Il n'y a que des initialiseurs qui spécifient les valeurs des champs entre accolades. Si vous avez besoin d'un constructeur, cela se fait par des méthodes statiques.
  • La méthode d'instance diffère de la méthode statique en ayant la référence & self comme premier argument.
  • Les classes, interfaces et méthodes peuvent également être généralisées. Mais contrairement à Java, ces informations ne sont pas perdues au moment de la compilation.

Un peu plus de sécurité


Comme je l'ai dit, Rust accorde une grande attention à la fiabilité du code et essaie d'éviter la plupart des erreurs au stade de la compilation. Pour cela, la possibilité de rendre les liens vides a été exclue. Cela m'a rappelé les types nullables de Kotlin. L'option est utilisée pour créer des liens vides. Tout comme dans Kotlin, en essayant d'accéder à une telle variable, le compilateur battra des mains, forçant à insérer des chèques. Essayer d'extraire la valeur sans vérifier peut entraîner une erreur. Mais cela ne peut certainement pas se faire par accident comme, par exemple, en Java.


J'ai également aimé le fait que toutes les variables et tous les champs de classe sont immuables par défaut. Bonjour encore Kotlin. Si la valeur peut changer, cela doit être indiqué explicitement avec le mot-clé mut . Je pense que le désir d'immuabilité améliore considérablement la lisibilité et la prévisibilité du code. Bien que l' option pour une raison quelconque soit modifiable, je n'ai pas compris cela, voici le code de la documentation:


 let mut x = Some(2); let y = x.take(); assert_eq!(x, None); assert_eq!(y, Some(2)); 

Transferts


La rouille est appelée enum . Ce n'est qu'en plus d'un nombre limité de valeurs qu'elles peuvent encore contenir des données et des méthodes arbitraires. Ainsi, c'est quelque chose entre les énumérations et les classes en Java. L' option d'énumération standard dans mon premier exemple appartient juste à ce type:


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

Il existe une construction spéciale pour traiter ces valeurs:


 fn main() { let a = Some(1); match a { None => println!("empty"), Some(v) => println!("{}", v) } } 

Aussi


Je n'ai pas l'intention d'écrire un manuel sur Rust, mais je veux simplement souligner ses caractéristiques. Dans cette section, je décrirai ce qui est utile, mais, à mon avis, pas si unique:


  • Les fans de programmation fonctionnelle ne seront pas déçus, il y a des lambdas pour eux. L'itérateur a des méthodes pour traiter la collection, par exemple, filter et for_each . Quelque chose comme les flux Java.
  • La construction de correspondance peut également être utilisée pour des choses plus complexes que l' énumération régulière, par exemple, pour le traitement de modèles.
  • Il existe un grand nombre de classes intégrées, par exemple, des collections: Vec, LinkedList, HashMap , etc.
  • Vous pouvez créer des macros
  • Il est possible d'ajouter des méthodes aux classes existantes
  • Inférence de type automatique prise en charge
  • Avec le langage vient un cadre de test standard
  • L'utilitaire de chargement intégré est utilisé pour créer et gérer les dépendances

Voler dans la pommade


Cette section est nécessaire pour compléter l'image.


Problème tueur


L'inconvénient principal vient de la caractéristique principale. Vous devez tout payer. Dans Rust, il est très gênant de travailler avec des structures de données de graphe modifiables, car tout objet ne doit pas avoir plus d'un lien. Pour contourner cette limitation, il existe un tas de classes intégrées:


  • Box - une valeur immuable sur le tas, un analogue des wrappers pour les primitives en Java
  • Cellule - valeur variable
  • RefCell - valeur variable accessible par référence
  • Rc - compteur de références, pour plusieurs références à un objet

Et ceci est une liste incomplète. Pour le premier échantillon de Rust, j'ai imprudemment décidé d'écrire une liste liée individuellement avec des méthodes de base. En fin de compte, le lien vers le nœud a abouti à l'option <Rc <RefCell <ListNode> >> suivante :


  • Option - pour traiter un lien vide
  • Rc - pour plusieurs liens, comme le dernier nœud est référencé par le nœud précédent et la feuille elle-même
  • RefCell - pour lien mutable
  • ListNode - l'élément suivant lui-même

Il semble moyen, un total de trois enveloppes autour d'un objet. Le code pour simplement ajouter un élément à la fin de la liste est très lourd, et il contient des éléments non évidents, tels que le clonage et l'emprunt:


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

Sur Kotlin, la même chose semble beaucoup plus simple:


 public fun add(value: Int) { val newNode = ListNode(null, value); root = root ?: newNode; last?.next = newNode last = newNode; } 

Comme je l'ai découvert plus tard, de telles structures ne sont pas typiques de Rust, et mon code est complètement non idiomatique. Les gens écrivent même des articles entiers:



Ici, Rust sacrifie la lisibilité pour la sécurité. De plus, de tels exercices peuvent toujours conduire à des liens en boucle qui se bloquent en mémoire, car aucun ramasse-miettes ne les enlèvera. Je n’ai pas écrit de code de travail dans Rust, donc j’ai du mal à dire à quel point de telles difficultés compliquent la vie. Il serait intéressant de recevoir les commentaires des ingénieurs en exercice.


Difficulté d'apprentissage


Le long processus d'apprentissage de Rust découle en grande partie de la section précédente. Avant d'écrire quoi que ce soit, vous devez passer du temps à maîtriser le concept clé de la propriété de la mémoire, comme il imprègne chaque ligne. Par exemple, la liste la plus simple m'a pris quelques soirées, alors que sur Kotlin la même chose est écrite en 10 minutes, bien que ce ne soit pas ma langue de travail. De plus, de nombreuses approches familières de l'écriture d'algorithmes ou de structures de données dans Rust seront différentes ou ne fonctionneront pas du tout. C'est-à-dire lors du passage à celui-ci, une restructuration plus profonde de la pensée sera nécessaire, il ne suffit pas de maîtriser la syntaxe. C'est loin de JavaScript, qui avale et endure tout. Je pense que Rust ne sera jamais la langue que les enfants apprennent dans une école de programmation. Même C / C ++ a plus de chances dans ce sens.


En fin de compte


J'ai trouvé l'idée de gérer la mémoire au stade de la compilation très intéressante. En C / C ++, je n'ai aucune expérience, donc je ne comparerai pas avec un pointeur intelligent. La syntaxe est généralement agréable et il n'y a rien de superflu. J'ai critiqué Rust pour la complexité de la mise en œuvre de structures de données graphiques, mais je soupçonne que c'est une caractéristique de tous les langages de programmation non GC. Peut-être que la comparaison avec Kotlin n'était pas entièrement honnête.


Todo


Dans cet article, je n'ai pas du tout abordé le multithreading, je pense que c'est un grand sujet à part. Il est toujours prévu d'écrire une sorte de structure de données ou d'algorithme plus compliqué que la liste, si vous avez des idées, partagez-les dans les commentaires. Il serait intéressant de savoir quels types d'applications sont généralement écrits en rouille.


Lisez


Si vous êtes intéressé par Rust, voici quelques liens:



UPD: Merci à tous pour vos commentaires. J'ai appris beaucoup de choses utiles par moi-même. Corrections d'inexactitudes et de fautes de frappe, ajout de liens. Je pense que de telles discussions contribuent grandement à l'étude des nouvelles technologies.

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


All Articles